@jskit-ai/connectors-catalog 0.2.2 → 0.2.4

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 (192) hide show
  1. package/README.md +4 -4
  2. package/docs/ai.md +3 -3
  3. package/docs/airtable.md +1 -1
  4. package/docs/algolia.md +2 -2
  5. package/docs/amazon-redshift.md +2 -2
  6. package/docs/apollo-io.md +2 -2
  7. package/docs/asana.md +2 -2
  8. package/docs/ashby.md +1 -1
  9. package/docs/attention.md +2 -2
  10. package/docs/aws-athena.md +1 -1
  11. package/docs/aws-credentials.md +3 -3
  12. package/docs/aws-s3.md +1 -1
  13. package/docs/bigquery.md +2 -2
  14. package/docs/brevo.md +2 -2
  15. package/docs/calendly.md +3 -3
  16. package/docs/canva.md +2 -2
  17. package/docs/chargebee.md +2 -2
  18. package/docs/clay.md +2 -2
  19. package/docs/clickhouse.md +1 -1
  20. package/docs/confidence-flags.md +1 -1
  21. package/docs/contentful.md +1 -1
  22. package/docs/dbt-semantic-layer.md +2 -2
  23. package/docs/elevenlabs.md +2 -2
  24. package/docs/firebase-cloud-messaging.md +3 -3
  25. package/docs/firecrawl.md +4 -4
  26. package/docs/fireflies.md +1 -1
  27. package/docs/gatewayapi.md +1 -1
  28. package/docs/gemini-enterprise.md +1 -1
  29. package/docs/github-api.md +1 -1
  30. package/docs/gitlab-api.md +1 -1
  31. package/docs/gmail.md +1 -1
  32. package/docs/gong.md +1 -1
  33. package/docs/google-ads.md +3 -3
  34. package/docs/google-analytics.md +2 -2
  35. package/docs/google-docs.md +1 -1
  36. package/docs/google-drive.md +1 -1
  37. package/docs/google-maps-platform.md +2 -2
  38. package/docs/google-search-console.md +1 -1
  39. package/docs/google-sheets.md +1 -1
  40. package/docs/google-slides.md +2 -2
  41. package/docs/granola.md +2 -2
  42. package/docs/hex.md +2 -2
  43. package/docs/heygen.md +2 -2
  44. package/docs/inngest.md +1 -1
  45. package/docs/klipy.md +1 -1
  46. package/docs/lightspeed.md +2 -2
  47. package/docs/linear.md +1 -1
  48. package/docs/linkedin.md +2 -2
  49. package/docs/mailgun.md +1 -1
  50. package/docs/microsoft-excel.md +2 -2
  51. package/docs/microsoft-fabric.md +1 -1
  52. package/docs/microsoft-outlook.md +1 -1
  53. package/docs/microsoft-powerpoint.md +1 -1
  54. package/docs/microsoft-teams.md +1 -1
  55. package/docs/microsoft-word.md +1 -1
  56. package/docs/notion.md +1 -1
  57. package/docs/oura.md +3 -3
  58. package/docs/paddle.md +3 -3
  59. package/docs/pipedrive.md +2 -2
  60. package/docs/prestashop.md +1 -1
  61. package/docs/replicate.md +1 -1
  62. package/docs/resend.md +1 -1
  63. package/docs/salesforce.md +1 -1
  64. package/docs/sanity.md +3 -3
  65. package/docs/semrush.md +2 -2
  66. package/docs/sentry.md +1 -1
  67. package/docs/slack.md +1 -1
  68. package/docs/snowflake.md +5 -5
  69. package/docs/storyblok.md +1 -1
  70. package/docs/stripe.md +2 -2
  71. package/docs/tally.md +1 -1
  72. package/docs/telegram.md +2 -2
  73. package/docs/tiktok.md +3 -3
  74. package/docs/twilio.md +3 -3
  75. package/docs/twitch.md +2 -2
  76. package/docs/wave.md +3 -3
  77. package/docs/wix.md +13 -13
  78. package/docs/wordpress-com.md +1 -1
  79. package/docs/wordpress-self-hosted.md +1 -1
  80. package/docs/workday.md +2 -2
  81. package/docs/x-twitter.md +3 -3
  82. package/docs/xero.md +3 -3
  83. package/docs/zoho-books.md +2 -2
  84. package/docs/zoho-crm.md +2 -2
  85. package/package.json +4 -3
  86. package/patterns/ai-connections/PATTERN.md +16 -1
  87. package/patterns/ai-connections/example/ai-model.js +12 -3
  88. package/patterns/ai-connections/example/perplexity-answer.js +15 -4
  89. package/patterns/api-key-connection/PATTERN.md +9 -1
  90. package/patterns/api-key-connection/example/connections.js +5 -2
  91. package/patterns/aws-storage-queries/PATTERN.md +13 -3
  92. package/patterns/aws-storage-queries/example/s3-transfer.js +14 -3
  93. package/patterns/event-delivery/PATTERN.md +1 -1
  94. package/patterns/firebase-messaging/PATTERN.md +10 -0
  95. package/patterns/firebase-messaging/example/server/notifications.js +8 -0
  96. package/patterns/google-ads-search/PATTERN.md +18 -7
  97. package/patterns/google-ads-search/example/ads-setup.js +13 -5
  98. package/patterns/oauth-connection/PATTERN.md +1 -1
  99. package/patterns/paddle-catalogue/PATTERN.md +11 -2
  100. package/patterns/paddle-catalogue/example/create-products.js +5 -2
  101. package/patterns/public-image/PATTERN.md +10 -1
  102. package/patterns/public-image/example/logo-url.js +4 -1
  103. package/patterns/redshift-queries/PATTERN.md +1 -1
  104. package/patterns/source-scanning/PATTERN.md +13 -2
  105. package/patterns/source-scanning/example/source-scanner.js +9 -0
  106. package/src/server/ai.js +19 -5
  107. package/src/server/apify.js +2 -2
  108. package/src/server/asana.js +6 -6
  109. package/src/server/ashby.js +2 -1
  110. package/src/server/attention.js +12 -11
  111. package/src/server/awsTransport.js +6 -0
  112. package/src/server/bigquery.js +5 -4
  113. package/src/server/brevo.js +2 -1
  114. package/src/server/clay.js +2 -1
  115. package/src/server/clickhouse.js +2 -1
  116. package/src/server/confidence.js +6 -1
  117. package/src/server/connectorValues.js +33 -0
  118. package/src/server/databricks.js +7 -7
  119. package/src/server/dbt-semantic-layer.js +5 -5
  120. package/src/server/elevenlabs.js +2 -2
  121. package/src/server/firebase-cloud-messaging.js +2 -2
  122. package/src/server/gemini-enterprise.js +5 -5
  123. package/src/server/google-ads-search.js +68 -48
  124. package/src/server/google-ads.js +15 -17
  125. package/src/server/google-drive.js +7 -7
  126. package/src/server/google-maps-platform.js +4 -2
  127. package/src/server/google-search-console.js +4 -3
  128. package/src/server/google.js +8 -4
  129. package/src/server/googleAdsApi.js +14 -0
  130. package/src/server/granola.js +2 -2
  131. package/src/server/hubspot.js +2 -1
  132. package/src/server/incident-io.js +3 -2
  133. package/src/server/lexware.js +5 -4
  134. package/src/server/lightspeed.js +7 -5
  135. package/src/server/linkedin.js +2 -2
  136. package/src/server/mcpTransport.js +8 -4
  137. package/src/server/microsoft-fabric.js +3 -3
  138. package/src/server/microsoft-onedrive.js +2 -2
  139. package/src/server/microsoft-onenote.js +4 -3
  140. package/src/server/microsoft-outlook.js +6 -6
  141. package/src/server/microsoft-sharepoint.js +4 -2
  142. package/src/server/microsoft-teams.js +2 -1
  143. package/src/server/microsoft.js +20 -13
  144. package/src/server/notion.js +2 -1
  145. package/src/server/pipedrive.js +2 -1
  146. package/src/server/salesforce.js +12 -12
  147. package/src/server/semrush.js +9 -8
  148. package/src/server/snowflake.js +20 -20
  149. package/src/server/storyblok.js +2 -1
  150. package/src/server/tally.js +2 -1
  151. package/src/server/telegram.js +2 -6
  152. package/src/server/tiktok.js +3 -3
  153. package/src/server/twilio.js +3 -2
  154. package/src/server/twitch.js +5 -3
  155. package/src/server/types.js +13 -0
  156. package/src/server/wix.js +20 -878
  157. package/src/server/wixBookings.js +449 -0
  158. package/src/server/wixCms.js +132 -0
  159. package/src/server/wixStores.js +296 -0
  160. package/src/server/wixValues.js +35 -0
  161. package/src/server/wiz.js +18 -4
  162. package/src/server/woocommerce.js +1 -1
  163. package/src/server/wordpress-com.js +2 -1
  164. package/src/server/workday.js +9 -9
  165. package/src/server/x-twitter.js +2 -1
  166. package/src/server/xero.js +40 -40
  167. package/src/server/zoho-books.js +6 -6
  168. package/src/server/zoho-crm.js +5 -5
  169. package/src/shared/confidence.js +6 -1
  170. package/src/shared/dbt-semantic-layer.js +1 -1
  171. package/src/shared/figma.js +1 -1
  172. package/src/shared/google.js +8 -4
  173. package/src/shared/googleScope.js +4 -0
  174. package/src/shared/microsoft-fabric.js +4 -5
  175. package/src/shared/microsoft.js +8 -3
  176. package/src/shared/salesforce.js +1 -1
  177. package/src/shared/tokens.js +7 -3
  178. package/src/shared/uuid.js +13 -0
  179. package/src/shared/wix.js +4 -3
  180. package/src/shared/zoho-books.js +3 -4
  181. package/src/shared/zoho-crm.js +1 -1
  182. package/src/shared/{zoho-regions.js → zoho.js} +4 -1
  183. package/test/ai.test.js +1 -1
  184. package/test/brevo.test.js +1 -1
  185. package/test/chargebee.test.js +1 -1
  186. package/test/google-ads.test.js +8 -4
  187. package/test/linkedin.test.js +1 -1
  188. package/test/resend.test.js +2 -2
  189. package/test/salesforce.test.js +1 -1
  190. package/test/snowflake.test.js +7 -7
  191. package/test/tiktok.test.js +1 -1
  192. package/test/wiz.test.js +1 -1
@@ -1,12 +1,12 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
- import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
4
4
  import { xeroDefinition } from "../shared/xero.js";
5
5
  import { jsonOperation } from "./jsonOperation.js";
6
+ import { isUuid } from "../shared/uuid.js";
7
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
6
8
 
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." };
9
+ const tenantId = { type: "string", required: true, validator: (value) => isUuid(value) || "Choose an organisation ID returned by connections.list." };
10
10
  const paging = {
11
11
  page: { type: "integer", min: 1, max: 1_000_000, defaultTo: 1 },
12
12
  pageSize: { type: "integer", min: 1, max: 200, defaultTo: 100 }
@@ -22,12 +22,12 @@ const reportFields = {
22
22
  timeframe: { type: "string", enum: ["MONTH", "QUARTER", "YEAR"] },
23
23
  standardLayout: { type: "boolean" }, paymentsOnly: { type: "boolean" }
24
24
  };
25
- const reportRow = row => row !== null && typeof row === "object" && !Array.isArray(row) && typeof row.RowType === "string" &&
25
+ const reportRow = row => isRecord(row) && typeof row.RowType === "string" &&
26
26
  (row.Cells === undefined || Array.isArray(row.Cells) && row.Cells.every(cell => typeof cell?.Value === "string"));
27
27
  const reportResult = value => Array.isArray(value?.Reports) && value.Reports.length === 1 && value.Reports.every(report =>
28
28
  typeof report?.ReportID === "string" && Array.isArray(report.Rows) && report.Rows.every(row => reportRow(row) &&
29
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) &&
30
+ const validConnections = (value) => Array.isArray(value) && value.every((entry) => isUuid(entry?.id) && isUuid(entry.tenantId) &&
31
31
  ["ORGANISATION", "PRACTICEMANAGER", "PRACTICE"].includes(entry.tenantType) && (entry.tenantName === null || typeof entry.tenantName === "string"));
32
32
 
33
33
  function accountingRead(resource, fields, scopes, validateResult) {
@@ -49,7 +49,7 @@ function accountingRead(resource, fields, scopes, validateResult) {
49
49
  }
50
50
 
51
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." } });
52
+ const schema = createSchema({ tenantId, ...fields, id: { type: "string", required: true, validator: value => isUuid(value) || "Use the record ID returned by Xero." } });
53
53
  return {
54
54
  scopes,
55
55
  request(input) {
@@ -62,7 +62,7 @@ function accountingRecord(resource, idName, scopes, fields = {}) {
62
62
  return { method: "GET", url: url.href,
63
63
  headers: { "Xero-tenant-id": tenant.toLowerCase() } };
64
64
  },
65
- validateResult: value => Array.isArray(value?.[resource]) && value[resource].length === 1 && uuid(value[resource][0]?.[idName])
65
+ validateResult: value => Array.isArray(value?.[resource]) && value[resource].length === 1 && isUuid(value[resource][0]?.[idName])
66
66
  };
67
67
  }
68
68
 
@@ -70,7 +70,7 @@ function contactWrite(update) {
70
70
  const schema = createSchema({
71
71
  tenantId,
72
72
  idempotencyKey: idempotencyField,
73
- ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Use the contact ID returned by Xero." } } : {}),
73
+ ...(update ? { id: { type: "string", required: true, validator: value => isUuid(value) || "Use the contact ID returned by Xero." } } : {}),
74
74
  Name: { type: "string", required: !update, minLength: 1, maxLength: 255 },
75
75
  FirstName: { type: "string", maxLength: 255 }, LastName: { type: "string", maxLength: 255 },
76
76
  EmailAddress: { type: "string", maxLength: 255 }, ContactNumber: { type: "string", maxLength: 50 }
@@ -83,15 +83,15 @@ function contactWrite(update) {
83
83
  return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/Contacts${update ? `/${id.toLowerCase()}` : ""}`,
84
84
  headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { Contacts: [contact] } };
85
85
  },
86
- validateResult: value => Array.isArray(value?.Contacts) && value.Contacts.length === 1 && uuid(value.Contacts[0]?.ContactID) &&
86
+ validateResult: value => Array.isArray(value?.Contacts) && value.Contacts.length === 1 && isUuid(value.Contacts[0]?.ContactID) &&
87
87
  value.Contacts[0].HasValidationErrors !== true && !value.Contacts[0].ValidationErrors?.length
88
88
  };
89
89
  }
90
90
 
91
91
  const paymentSchema = createSchema({
92
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." },
93
+ invoiceId: { type: "string", required: true, validator: value => isUuid(value) || "Choose an invoice ID." },
94
+ accountId: { type: "string", required: true, validator: value => isUuid(value) || "Choose an account ID." },
95
95
  Date: dateField,
96
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
97
  CurrencyRate: { type: "number", validator: value => Number.isFinite(value) && value > 0 || "Use a positive finite exchange rate." },
@@ -101,9 +101,9 @@ const finiteAmount = { type: "number", validator: value => Number.isFinite(value
101
101
  function invoiceWrite(update) {
102
102
  const schema = createSchema({
103
103
  tenantId, idempotencyKey: idempotencyField,
104
- ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Choose the invoice ID." } } : {}),
104
+ ...(update ? { id: { type: "string", required: true, validator: value => isUuid(value) || "Choose the invoice ID." } } : {}),
105
105
  Type: { type: "string", required: !update, enum: ["ACCREC", "ACCPAY"] },
106
- contactId: { type: "string", required: !update, validator: value => uuid(value) || "Choose a contact ID." },
106
+ contactId: { type: "string", required: !update, validator: value => isUuid(value) || "Choose a contact ID." },
107
107
  Date: { ...dateField, required: !update }, DueDate: { ...dateField, required: false },
108
108
  CurrencyCode: { type: "string", required: !update, validator: value => /^[A-Z]{3}$/u.test(value) || "Use the three-letter invoice currency code." },
109
109
  LineAmountTypes: { type: "string", required: !update, enum: ["Exclusive", "Inclusive", "NoTax"] },
@@ -111,7 +111,7 @@ function invoiceWrite(update) {
111
111
  Reference: { type: "string", maxLength: 255 }, InvoiceNumber: { type: "string", maxLength: 255 },
112
112
  LineItems: { type: "array", required: !update, validator: values => values.length > 0 && values.length <= 100 || "Supply 1–100 invoice lines.",
113
113
  items: { type: "object", schema: createSchema({
114
- LineItemID: { type: "string", validator: value => uuid(value) || "Use the existing line item ID." },
114
+ LineItemID: { type: "string", validator: value => isUuid(value) || "Use the existing line item ID." },
115
115
  Description: { type: "string", required: true, minLength: 1, maxLength: 4000 },
116
116
  Quantity: { ...finiteAmount, required: true, min: 0.0001 },
117
117
  UnitAmount: { ...finiteAmount, required: true }, AccountCode: { type: "string", required: true, minLength: 1, maxLength: 50 },
@@ -131,7 +131,7 @@ function invoiceWrite(update) {
131
131
  return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/Invoices${update ? `/${id.toLowerCase()}` : ""}?unitdp=4`,
132
132
  headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { Invoices: [invoice] } };
133
133
  },
134
- validateResult: value => Array.isArray(value?.Invoices) && value.Invoices.length === 1 && uuid(value.Invoices[0]?.InvoiceID) &&
134
+ validateResult: value => Array.isArray(value?.Invoices) && value.Invoices.length === 1 && isUuid(value.Invoices[0]?.InvoiceID) &&
135
135
  value.Invoices[0].HasErrors !== true && !value.Invoices[0].ValidationErrors?.length
136
136
  };
137
137
  }
@@ -141,8 +141,8 @@ function attachmentOperation(action) {
141
141
  const schema = createSchema({
142
142
  tenantId,
143
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." } } : {}),
144
+ id: { type: "string", required: true, validator: value => isUuid(value) || "Choose the parent record ID." },
145
+ ...(action === "download" ? { attachmentId: { type: "string", required: true, validator: value => isUuid(value) || "Choose an attachment ID from attachments.list." } } : {}),
146
146
  ...(writing ? {
147
147
  idempotencyKey: idempotencyField,
148
148
  filename: { type: "string", required: true, minLength: 1, maxLength: 255,
@@ -172,24 +172,24 @@ function attachmentOperation(action) {
172
172
  validateResult: action === "download"
173
173
  ? value => typeof value?.contentBase64 === "string" && Number.isInteger(value.size) && value.size > 0
174
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)
175
+ isUuid(file?.AttachmentID) && typeof file.FileName === "string" && Number.isInteger(file.ContentLength) && file.ContentLength >= 0)
176
176
  };
177
177
  }
178
178
 
179
179
  function bankTransactionWrite(update) {
180
180
  const schema = createSchema({
181
181
  tenantId, idempotencyKey: idempotencyField,
182
- ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Choose a bank transaction ID." } } : {}),
182
+ ...(update ? { id: { type: "string", required: true, validator: value => isUuid(value) || "Choose a bank transaction ID." } } : {}),
183
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." },
184
+ contactId: { type: "string", required: !update, validator: value => isUuid(value) || "Choose a contact ID." },
185
+ bankAccountId: { type: "string", required: !update, validator: value => isUuid(value) || "Choose a bank account ID." },
186
186
  Date: { ...dateField, required: !update }, Reference: { type: "string", maxLength: 255 },
187
187
  CurrencyRate: { type: "number", validator: value => Number.isFinite(value) && value > 0 || "Use a positive finite exchange rate." },
188
188
  LineAmountTypes: { type: "string", required: !update, enum: ["Exclusive", "Inclusive", "NoTax"] },
189
189
  ...(update ? { Status: { type: "string", enum: ["AUTHORISED", "DELETED"] } } : {}),
190
190
  LineItems: { type: "array", required: !update, validator: values => values.length > 0 && values.length <= 100 || "Supply 1–100 transaction lines.",
191
191
  items: { type: "object", schema: createSchema({
192
- LineItemID: { type: "string", validator: value => uuid(value) || "Use the existing line item ID." },
192
+ LineItemID: { type: "string", validator: value => isUuid(value) || "Use the existing line item ID." },
193
193
  Description: { type: "string", required: true, minLength: 1, maxLength: 4000 },
194
194
  Quantity: { ...finiteAmount, required: true, min: 0.0001 }, UnitAmount: { ...finiteAmount, required: true },
195
195
  AccountCode: { type: "string", required: true, minLength: 1, maxLength: 50 }, TaxType: { type: "string", maxLength: 50 }
@@ -207,7 +207,7 @@ function bankTransactionWrite(update) {
207
207
  return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/BankTransactions${update ? `/${id.toLowerCase()}` : ""}?unitdp=4`,
208
208
  headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { BankTransactions: [transaction] } };
209
209
  },
210
- validateResult: value => Array.isArray(value?.BankTransactions) && value.BankTransactions.length === 1 && uuid(value.BankTransactions[0]?.BankTransactionID) &&
210
+ validateResult: value => Array.isArray(value?.BankTransactions) && value.BankTransactions.length === 1 && isUuid(value.BankTransactions[0]?.BankTransactionID) &&
211
211
  !value.BankTransactions[0].ValidationErrors?.length
212
212
  };
213
213
  }
@@ -215,7 +215,7 @@ function bankTransactionWrite(update) {
215
215
  function manualJournalWrite(update) {
216
216
  const schema = createSchema({
217
217
  tenantId, idempotencyKey: idempotencyField,
218
- ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Choose a manual journal ID." } } : {}),
218
+ ...(update ? { id: { type: "string", required: true, validator: value => isUuid(value) || "Choose a manual journal ID." } } : {}),
219
219
  Narration: { type: "string", required: !update, minLength: 1, maxLength: 4000 },
220
220
  Date: { ...dateField, required: !update },
221
221
  LineAmountTypes: { type: "string", required: !update, enum: ["Exclusive", "Inclusive", "NoTax"] },
@@ -244,7 +244,7 @@ function manualJournalWrite(update) {
244
244
  return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/ManualJournals${update ? `/${id.toLowerCase()}` : ""}`,
245
245
  headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { ManualJournals: [journal] } };
246
246
  },
247
- validateResult: value => Array.isArray(value?.ManualJournals) && value.ManualJournals.length === 1 && uuid(value.ManualJournals[0]?.ManualJournalID) &&
247
+ validateResult: value => Array.isArray(value?.ManualJournals) && value.ManualJournals.length === 1 && isUuid(value.ManualJournals[0]?.ManualJournalID) &&
248
248
  !value.ManualJournals[0].ValidationErrors?.length
249
249
  };
250
250
  }
@@ -260,20 +260,20 @@ const xeroProvider = Object.freeze({
260
260
  "attachments.replace": attachmentOperation("replace"),
261
261
  "connections.list": jsonOperation(connectionsUrl, {}, validConnections),
262
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")),
263
+ value => Array.isArray(value?.Accounts) && value.Accounts.every(account => isUuid(account?.AccountID) && typeof account.Type === "string")),
264
264
  "currencies.list": accountingRead("Currencies", {}, ["accounting.settings.read", "accounting.settings"],
265
265
  value => Array.isArray(value?.Currencies) && value.Currencies.every(currency => /^[A-Z]{3}$/u.test(currency?.Code) && typeof currency.Description === "string")),
266
266
  "taxRates.list": accountingRead("TaxRates", {}, ["accounting.settings.read", "accounting.settings"],
267
267
  value => Array.isArray(value?.TaxRates) && value.TaxRates.every(rate => typeof rate?.TaxType === "string" && typeof rate.Name === "string")),
268
268
  "trackingCategories.list": accountingRead("TrackingCategories", { includeArchived: { type: "boolean", defaultTo: false } },
269
269
  ["accounting.settings.read", "accounting.settings"],
270
- value => Array.isArray(value?.TrackingCategories) && value.TrackingCategories.every(category => uuid(category?.TrackingCategoryID) && typeof category.Name === "string")),
270
+ value => Array.isArray(value?.TrackingCategories) && value.TrackingCategories.every(category => isUuid(category?.TrackingCategoryID) && typeof category.Name === "string")),
271
271
  "reports.agedReceivables": accountingRead("Reports/AgedReceivablesByContact", {
272
- contactId: { type: "string", required: true, validator: value => uuid(value) || "Choose a contact ID." },
272
+ contactId: { type: "string", required: true, validator: value => isUuid(value) || "Choose a contact ID." },
273
273
  date: dateField, fromDate: { ...dateField, required: false }, toDate: { ...dateField, required: false }
274
274
  }, ["accounting.reports.aged.read"], reportResult),
275
275
  "reports.agedPayables": accountingRead("Reports/AgedPayablesByContact", {
276
- contactId: { type: "string", required: true, validator: value => uuid(value) || "Choose a contact ID." },
276
+ contactId: { type: "string", required: true, validator: value => isUuid(value) || "Choose a contact ID." },
277
277
  date: dateField, fromDate: { ...dateField, required: false }, toDate: { ...dateField, required: false }
278
278
  }, ["accounting.reports.aged.read"], reportResult),
279
279
  "reports.executiveSummary": accountingRead("Reports/ExecutiveSummary", { date: dateField },
@@ -285,7 +285,7 @@ const xeroProvider = Object.freeze({
285
285
  "budgets.list": accountingRead("Budgets", {
286
286
  DateFrom: { ...dateField, required: false }, DateTo: { ...dateField, required: false }
287
287
  }, ["accounting.budgets.read"],
288
- value => Array.isArray(value?.Budgets) && value.Budgets.every(budget => uuid(budget?.BudgetID) && ["OVERALL", "TRACKING"].includes(budget.Type))),
288
+ value => Array.isArray(value?.Budgets) && value.Budgets.every(budget => isUuid(budget?.BudgetID) && ["OVERALL", "TRACKING"].includes(budget.Type))),
289
289
  "budgets.get": accountingRecord("Budgets", "BudgetID", ["accounting.budgets.read"], {
290
290
  DateFrom: { ...dateField, required: false }, DateTo: { ...dateField, required: false }
291
291
  }),
@@ -297,13 +297,13 @@ const xeroProvider = Object.freeze({
297
297
  "bankTransactions.update": bankTransactionWrite(true),
298
298
  "bankTransactions.list": accountingRead("BankTransactions", { page: paging.page },
299
299
  ["accounting.banktransactions.read", "accounting.banktransactions"],
300
- value => Array.isArray(value?.BankTransactions) && value.BankTransactions.length <= 100 && value.BankTransactions.every(record => uuid(record?.BankTransactionID))),
300
+ value => Array.isArray(value?.BankTransactions) && value.BankTransactions.length <= 100 && value.BankTransactions.every(record => isUuid(record?.BankTransactionID))),
301
301
  "bankTransactions.get": accountingRecord("BankTransactions", "BankTransactionID", ["accounting.banktransactions.read", "accounting.banktransactions"]),
302
302
  "manualJournals.create": manualJournalWrite(false),
303
303
  "manualJournals.update": manualJournalWrite(true),
304
304
  "manualJournals.list": accountingRead("ManualJournals", { page: paging.page },
305
305
  ["accounting.manualjournals.read", "accounting.manualjournals"],
306
- value => Array.isArray(value?.ManualJournals) && value.ManualJournals.length <= 100 && value.ManualJournals.every(record => uuid(record?.ManualJournalID))),
306
+ value => Array.isArray(value?.ManualJournals) && value.ManualJournals.length <= 100 && value.ManualJournals.every(record => isUuid(record?.ManualJournalID))),
307
307
  "manualJournals.get": accountingRecord("ManualJournals", "ManualJournalID", ["accounting.manualjournals.read", "accounting.manualjournals"]),
308
308
  "payments.create": {
309
309
  scopes: ["accounting.payments"],
@@ -313,7 +313,7 @@ const xeroProvider = Object.freeze({
313
313
  headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey },
314
314
  body: { Payments: [{ ...payment, Invoice: { InvoiceID: invoiceId }, Account: { AccountID: accountId } }] } };
315
315
  },
316
- validateResult: value => Array.isArray(value?.Payments) && value.Payments.length === 1 && uuid(value.Payments[0]?.PaymentID) &&
316
+ validateResult: value => Array.isArray(value?.Payments) && value.Payments.length === 1 && isUuid(value.Payments[0]?.PaymentID) &&
317
317
  Number.isFinite(value.Payments[0].Amount) && value.Payments[0].HasValidationErrors !== true && !value.Payments[0].ValidationErrors?.length
318
318
  },
319
319
  "contacts.get": accountingRecord("Contacts", "ContactID", ["accounting.contacts.read", "accounting.contacts"]),
@@ -329,13 +329,13 @@ const xeroProvider = Object.freeze({
329
329
  "payments.list": accountingRead("Payments", { page: paging.page, pageSize: { ...paging.pageSize, max: 100 } },
330
330
  ["accounting.payments.read", "accounting.payments"],
331
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")),
332
+ isUuid(entry?.PaymentID) && Number.isFinite(entry.Amount) && typeof entry.Status === "string")),
333
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)),
334
+ (value) => Array.isArray(value?.Organisations) && value.Organisations.length === 1 && isUuid(value.Organisations[0]?.OrganisationID)),
335
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))),
336
+ ["accounting.contacts.read", "accounting.contacts"], (value) => Array.isArray(value?.Contacts) && value.Contacts.length <= 200 && value.Contacts.every((entry) => isUuid(entry?.ContactID))),
337
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)))
338
+ (value) => Array.isArray(value?.Invoices) && value.Invoices.length <= 200 && value.Invoices.every((entry) => isUuid(entry?.InvoiceID)))
339
339
  },
340
340
  async exchange(address, options, { request, fetchImpl }) {
341
341
  if (address !== connectionsUrl) {
@@ -354,7 +354,7 @@ const xeroProvider = Object.freeze({
354
354
  headers: { ...options.headers, Accept: downloading ? "application/octet-stream" : "application/json" } });
355
355
  if (!response.ok) {
356
356
  await response.body?.cancel();
357
- throw Object.assign(new Error("Xero attachment request failed."), { status: response.status });
357
+ throw providerStatusError(response.status);
358
358
  }
359
359
  const reader = response.body?.getReader(); const chunks = []; let size = 0;
360
360
  if (reader) try {
@@ -380,7 +380,7 @@ const xeroProvider = Object.freeze({
380
380
  if (record) {
381
381
  const idName = { Contacts: "ContactID", Invoices: "InvoiceID", Budgets: "BudgetID", BankTransactions: "BankTransactionID", ManualJournals: "ManualJournalID" }[record[1]];
382
382
  const returnedId = result?.[record[1]]?.[0]?.[idName];
383
- if (!uuid(returnedId) || returnedId.toLowerCase() !== record[2].toLowerCase()) {
383
+ if (!isUuid(returnedId) || returnedId.toLowerCase() !== record[2].toLowerCase()) {
384
384
  throw new ConnectorError("connector_response_invalid", "Xero returned a different record.", { statusCode: 502, messageCode: "connectors.shared.differentRecord", messageParams: { provider: "Xero" } });
385
385
  }
386
386
  }
@@ -1,15 +1,15 @@
1
1
  import { createSchema } from "json-rest-schema";
2
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";
3
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
4
+ import { zohoBooksDefinition, zohoBooksOrganizationField } from "../shared/zoho-books.js";
5
+ import { isZohoId, zohoRegions } from "../shared/zoho.js";
6
6
  import { jsonOperation } from "./jsonOperation.js";
7
7
  import { zohoAuthorizationParameters, zohoOAuthMetadata, normalizeZohoTokenResponse } from "./zohoOAuth.js";
8
8
 
9
9
  const regionFor = ({ region = "eu" } = {}) => zohoRegions.find(({ value }) => value === region);
10
10
  const apiOrigin = (settings) => `https://www.zohoapis.${regionFor(settings).suffix}`;
11
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");
12
+ isZohoId(org?.organization_id) && typeof org.name === "string" && typeof org.is_org_active === "boolean");
13
13
 
14
14
  function booksList(resource, identifier) {
15
15
  const schema = createSchema({
@@ -33,7 +33,7 @@ function booksList(resource, identifier) {
33
33
  return { method: "GET", url: url.href };
34
34
  },
35
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"
36
+ value[resource].every((record) => isZohoId(record?.[identifier])) && typeof value.page_context?.has_more_page === "boolean"
37
37
  };
38
38
  }
39
39
 
@@ -57,7 +57,7 @@ const zohoBooksProvider = Object.freeze({
57
57
  const response = await fetchImpl(address, { ...options, headers, credentials: "omit" });
58
58
  if (!response.ok) {
59
59
  if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Books resource is not available.", { statusCode: 404, messageCode: "connectors.zohoBooks.resourceNotAvailable" });
60
- throw Object.assign(new Error("Zoho Books request failed."), { status: response.status });
60
+ throw providerStatusError(response.status);
61
61
  }
62
62
  let value;
63
63
  try { value = await response.json(); } catch { /* validated below */ }
@@ -1,13 +1,13 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
- import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
4
+ import { isZohoId } from "../shared/zoho.js";
4
5
  import { zohoCrmDefinition, zohoCrmRegions } from "../shared/zoho-crm.js";
5
6
  import { jsonOperation } from "./jsonOperation.js";
6
7
  import { zohoAuthorizationParameters, zohoOAuthMetadata, normalizeZohoTokenResponse } from "./zohoOAuth.js";
7
8
 
8
9
  const regionFor = ({ region = "us" } = {}) => zohoCrmRegions.find(({ value }) => value === region);
9
10
  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
11
  const pageToken = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !/[\s\p{Cc}]/u.test(value);
12
12
 
13
13
  function recordsList(module, defaults) {
@@ -32,7 +32,7 @@ function recordsList(module, defaults) {
32
32
  for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
33
33
  return { method: "GET", url: url.href };
34
34
  },
35
- validateResult: (result) => Array.isArray(result?.data) && result.data.length <= 200 && result.data.every((record) => identifier(record?.id)) &&
35
+ validateResult: (result) => Array.isArray(result?.data) && result.data.length <= 200 && result.data.every((record) => isZohoId(record?.id)) &&
36
36
  Number.isSafeInteger(result.info?.count) && result.info.count === result.data.length &&
37
37
  Number.isSafeInteger(result.info.per_page) && result.info.per_page >= 1 && result.info.per_page <= 200 &&
38
38
  typeof result.info.more_records === "boolean" &&
@@ -51,7 +51,7 @@ const zohoCrmProvider = Object.freeze({
51
51
  operations: {
52
52
  "users.current": {
53
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)),
54
+ (value) => Array.isArray(value?.users) && value.users.length === 1 && isZohoId(value.users[0]?.id)),
55
55
  scopes: ["ZohoCRM.users.READ", "ZohoCRM.users.ALL"]
56
56
  },
57
57
  "leads.list": recordsList("Leads", "Last_Name,Email"),
@@ -73,7 +73,7 @@ const zohoCrmProvider = Object.freeze({
73
73
  if (!response.ok) {
74
74
  if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This CRM resource is not available.", { statusCode: 404, messageCode: "connectors.zohoCrm.resourceNotAvailable" });
75
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 });
76
+ throw providerStatusError(response.status);
77
77
  }
78
78
  if (response.status !== 200 || !value || value.status === "error" || value.code) {
79
79
  throw new ConnectorError("connector_response_invalid", "Zoho CRM returned an unexpected response.", { statusCode: 502, messageCode: "connectors.shared.unexpectedResponse", messageParams: { provider: "Zoho CRM" } });
@@ -7,7 +7,12 @@ const confidenceScopes = [
7
7
 
8
8
  function definition(id, name, description) {
9
9
  return Object.freeze({
10
- id, name, description, accountModes: ["assistant"], authenticationMethods: ["oauth2"], scopes: confidenceScopes,
10
+ id,
11
+ name,
12
+ description,
13
+ accountModes: ["assistant"],
14
+ authenticationMethods: ["oauth2"],
15
+ scopes: confidenceScopes,
11
16
  setup: { url: "https://confidence.spotify.com/docs/sdks/mcp-servers", clientRegistrationEndpoint: "https://mcp.confidence.dev/register", steps: [
12
17
  "Use a Confidence account with access to the intended organisation. This assistant connection uses browser OAuth, not a flag client secret or management API token.",
13
18
  "Confirm the suggested callback is the route your application will serve. Choose Register client and connect to create a project-owned client and save its ID, secret and callback through project Env. Existing Env values are preserved. Manual users can send the displayed registration JSON with Content-Type application/json; CLI users can call registerConfidenceClient.",
@@ -25,7 +25,7 @@ const dbtSemanticLayerDefinition = Object.freeze({
25
25
  "Return to Project details and copy the GraphQL hostname and Environment ID from the Semantic Layer connection details. Enter them here, preserving every ID digit. Enter env:DBT_SERVICE_TOKEN in Service token reference and save configuration. Choose Set credential in Env, paste the token as DBT_SERVICE_TOKEN and save it there.",
26
26
  "Return here and choose Connect account / Verify again. Check connection only reads saved status. Verification reads environment metadata; an explicitly wired application can then create metric queries and request their status and JSON result pages. If denied, check the host, environment, linked warehouse credential and token permissions. Replace the token through Env when rotating it; local Disconnect does not revoke it in dbt.",
27
27
  "Query execution uses your warehouse capacity. The app owns permitted metrics, trusted dimension/time filters and bounded polling. JSON results suit small tables; use dbt’s native Arrow tooling for large production workloads. Do not retry an uncertain query submission automatically. Your application must explicitly expose approved dbt tools to its assistant.",
28
- "This token flow has no OAuth callback or app-user login. Public and Online need the customer's own account, permissions and capacity."
28
+ "This token flow has no OAuth callback or app-user login. Every deployment needs the customer's own account, permissions and capacity."
29
29
  ] }
30
30
  });
31
31
 
@@ -12,7 +12,7 @@ const figmaDefinition = Object.freeze({
12
12
  "Save, begin authorization, sign into Figma and review its consent screen. A personal access token cannot replace this MCP OAuth registration.",
13
13
  "The host must authorize each tool and its arguments. The MCP permission can expose write tools; listing a tool does not authorize an edit.",
14
14
  "Use a Figma file/frame link to identify the allowed file and node. The assistant host consumes returned design text/images and chooses its framework; connecting alone does not generate screens. Assistant tool attachment requires explicit host integration.",
15
- "Remote MCP supports both local and hosted assistant applications. Desktop MCP must run beside the consuming desktop client; a VPS localhost address points to the VPS, not your laptop. Do not enter a desktop localhost URL as the hosted remote endpoint.",
15
+ "Remote MCP supports both local and hosted assistant applications. Desktop MCP must run beside the consuming desktop client; a server's localhost address points to that server, not your laptop. Do not enter a desktop localhost URL as the hosted remote endpoint.",
16
16
  "Disconnect removes the local grant; it does not undo design edits or revoke all provider access. This uses Figma's remote server. Desktop MCP, app-user login and automatic assistant attachment are separate capabilities."
17
17
  ]
18
18
  }
@@ -1,10 +1,14 @@
1
1
  import { createSchema } from "json-rest-schema";
2
- const scope = (name) => name.startsWith("https:") ? name : `https://www.googleapis.com/auth/${name}`;
2
+ import { googleScope } from "./googleScope.js";
3
3
  function definition(id, name, api, description, recommended, permissions, verificationFields = [], setupNotes = []) {
4
4
  return Object.freeze({
5
- id, name, description, verificationFields,
6
- accountModes: ["shared", "per-user", "assistant"], authenticationMethods: ["oauth2"],
7
- scopes: permissions.map(([value, label, required = false]) => ({ value: scope(value), label,
5
+ id,
6
+ name,
7
+ description,
8
+ verificationFields,
9
+ accountModes: ["shared", "per-user", "assistant"],
10
+ authenticationMethods: ["oauth2"],
11
+ scopes: permissions.map(([value, label, required = false]) => ({ value: googleScope(value), label,
8
12
  recommended: Array.isArray(recommended) ? recommended.includes(value) : value === recommended, ...(required ? { required: true } : {}) })),
9
13
  setup: {
10
14
  url: "https://developers.google.com/identity/protocols/oauth2/web-server",
@@ -0,0 +1,4 @@
1
+ // A Google OAuth scope's full URL; a scope already given as a URL is kept.
2
+ const googleScope = (name) => name.startsWith("https:") ? name : `https://www.googleapis.com/auth/${name}`;
3
+
4
+ export { googleScope };
@@ -1,10 +1,9 @@
1
1
  import { createSchema } from "json-rest-schema";
2
+ import { UUID_SOURCE, isUuid } from "./uuid.js";
2
3
 
3
4
  const delegatedScope = "https://analysis.windows.net/powerbi/api/GraphQLApi.Execute.All";
4
5
  const serviceScope = "https://api.fabric.microsoft.com/.default";
5
- const guid = "[a-fA-F0-9]{8}-(?:[a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}";
6
- const guidPattern = new RegExp(`^${guid}$`, "u");
7
- const endpointPattern = new RegExp(`^https://api\\.fabric\\.microsoft\\.com/v1/workspaces/${guid}/(?:graphqlapis|graphQLApis|GraphQLApis)/${guid}/graphql$`, "u");
6
+ const endpointPattern = new RegExp(`^https://api\\.fabric\\.microsoft\\.com/v1/workspaces/${UUID_SOURCE}/(?:graphqlapis|graphQLApis|GraphQLApis)/${UUID_SOURCE}/graphql$`, "u");
8
7
  const scopes = [
9
8
  { value: delegatedScope, label: "jskit.connectors.microsoftFabric.scopes.delegatedScope.label", recommended: true },
10
9
  { value: "offline_access", label: "jskit.connectors.microsoftFabric.scopes.offlineAccess.label", recommended: true },
@@ -16,11 +15,11 @@ const microsoftFabricDefinition = Object.freeze({
16
15
  description: "jskit.connectors.microsoftFabric.description",
17
16
  accountModes: ["shared", "per-user", "assistant"], authenticationMethods: ["oauth2"],
18
17
  oauthGrantTypes: ["authorization_code", "client_credentials"], oauthClientAuthenticationMethods: ["client_secret_post"],
19
- validateClientId: (value) => guidPattern.test(value) || "Enter the Application (client) ID as a GUID.",
18
+ validateClientId: (value) => isUuid(value) || "Enter the Application (client) ID as a GUID.",
20
19
  oauthGrantHint: "jskit.connectors.microsoftFabric.oauthGrantHint",
21
20
  settingsSchema: createSchema({
22
21
  tenantId: { type: "string", required: true, maxLength: 36,
23
- validator: (value) => guidPattern.test(value) || "Enter the Directory (tenant) ID as a GUID, not common or a client ID." },
22
+ validator: (value) => isUuid(value) || "Enter the Directory (tenant) ID as a GUID, not common or a client ID." },
24
23
  graphqlEndpoint: { type: "string", required: true, maxLength: 256,
25
24
  validator: (value) => endpointPattern.test(value) || "Copy the Fabric GraphQL API endpoint with workspace and API IDs, without a query string or fragment." }
26
25
  }),
@@ -1,4 +1,5 @@
1
1
  import { createSchema } from "json-rest-schema";
2
+ import { isUuid } from "./uuid.js";
2
3
 
3
4
  const registrationUrl = "https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app";
4
5
 
@@ -6,15 +7,19 @@ function definition(id, name, description, permission, label, organizationOnly =
6
7
  const authority = organizationOnly ? "organizations" : "common";
7
8
  const audiences = organizationOnly ? ["organizations"] : ["common", "organizations", "consumers"];
8
9
  return Object.freeze({
9
- id, name, description, verificationFields,
10
- accountModes: ["shared", "per-user", "assistant"], authenticationMethods: ["oauth2"],
10
+ id,
11
+ name,
12
+ description,
13
+ verificationFields,
14
+ accountModes: ["shared", "per-user", "assistant"],
15
+ authenticationMethods: ["oauth2"],
11
16
  scopes: [
12
17
  { value: permission, label, recommended: true },
13
18
  { value: "offline_access", label: "jskit.connectors.microsoft.scopes.offlineAccess.label", recommended: true }
14
19
  ],
15
20
  settingsSchema: createSchema({
16
21
  tenantId: { type: "string", maxLength: 36, defaultTo: authority,
17
- validator: (value) => audiences.includes(value) || /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/iu.test(value) ||
22
+ validator: (value) => audiences.includes(value) || isUuid(value) ||
18
23
  `Use ${audiences.join(", ")} or your Directory (tenant) ID as a GUID.` }
19
24
  }),
20
25
  settingsFields: [{ name: "tenantId", label: "jskit.connectors.microsoft.fields.tenantId.label", placeholder: authority,
@@ -29,7 +29,7 @@ const salesforceDefinition = Object.freeze({
29
29
  "Enable OAuth, register the exact backend callback, and select api plus refresh_token/offline_access. Keep PKCE enabled and require the secret for Web Server and Refresh Token flows.",
30
30
  "Open the app's Settings, then Consumer Key and Secret. Copy the key as Client ID; put the secret and callback in Env and save their references here.",
31
31
  "An administrator grants API access and authorizes the app/users through its policies and permission sets. Save configuration, then start consent from your application's connection flow.",
32
- "Public/Online registrations need separate provisioning and distribution decisions. Local apps cannot serve unrelated customer orgs; packaged apps require installation and subscriber policies. Org API quotas remain shared."
32
+ "Registrations shared beyond this org need separate provisioning and distribution decisions. Local apps cannot serve unrelated customer orgs; packaged apps require installation and subscriber policies. Org API quotas remain shared."
33
33
  ] }
34
34
  });
35
35
 
@@ -2,8 +2,13 @@ import { createSchema } from "json-rest-schema";
2
2
 
3
3
  function definition(id, name, description, url, steps) {
4
4
  return Object.freeze({
5
- id, name, description, accountModes: ["shared", "assistant"],
6
- authenticationMethods: ["api-key"], scopes: [], setup: { url, steps }
5
+ id,
6
+ name,
7
+ description,
8
+ accountModes: ["shared", "assistant"],
9
+ authenticationMethods: ["api-key"],
10
+ scopes: [],
11
+ setup: { url, steps }
7
12
  });
8
13
  }
9
14
 
@@ -725,7 +730,6 @@ const clayDefinition = Object.freeze({
725
730
  ]),
726
731
  apiKeyReferenceLabel: "jskit.connectors.clay.apiKeyReferenceLabel"
727
732
  });
728
-
729
733
  const tokenDefinitions = Object.freeze([
730
734
  airtableDefinition, notionDefinition, brevoDefinition, elevenlabsDefinition, githubApiDefinition, apifyDefinition,
731
735
  calendlyDefinition, hubspotDefinition, linearDefinition, pipedriveDefinition, gitlabApiDefinition, tallyDefinition, contentfulDefinition, asanaDefinition,
@@ -0,0 +1,13 @@
1
+ // The UUID form providers give account, tenant, site and resource IDs.
2
+
3
+ /** A hyphenated hexadecimal UUID in either case, for building larger patterns. */
4
+ const UUID_SOURCE = "[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}";
5
+ const uuidPattern = new RegExp(`^${UUID_SOURCE}$`, "u");
6
+
7
+ /**
8
+ * A UUID in its hyphenated hexadecimal form.
9
+ * @param {unknown} value
10
+ */
11
+ const isUuid = (value) => typeof value === "string" && uuidPattern.test(value);
12
+
13
+ export { UUID_SOURCE, isUuid };
package/src/shared/wix.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { createSchema } from "json-rest-schema";
2
+ import { isUuid } from "./uuid.js";
2
3
 
3
4
  const wixDefinition = Object.freeze({
4
5
  id: "wix", name: "Wix", description: "jskit.connectors.wix.description",
5
6
  accountModes: ["shared", "assistant"], authenticationMethods: ["api-key"], scopes: [],
6
7
  settingsSchema: createSchema({ accountId: { type: "string", required: true, minLength: 36, maxLength: 36,
7
- validator: (value) => /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu.test(value) || "Enter the Account ID from Wix's API Keys Manager." },
8
+ validator: (value) => isUuid(value) || "Enter the Account ID from Wix's API Keys Manager." },
8
9
  selectableSiteIds: { type: "string", maxLength: 4000, validator: value => {
9
10
  const ids = value.split(",").map(id => id.trim());
10
- return ids.length <= 100 && ids.every(id => /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu.test(id)) && new Set(ids.map(id => id.toLowerCase())).size === ids.length || "Enter up to 100 distinct Site IDs separated by commas.";
11
+ return ids.length <= 100 && ids.every(id => isUuid(id)) && new Set(ids.map(id => id.toLowerCase())).size === ids.length || "Enter up to 100 distinct Site IDs separated by commas.";
11
12
  } },
12
- siteId: { type: "string", minLength: 36, maxLength: 36, validator: value => /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu.test(value) || "Copy the Site ID from the Wix site dashboard URL." } }),
13
+ siteId: { type: "string", minLength: 36, maxLength: 36, validator: value => isUuid(value) || "Copy the Site ID from the Wix site dashboard URL." } }),
13
14
  settingsFields: [{ name: "accountId", label: "jskit.connectors.wix.fields.accountId.label", placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
14
15
  hint: "jskit.connectors.wix.fields.accountId.hint" },
15
16
  { name: "selectableSiteIds", label: "jskit.connectors.wix.fields.selectableSiteIds.label", placeholder: "jskit.connectors.wix.fields.selectableSiteIds.placeholder", hint: "jskit.connectors.wix.fields.selectableSiteIds.hint" },
@@ -1,9 +1,8 @@
1
1
  import { createSchema } from "json-rest-schema";
2
- import { zohoRegions } from "./zoho-regions.js";
2
+ import { isZohoId, zohoRegions } from "./zoho.js";
3
3
 
4
- const zohoBooksIdentifier = (value) => typeof value === "string" && /^[1-9][0-9]{0,29}$/u.test(value);
5
4
  const zohoBooksOrganizationField = { type: "string", noTrim: true,
6
- validator: (value) => zohoBooksIdentifier(value) || "Use an organisation ID containing digits only." };
5
+ validator: (value) => isZohoId(value) || "Use an organisation ID containing digits only." };
7
6
 
8
7
  const zohoBooksDefinition = Object.freeze({
9
8
  id: "zoho-books", name: "Zoho Books", description: "jskit.connectors.zohoBooks.description",
@@ -36,4 +35,4 @@ const zohoBooksDefinition = Object.freeze({
36
35
  ] }
37
36
  });
38
37
 
39
- export { zohoBooksDefinition, zohoBooksIdentifier, zohoBooksOrganizationField };
38
+ export { zohoBooksDefinition, zohoBooksOrganizationField };
@@ -1,5 +1,5 @@
1
1
  import { createSchema } from "json-rest-schema";
2
- import { zohoRegions } from "./zoho-regions.js";
2
+ import { zohoRegions } from "./zoho.js";
3
3
 
4
4
  const zohoCrmRegions = Object.freeze(zohoRegions.filter(({ value }) => value !== "sa"));
5
5
 
@@ -9,4 +9,7 @@ const zohoRegions = Object.freeze([
9
9
  { value: "sa", title: "jskit.connectors.zoho.regions.sa.title", accounts: "https://accounts.zoho.sa", suffix: "sa" }
10
10
  ]);
11
11
 
12
- export { zohoRegions };
12
+ // Zoho's organisation and record IDs: up to 30 digits, without a leading zero.
13
+ const isZohoId = (value) => typeof value === "string" && /^[1-9][0-9]{0,29}$/u.test(value);
14
+
15
+ export { isZohoId, zohoRegions };
package/test/ai.test.js CHANGED
@@ -9,7 +9,7 @@ import { parseIntegrationConfiguration, validateIntegrationConfiguration } from
9
9
  const configuration = (patch = {}) => ({ schemaVersion: 1, registrations: {}, integrations: {
10
10
  suggestions: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, ...patch }
11
11
  } });
12
- const actor = { applicationId: "dogandgroom", subjectId: "customer-one" };
12
+ const actor = { applicationId: "bookings", subjectId: "customer-one" };
13
13
  const request = { context: actor, integrationId: "suggestions" };
14
14
  const authorize = async (context) => context?.applicationId === actor.applicationId ? context : null;
15
15
  const validate = (config) => validateIntegrationConfiguration(config, { providers: [aiDefinition] });