@jskit-ai/connectors-catalog 0.1.67 → 0.2.0

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 (137) hide show
  1. package/CHANGELOG.md +1722 -0
  2. package/README.md +11 -0
  3. package/locales/en.json +3139 -0
  4. package/package.json +7 -6
  5. package/src/server/ai.js +4 -4
  6. package/src/server/amazon-redshift.js +5 -5
  7. package/src/server/apify.js +1 -1
  8. package/src/server/apollo-io.js +1 -1
  9. package/src/server/asana.js +3 -3
  10. package/src/server/ashby.js +3 -3
  11. package/src/server/attention.js +2 -2
  12. package/src/server/aws-athena.js +3 -3
  13. package/src/server/aws-s3.js +1 -1
  14. package/src/server/awsTransport.js +9 -9
  15. package/src/server/brevo.js +3 -3
  16. package/src/server/calendly.js +2 -2
  17. package/src/server/chargebee.js +1 -1
  18. package/src/server/databricks.js +2 -2
  19. package/src/server/dbt-semantic-layer.js +8 -8
  20. package/src/server/elevenlabs.js +4 -4
  21. package/src/server/firebase-cloud-messaging.js +8 -8
  22. package/src/server/firecrawl.js +2 -2
  23. package/src/server/gatewayapi.js +4 -4
  24. package/src/server/github-api.js +3 -3
  25. package/src/server/gitlab-api.js +1 -1
  26. package/src/server/gmail.js +1 -1
  27. package/src/server/gong.js +4 -4
  28. package/src/server/google-ads-search.js +16 -16
  29. package/src/server/google-ads.js +7 -7
  30. package/src/server/google-drive.js +8 -6
  31. package/src/server/google-maps-platform.js +5 -5
  32. package/src/server/google-search-console.js +1 -1
  33. package/src/server/google-sheets.js +3 -2
  34. package/src/server/google.js +3 -2
  35. package/src/server/granola.js +5 -5
  36. package/src/server/hubspot.js +5 -3
  37. package/src/server/inngest.js +4 -4
  38. package/src/server/klipy.js +1 -1
  39. package/src/server/lightspeed.js +4 -4
  40. package/src/server/linear.js +2 -2
  41. package/src/server/linkedin.js +6 -6
  42. package/src/server/mapbox.js +1 -1
  43. package/src/server/mcpRegistration.js +3 -2
  44. package/src/server/mcpTransport.js +1 -1
  45. package/src/server/microsoft-fabric.js +3 -3
  46. package/src/server/microsoft-onedrive.js +1 -1
  47. package/src/server/microsoft-onenote.js +1 -1
  48. package/src/server/microsoft-outlook.js +1 -1
  49. package/src/server/microsoft.js +4 -3
  50. package/src/server/n8n.js +4 -4
  51. package/src/server/oura.js +1 -1
  52. package/src/server/paddle.js +1 -1
  53. package/src/server/pipedrive.js +4 -4
  54. package/src/server/posthog.js +3 -3
  55. package/src/server/salesforce.js +8 -8
  56. package/src/server/semrush.js +25 -25
  57. package/src/server/shopify.js +6 -6
  58. package/src/server/slack.js +10 -10
  59. package/src/server/snowflake.js +5 -5
  60. package/src/server/tally.js +1 -1
  61. package/src/server/telegram.js +1 -1
  62. package/src/server/tiktok.js +14 -14
  63. package/src/server/twilio.js +3 -3
  64. package/src/server/twitch.js +22 -21
  65. package/src/server/wave.js +34 -34
  66. package/src/server/wix.js +73 -73
  67. package/src/server/wiz.js +18 -18
  68. package/src/server/woocommerce.js +23 -23
  69. package/src/server/wordpress-com.js +3 -3
  70. package/src/server/wordpress-self-hosted.js +2 -2
  71. package/src/server/workday.js +3 -3
  72. package/src/server/x-twitter.js +6 -6
  73. package/src/server/xero.js +15 -15
  74. package/src/server/zoho-books.js +8 -8
  75. package/src/server/zoho-crm.js +5 -5
  76. package/src/server/zohoOAuth.js +1 -1
  77. package/src/shared/ai.js +24 -13
  78. package/src/shared/algolia.js +4 -4
  79. package/src/shared/amazon-redshift.js +16 -16
  80. package/src/shared/amplitude.js +6 -6
  81. package/src/shared/atlassian.js +19 -19
  82. package/src/shared/aws.js +18 -18
  83. package/src/shared/canva.js +18 -18
  84. package/src/shared/chargebee.js +3 -3
  85. package/src/shared/clickhouse.js +9 -9
  86. package/src/shared/confidence.js +6 -6
  87. package/src/shared/databricks.js +8 -8
  88. package/src/shared/dbt-semantic-layer.js +7 -7
  89. package/src/shared/english-ai.js +6 -0
  90. package/src/shared/english-definitions.js +34 -0
  91. package/src/shared/english-texts.js +7 -0
  92. package/src/shared/figma.js +2 -2
  93. package/src/shared/firebase-cloud-messaging.js +11 -11
  94. package/src/shared/firecrawl.js +1 -1
  95. package/src/shared/gemini-enterprise.js +7 -7
  96. package/src/shared/gong.js +5 -5
  97. package/src/shared/google-ads.js +5 -5
  98. package/src/shared/google-maps-platform.js +7 -7
  99. package/src/shared/google.js +40 -40
  100. package/src/shared/granola.js +5 -5
  101. package/src/shared/hex.js +7 -7
  102. package/src/shared/inngest.js +8 -8
  103. package/src/shared/lightspeed.js +52 -52
  104. package/src/shared/linkedin.js +6 -6
  105. package/src/shared/logo-dev.js +3 -3
  106. package/src/shared/mapbox.js +6 -6
  107. package/src/shared/mcp.js +9 -9
  108. package/src/shared/microsoft-fabric.js +9 -9
  109. package/src/shared/microsoft.js +52 -52
  110. package/src/shared/miro.js +5 -5
  111. package/src/shared/oura.js +5 -5
  112. package/src/shared/posthog.js +9 -9
  113. package/src/shared/prestashop.js +5 -5
  114. package/src/shared/resend.js +1 -1
  115. package/src/shared/salesforce.js +8 -8
  116. package/src/shared/semrush.js +6 -6
  117. package/src/shared/shopify.js +26 -26
  118. package/src/shared/slack.js +61 -61
  119. package/src/shared/snowflake.js +15 -15
  120. package/src/shared/tiktok.js +7 -7
  121. package/src/shared/tokens.js +126 -126
  122. package/src/shared/twilio.js +7 -7
  123. package/src/shared/twitch.js +24 -24
  124. package/src/shared/wave.js +33 -33
  125. package/src/shared/wix.js +5 -5
  126. package/src/shared/wiz.js +14 -14
  127. package/src/shared/wordpress-com.js +9 -9
  128. package/src/shared/wordpress.js +11 -11
  129. package/src/shared/workday.js +8 -8
  130. package/src/shared/x-twitter.js +5 -5
  131. package/src/shared/xero.js +25 -25
  132. package/src/shared/zoho-books.js +10 -10
  133. package/src/shared/zoho-crm.js +16 -16
  134. package/src/shared/zoho-regions.js +8 -8
  135. package/test/englishDefinitions.test.js +37 -0
  136. package/test/i18nCatalog.test.js +16 -0
  137. package/test/shopify.test.js +3 -1
@@ -41,10 +41,10 @@ function deleteRecord(resource) {
41
41
  return {
42
42
  scopes: [],
43
43
  request(input, settings) {
44
- if (typeof input?.force !== "boolean") throw new ConnectorError("connector_input_invalid", "Choose explicitly whether to permanently delete.", { statusCode: 422 });
44
+ if (typeof input?.force !== "boolean") throw new ConnectorError("connector_input_invalid", "Choose explicitly whether to permanently delete.", { statusCode: 422, messageCode: "connectors.woocommerce.chooseExplicitlyWhetherPermanentlyDelete" });
45
45
  const { id, force, product_id, reassign } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
46
- if (permanentOnly && !force) throw new ConnectorError("connector_input_invalid", "This resource does not support trash; permanent deletion requires force=true.", { statusCode: 422 });
47
- if (reassign === id) throw new ConnectorError("connector_input_invalid", "Choose a different reassignment user.", { statusCode: 422 });
46
+ if (permanentOnly && !force) throw new ConnectorError("connector_input_invalid", "This resource does not support trash; permanent deletion requires force=true.", { statusCode: 422, messageCode: "connectors.woocommerce.resourceDoesNotSupportTrash" });
47
+ if (reassign === id) throw new ConnectorError("connector_input_invalid", "Choose a different reassignment user.", { statusCode: 422, messageCode: "connectors.woocommerce.chooseDifferentReassignmentUser" });
48
48
  const path = resource === "variations" ? `products/${product_id}/variations` : resource;
49
49
  const url = new URL(`${baseUrl(settings)}/${path}/${id}`);
50
50
  url.searchParams.set("force", String(force));
@@ -75,11 +75,11 @@ function categoryOperation(action) {
75
75
  scopes: [],
76
76
  request(input, settings) {
77
77
  const { id, ...fields } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
78
- if (action === "delete" && input.force !== true) throw new ConnectorError("connector_input_invalid", "Category deletion is permanent and requires force=true.", { statusCode: 422 });
79
- if (id && fields.parent === id) throw new ConnectorError("connector_input_invalid", "A category cannot be its own parent.", { statusCode: 422 });
78
+ if (action === "delete" && input.force !== true) throw new ConnectorError("connector_input_invalid", "Category deletion is permanent and requires force=true.", { statusCode: 422, messageCode: "connectors.woocommerce.categoryDeletionPermanentRequiresForce" });
79
+ if (id && fields.parent === id) throw new ConnectorError("connector_input_invalid", "A category cannot be its own parent.", { statusCode: 422, messageCode: "connectors.woocommerce.categoryCannotOwnParent" });
80
80
  const url = new URL(`${baseUrl(settings)}/products/categories${id ? `/${id}` : ""}`);
81
81
  if (writing) {
82
- if (!Object.keys(fields).length) throw new ConnectorError("connector_input_invalid", "Supply category changes.", { statusCode: 422 });
82
+ if (!Object.keys(fields).length) throw new ConnectorError("connector_input_invalid", "Supply category changes.", { statusCode: 422, messageCode: "connectors.woocommerce.supplyCategoryChanges" });
83
83
  return { method: action === "create" ? "POST" : "PUT", url: url.href, body: fields };
84
84
  }
85
85
  for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, String(value));
@@ -109,9 +109,9 @@ function couponWrite(update) {
109
109
  scopes: [],
110
110
  request(input, settings) {
111
111
  const { id, ...body } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
112
- if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Supply coupon changes.", { statusCode: 422 });
112
+ if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Supply coupon changes.", { statusCode: 422, messageCode: "connectors.woocommerce.supplyCouponChanges" });
113
113
  if (body.discount_type === "percent" && body.amount !== undefined && Number(body.amount) > 100) {
114
- throw new ConnectorError("connector_input_invalid", "Percentage discounts cannot exceed 100.", { statusCode: 422 });
114
+ throw new ConnectorError("connector_input_invalid", "Percentage discounts cannot exceed 100.", { statusCode: 422, messageCode: "connectors.woocommerce.percentageDiscountsCannotExceed100" });
115
115
  }
116
116
  return { method: update ? "PUT" : "POST", url: `${baseUrl(settings)}/coupons${update ? `/${id}` : ""}`, body };
117
117
  }, validateResult: validRecord.coupons
@@ -147,11 +147,11 @@ function productWrite(update) {
147
147
  request(input, settings) {
148
148
  for (const field of ["regular_price", "sale_price"]) {
149
149
  if (input?.[field] !== undefined && typeof input[field] !== "string") {
150
- throw new ConnectorError("connector_input_invalid", "Product prices must be decimal strings.", { statusCode: 422 });
150
+ throw new ConnectorError("connector_input_invalid", "Product prices must be decimal strings.", { statusCode: 422, messageCode: "connectors.woocommerce.productPricesMustDecimalStrings" });
151
151
  }
152
152
  }
153
153
  const { id, ...body } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
154
- if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Supply product changes.", { statusCode: 422 });
154
+ if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Supply product changes.", { statusCode: 422, messageCode: "connectors.woocommerce.supplyProductChanges" });
155
155
  return { method: update ? "PUT" : "POST", url: `${baseUrl(settings)}/products${update ? `/${id}` : ""}`, body };
156
156
  }, validateResult: validRecord.products
157
157
  };
@@ -177,7 +177,7 @@ function customerWrite(update) {
177
177
  request(input, settings) {
178
178
  const { id, ...body } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
179
179
  if (!Object.keys(body).length || [body.billing, body.shipping].some(value => value && !Object.keys(value).length)) {
180
- throw new ConnectorError("connector_input_invalid", "Supply customer changes and non-empty address objects.", { statusCode: 422 });
180
+ throw new ConnectorError("connector_input_invalid", "Supply customer changes and non-empty address objects.", { statusCode: 422, messageCode: "connectors.woocommerce.supplyCustomerChangesNonEmpty" });
181
181
  }
182
182
  return { method: update ? "PUT" : "POST", url: `${baseUrl(settings)}/customers${update ? `/${id}` : ""}`, body };
183
183
  }, validateResult: validRecord.customers
@@ -207,12 +207,12 @@ function variationOperation(action) {
207
207
  scopes: [],
208
208
  request(input, settings) {
209
209
  for (const field of ["regular_price", "sale_price"]) {
210
- if (input?.[field] !== undefined && typeof input[field] !== "string") throw new ConnectorError("connector_input_invalid", "Variation prices must be decimal strings.", { statusCode: 422 });
210
+ if (input?.[field] !== undefined && typeof input[field] !== "string") throw new ConnectorError("connector_input_invalid", "Variation prices must be decimal strings.", { statusCode: 422, messageCode: "connectors.woocommerce.variationPricesMustDecimalStrings" });
211
211
  }
212
212
  const { product_id, id, ...fields } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
213
213
  const url = new URL(`${baseUrl(settings)}/products/${product_id}/variations${id ? `/${id}` : ""}`);
214
214
  if (writing) {
215
- if (!Object.keys(fields).length) throw new ConnectorError("connector_input_invalid", "Supply variation changes.", { statusCode: 422 });
215
+ if (!Object.keys(fields).length) throw new ConnectorError("connector_input_invalid", "Supply variation changes.", { statusCode: 422, messageCode: "connectors.woocommerce.supplyVariationChanges" });
216
216
  return { method: action === "create" ? "POST" : "PUT", url: url.href, body: fields };
217
217
  }
218
218
  for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, String(value));
@@ -243,11 +243,11 @@ function refundOperation(action) {
243
243
  request(input, settings) {
244
244
  if (action === "create" && (typeof input?.amount !== "string" || typeof input.api_refund !== "boolean" || typeof input.api_restock !== "boolean" ||
245
245
  input.line_items?.some?.(line => typeof line?.refund_total !== "string"))) {
246
- throw new ConnectorError("connector_input_invalid", "Choose explicit refund/restock flags and decimal-string amounts.", { statusCode: 422 });
246
+ throw new ConnectorError("connector_input_invalid", "Choose explicit refund/restock flags and decimal-string amounts.", { statusCode: 422, messageCode: "connectors.woocommerce.chooseExplicitRefundRestockFlags" });
247
247
  }
248
248
  const { order_id, id, ...fields } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
249
249
  if (action === "create" && fields.api_restock && !fields.line_items?.some(line => line.quantity > 0)) {
250
- throw new ConnectorError("connector_input_invalid", "Restocking requires selected order lines and positive quantities.", { statusCode: 422 });
250
+ throw new ConnectorError("connector_input_invalid", "Restocking requires selected order lines and positive quantities.", { statusCode: 422, messageCode: "connectors.woocommerce.restockingRequiresSelectedOrderLines" });
251
251
  }
252
252
  const url = new URL(`${baseUrl(settings)}/orders/${order_id}/refunds${id ? `/${id}` : ""}`);
253
253
  if (action === "create") return { method: "POST", url: url.href, body: fields };
@@ -268,7 +268,7 @@ function reportOperation(report) {
268
268
  const fields = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
269
269
  if ((fields.date_min === undefined) !== (fields.date_max === undefined) ||
270
270
  (fields.period && fields.date_min) || (fields.date_min && fields.date_min > fields.date_max)) {
271
- throw new ConnectorError("connector_input_invalid", "Choose a period or a complete ascending date range.", { statusCode: 422 });
271
+ throw new ConnectorError("connector_input_invalid", "Choose a period or a complete ascending date range.", { statusCode: 422, messageCode: "connectors.woocommerce.choosePeriodCompleteAscendingDate" });
272
272
  }
273
273
  const url = new URL(`${baseUrl(settings)}/reports/${report}`);
274
274
  for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, value);
@@ -316,13 +316,13 @@ const woocommerceProvider = Object.freeze({
316
316
  async exchange(address, options, { request }) {
317
317
  const result = await request(address, options);
318
318
  const match = /\/wp-json\/wc\/v3\/(?:products|orders|customers|coupons)\/(\d+)$/u.exec(new URL(address).pathname);
319
- if (match && result?.id !== Number(match[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different record.", { statusCode: 502 });
319
+ if (match && result?.id !== Number(match[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different record.", { statusCode: 502, messageCode: "connectors.shared.differentRecord", messageParams: { provider: "WooCommerce" } });
320
320
  const variation = /\/wp-json\/wc\/v3\/products\/\d+\/variations\/(\d+)$/u.exec(new URL(address).pathname);
321
- if (variation && result?.id !== Number(variation[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different variation.", { statusCode: 502 });
321
+ if (variation && result?.id !== Number(variation[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different variation.", { statusCode: 502, messageCode: "connectors.woocommerce.returnedDifferentVariation" });
322
322
  const category = /\/wp-json\/wc\/v3\/products\/categories\/(\d+)$/u.exec(new URL(address).pathname);
323
- if (category && result?.id !== Number(category[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different category.", { statusCode: 502 });
323
+ if (category && result?.id !== Number(category[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different category.", { statusCode: 502, messageCode: "connectors.woocommerce.returnedDifferentCategory" });
324
324
  const refund = /\/wp-json\/wc\/v3\/orders\/\d+\/refunds\/(\d+)$/u.exec(new URL(address).pathname);
325
- if (refund && result?.id !== Number(refund[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different refund.", { statusCode: 502 });
325
+ if (refund && result?.id !== Number(refund[1])) throw new ConnectorError("connector_response_invalid", "WooCommerce returned a different refund.", { statusCode: 502, messageCode: "connectors.woocommerce.returnedDifferentRefund" });
326
326
  return result;
327
327
  },
328
328
  operations: {
@@ -362,7 +362,7 @@ const woocommerceProvider = Object.freeze({
362
362
  scopes: [],
363
363
  request(input, settings) {
364
364
  const { id, ...body } = validateSchemaPayload({ schema: orderUpdateSchema, mode: "replace" }, input, { statusCode: 422 });
365
- if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Supply order changes.", { statusCode: 422 });
365
+ if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Supply order changes.", { statusCode: 422, messageCode: "connectors.woocommerce.supplyOrderChanges" });
366
366
  return { method: "PUT", url: `${baseUrl(settings)}/orders/${id}`, body };
367
367
  }, validateResult: validRecord.orders
368
368
  },
@@ -399,12 +399,12 @@ function verifyWooCommerceWebhook({ rawBody, signature, secret } = {}) {
399
399
  if (!(rawBody instanceof Uint8Array) || rawBody.byteLength > 2 * 1024 * 1024 ||
400
400
  typeof secret !== "string" || !secret || typeof signature !== "string" ||
401
401
  !/^[A-Za-z0-9+/]{43}=$/u.test(signature)) {
402
- throw new ConnectorError("connector_webhook_invalid", "Invalid WooCommerce webhook signature inputs.", { statusCode: 401 });
402
+ throw new ConnectorError("connector_webhook_invalid", "Invalid WooCommerce webhook signature inputs.", { statusCode: 401, messageCode: "connectors.woocommerce.invalidWebhookSignatureInputs" });
403
403
  }
404
404
  const supplied = Buffer.from(signature, "base64");
405
405
  const expected = createHmac("sha256", secret).update(rawBody).digest();
406
406
  if (supplied.toString("base64") !== signature || !timingSafeEqual(expected, supplied)) {
407
- throw new ConnectorError("connector_webhook_invalid", "Invalid WooCommerce webhook signature.", { statusCode: 401 });
407
+ throw new ConnectorError("connector_webhook_invalid", "Invalid WooCommerce webhook signature.", { statusCode: 401, messageCode: "connectors.shared.invalidWebhookSignature", messageParams: { provider: "WooCommerce" } });
408
408
  }
409
409
  return true;
410
410
  }
@@ -44,7 +44,7 @@ function siteOperation(action, fields, { resource = "posts", scope = resource, r
44
44
  const { siteId, postId, mediaId, commentId, termSlug, context, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
45
45
  delete values.confirmDeletion;
46
46
  if (changesRequired && !Object.keys(values).length) {
47
- throw new ConnectorError("connector_input_invalid", "Supply at least one change.", { statusCode: 422 });
47
+ throw new ConnectorError("connector_input_invalid", "Supply at least one change.", { statusCode: 422, messageCode: "connectors.shared.supplyLeastChange" });
48
48
  }
49
49
  const target = termSlug === undefined ? postId ?? mediaId ?? commentId ?? (collection ? "" : "new") : `slug:${encodeURIComponent(termSlug)}`;
50
50
  const url = new URL(`https://public-api.wordpress.com/rest/v1.1/sites/${siteId}/${resource}/${target}${action ? `/${action}` : ""}`);
@@ -115,7 +115,7 @@ const wordpressComProvider = Object.freeze({
115
115
  apiOrigins: ["https://public-api.wordpress.com"], checkOperation: "profile.read",
116
116
  grantedScopesFromVerification(profile, { clientId }) {
117
117
  if (String(profile.token_client_id) !== clientId) {
118
- throw new ConnectorError("connector_response_invalid", "The verified token belongs to a different application.", { statusCode: 502 });
118
+ throw new ConnectorError("connector_response_invalid", "The verified token belongs to a different application.", { statusCode: 502, messageCode: "connectors.wordpressCom.verifiedTokenBelongsDifferentApplication" });
119
119
  }
120
120
  return profile.token_scope;
121
121
  },
@@ -194,7 +194,7 @@ const wordpressComProvider = Object.freeze({
194
194
  const { siteId, filename, mimeType, contentBase64, ...metadata } = validateSchemaPayload({ schema: mediaUploadSchema, mode: "replace" }, input, { statusCode: 422 });
195
195
  const bytes = Buffer.from(contentBase64, "base64");
196
196
  if (!bytes.length || bytes.length > 5 * 1024 * 1024 || bytes.toString("base64") !== contentBase64) {
197
- throw new ConnectorError("connector_input_invalid", "Supply canonical base64 for a nonempty file of at most 5 MiB.", { statusCode: 422 });
197
+ throw new ConnectorError("connector_input_invalid", "Supply canonical base64 for a nonempty file of at most 5 MiB.", { statusCode: 422, messageCode: "connectors.shared.supplyCanonicalBase64NonemptyFile" });
198
198
  }
199
199
  const body = new FormData();
200
200
  body.append("media[0]", new Blob([bytes], { type: mimeType }), filename);
@@ -24,7 +24,7 @@ function wordpressOperation(resource, fields, method, validateResult, { write =
24
24
  const { id: resourceId, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
25
25
  const url = new URL(`${baseUrl(settings)}/${resource}${resourceId ? `/${resourceId}` : ""}`);
26
26
  if (write) {
27
- if (!Object.keys(values).length) throw new ConnectorError("connector_input_invalid", "Supply at least one change.", { statusCode: 422 });
27
+ if (!Object.keys(values).length) throw new ConnectorError("connector_input_invalid", "Supply at least one change.", { statusCode: 422, messageCode: "connectors.shared.supplyLeastChange" });
28
28
  return { method, url: url.href, body: values };
29
29
  }
30
30
  for (const [key, value] of Object.entries(values)) url.searchParams.set(key, String(value));
@@ -96,7 +96,7 @@ const wordpressSelfHostedProvider = Object.freeze({
96
96
  const { filename, mimeType, contentBase64, ...metadata } = validateSchemaPayload({ schema: uploadSchema, mode: "replace" }, input, { statusCode: 422 });
97
97
  const bytes = Buffer.from(contentBase64, "base64");
98
98
  if (!bytes.length || bytes.length > 5 * 1024 * 1024 || bytes.toString("base64") !== contentBase64) {
99
- throw new ConnectorError("connector_input_invalid", "Supply canonical base64 for a nonempty file of at most 5 MiB.", { statusCode: 422 });
99
+ throw new ConnectorError("connector_input_invalid", "Supply canonical base64 for a nonempty file of at most 5 MiB.", { statusCode: 422, messageCode: "connectors.shared.supplyCanonicalBase64NonemptyFile" });
100
100
  }
101
101
  const body = new FormData();
102
102
  body.append("file", new Blob([bytes], { type: mimeType }), filename);
@@ -56,7 +56,7 @@ function absenceRead(kind) {
56
56
  request(input, settings) {
57
57
  const { worker: workerId, ...query } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
58
58
  if (query.fromDate && query.toDate && query.fromDate > query.toDate) {
59
- throw new ConnectorError("connector_input_invalid", "The start date must not follow the end date.", { statusCode: 422 });
59
+ throw new ConnectorError("connector_input_invalid", "The start date must not follow the end date.", { statusCode: 422, messageCode: "connectors.workday.datesOrder" });
60
60
  }
61
61
  const { host, prefix = "", tenant } = workdayRestEndpoint(settings.restApiEndpoint);
62
62
  const path = kind === "statuses" ? "values/timeOff/status/" : kind === "balances" ? "balances" : `workers/${workerId}/timeOffDetails`;
@@ -105,11 +105,11 @@ const workdayProvider = Object.freeze({
105
105
  const url = new URL(`https://${host}/ccx/service/customreport2/${tenant}/${encodeURIComponent(owner)}/${encodeURIComponent(report)}`);
106
106
  const names = new Set();
107
107
  for (const { name, value } of prompts) {
108
- if (names.has(name)) throw new ConnectorError("connector_input_invalid", "Supply each report prompt once; join multi-instance WIDs with !.", { statusCode: 422 });
108
+ if (names.has(name)) throw new ConnectorError("connector_input_invalid", "Supply each report prompt once; join multi-instance WIDs with !.", { statusCode: 422, messageCode: "connectors.workday.supplyReportPromptOnceJoin" });
109
109
  names.add(name); url.searchParams.set(name, value);
110
110
  }
111
111
  url.searchParams.set("format", "json");
112
- if (url.href.length > 16384) throw new ConnectorError("connector_input_invalid", "Use a smaller report prompt request.", { statusCode: 422 });
112
+ if (url.href.length > 16384) throw new ConnectorError("connector_input_invalid", "Use a smaller report prompt request.", { statusCode: 422, messageCode: "connectors.workday.useSmallerReportPromptRequest" });
113
113
  return { method: "GET", url: url.href };
114
114
  },
115
115
  validateResult: result => object(result) && !result.error && !result.errors && Array.isArray(result.Report_Entry) && result.Report_Entry.every(object)
@@ -39,7 +39,7 @@ const xTwitterProvider = Object.freeze({
39
39
  ...xTwitterDefinition, apiOrigins: ["https://api.x.com"],
40
40
  apiKey: { headers: (key) => {
41
41
  if (!/^[\x21-\x7e]+$/u.test(key) || key.length > 8192) {
42
- throw new ConnectorError("connector_binding_missing", "Use the X app-only token without spaces, control characters or a Bearer prefix.");
42
+ throw new ConnectorError("connector_binding_missing", "Use the X app-only token without spaces, control characters or a Bearer prefix.", { messageCode: "connectors.xTwitter.useAppTokenWithoutSpaces" });
43
43
  }
44
44
  return { Authorization: `Bearer ${key}` };
45
45
  } },
@@ -76,7 +76,7 @@ const xTwitterProvider = Object.freeze({
76
76
  request(input) {
77
77
  // Snowflake IDs exceed JS's safe integer range; reject numbers before the schema can coerce them.
78
78
  if (typeof input?.userId !== "string") {
79
- const error = new ConnectorError("connector_input_invalid", "Pass the X user ID as a string.", { statusCode: 422 });
79
+ const error = new ConnectorError("connector_input_invalid", "Pass the X user ID as a string.", { statusCode: 422, messageCode: "connectors.xTwitter.passUserIdString" });
80
80
  error.fieldErrors = { userId: "Pass the X user ID as a string." };
81
81
  throw error;
82
82
  }
@@ -94,17 +94,17 @@ const xTwitterProvider = Object.freeze({
94
94
  let result;
95
95
  try { result = await request(address, options); } catch (error) {
96
96
  const status = Number(error?.status || error?.statusCode);
97
- if (status === 402) throw new ConnectorError("connector_billing_required", "Check this X application's API credits and billing access.", { statusCode: 402 });
98
- if (status === 404) throw new ConnectorError("connector_resource_not_found", "The X resource was not found or is not available.", { statusCode: 404 });
97
+ if (status === 402) throw new ConnectorError("connector_billing_required", "Check this X application's API credits and billing access.", { statusCode: 402, messageCode: "connectors.xTwitter.checkApplicationApiCreditsBilling" });
98
+ if (status === 404) throw new ConnectorError("connector_resource_not_found", "The X resource was not found or is not available.", { statusCode: 404, messageCode: "connectors.xTwitter.resourceNotFoundNotAvailable" });
99
99
  throw error;
100
100
  }
101
101
  if (Array.isArray(result?.errors) && result.errors.length) {
102
102
  if (!result.data && result.errors.every((error) => [
103
103
  "https://api.x.com/2/problems/resource-not-found", "https://api.twitter.com/2/problems/resource-not-found"
104
104
  ].includes(error?.type))) {
105
- throw new ConnectorError("connector_resource_not_found", "The X resource was not found or is not available.", { statusCode: 404 });
105
+ throw new ConnectorError("connector_resource_not_found", "The X resource was not found or is not available.", { statusCode: 404, messageCode: "connectors.xTwitter.resourceNotFoundNotAvailable" });
106
106
  }
107
- throw new ConnectorError("connector_response_incomplete", "X could not return the complete requested data.", { statusCode: 502 });
107
+ throw new ConnectorError("connector_response_incomplete", "X could not return the complete requested data.", { statusCode: 502, messageCode: "connectors.xTwitter.couldNotReturnCompleteRequested" });
108
108
  }
109
109
  return result;
110
110
  }
@@ -38,7 +38,7 @@ function accountingRead(resource, fields, scopes, validateResult) {
38
38
  const { tenantId, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
39
39
  if (values.fromDate && values.toDate && values.fromDate > values.toDate ||
40
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 });
41
+ throw new ConnectorError("connector_input_invalid", "The report start date must not follow its end date.", { statusCode: 422, messageCode: "connectors.xero.reportDatesOrder" });
42
42
  }
43
43
  const url = new URL(`https://api.xero.com/api.xro/2.0/${resource}`);
44
44
  for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
@@ -55,7 +55,7 @@ function accountingRecord(resource, idName, scopes, fields = {}) {
55
55
  request(input) {
56
56
  const { tenantId: tenant, id, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
57
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 });
58
+ throw new ConnectorError("connector_input_invalid", "The budget start date must not follow its end date.", { statusCode: 422, messageCode: "connectors.xero.budgetDatesOrder" });
59
59
  }
60
60
  const url = new URL(`https://api.xero.com/api.xro/2.0/${resource}/${id.toLowerCase()}`);
61
61
  for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
@@ -79,7 +79,7 @@ function contactWrite(update) {
79
79
  scopes: ["accounting.contacts"],
80
80
  request(input) {
81
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 });
82
+ if (Object.keys(contact).length === 0) throw new ConnectorError("connector_input_invalid", "Supply a contact field to change.", { statusCode: 422, messageCode: "connectors.xero.supplyContactFieldChange" });
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
  },
@@ -123,9 +123,9 @@ function invoiceWrite(update) {
123
123
  scopes: ["accounting.invoices"],
124
124
  request(input) {
125
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 });
126
+ if (!contactId && !Object.keys(invoice).length) throw new ConnectorError("connector_input_invalid", "Supply an invoice field to change.", { statusCode: 422, messageCode: "connectors.xero.supplyInvoiceFieldChange" });
127
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 });
128
+ throw new ConnectorError("connector_input_invalid", "Discount rates apply only to sales invoices.", { statusCode: 422, messageCode: "connectors.xero.discountRatesApplySalesInvoices" });
129
129
  }
130
130
  if (contactId) invoice.Contact = { ContactID: contactId };
131
131
  return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/Invoices${update ? `/${id.toLowerCase()}` : ""}?unitdp=4`,
@@ -161,7 +161,7 @@ function attachmentOperation(action) {
161
161
  if (writing) {
162
162
  body = Buffer.from(contentBase64, "base64");
163
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 });
164
+ throw new ConnectorError("connector_input_invalid", "Supply canonical base64 for a nonempty attachment of at most 3 MiB.", { statusCode: 422, messageCode: "connectors.xero.supplyCanonicalBase64NonemptyAttachment" });
165
165
  }
166
166
  headers["Content-Type"] = "application/octet-stream";
167
167
  headers["Idempotency-Key"] = idempotencyKey;
@@ -200,7 +200,7 @@ function bankTransactionWrite(update) {
200
200
  scopes: ["accounting.banktransactions"],
201
201
  request(input) {
202
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 });
203
+ if (!contactId && !bankAccountId && !Object.keys(transaction).length) throw new ConnectorError("connector_input_invalid", "Supply a bank transaction field to change.", { statusCode: 422, messageCode: "connectors.xero.supplyBankTransactionFieldChange" });
204
204
  if (contactId) transaction.Contact = { ContactID: contactId };
205
205
  if (bankAccountId) transaction.BankAccount = { AccountID: bankAccountId };
206
206
  if (update) transaction.BankTransactionID = id.toLowerCase();
@@ -239,7 +239,7 @@ function manualJournalWrite(update) {
239
239
  scopes: ["accounting.manualjournals"],
240
240
  request(input) {
241
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 });
242
+ if (!Object.keys(journal).length) throw new ConnectorError("connector_input_invalid", "Supply a journal field to change.", { statusCode: 422, messageCode: "connectors.xero.supplyJournalFieldChange" });
243
243
  if (update) journal.ManualJournalID = id.toLowerCase();
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] } };
@@ -342,9 +342,9 @@ const xeroProvider = Object.freeze({
342
342
  const tenant = options.headers["Xero-tenant-id"];
343
343
  const headers = Object.fromEntries(Object.entries(options.headers).filter(([key]) => ["authorization", "accept"].includes(key.toLowerCase())));
344
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 });
345
+ if (!validConnections(connections)) throw new ConnectorError("connector_response_invalid", "Xero returned invalid connections.", { statusCode: 502, messageCode: "connectors.xero.returnedInvalidConnections" });
346
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 });
347
+ throw new ConnectorError("connector_permission_denied", "This Xero organisation is not connected to this account.", { statusCode: 403, messageCode: "connectors.xero.organisationNotConnectedAccount" });
348
348
  }
349
349
  }
350
350
  const attachmentFile = /\/Attachments\/[^/]+$/u.test(new URL(address).pathname);
@@ -363,7 +363,7 @@ const xeroProvider = Object.freeze({
363
363
  size += value.byteLength;
364
364
  if (size > 3 * 1024 * 1024) {
365
365
  await reader.cancel();
366
- throw new ConnectorError("connector_response_too_large", "Xero attachment responses are limited to 3 MiB.", { statusCode: 413 });
366
+ throw new ConnectorError("connector_response_too_large", "Xero attachment responses are limited to 3 MiB.", { statusCode: 413, messageCode: "connectors.xero.attachmentResponsesLimited3Mib" });
367
367
  }
368
368
  chunks.push(value);
369
369
  }
@@ -372,7 +372,7 @@ const xeroProvider = Object.freeze({
372
372
  if (downloading) return { contentBase64: bytes.toString("base64"), size,
373
373
  contentType: response.headers.get("content-type")?.split(";")[0] || "application/octet-stream" };
374
374
  try { return JSON.parse(bytes.toString("utf8")); }
375
- catch { throw new ConnectorError("connector_response_invalid", "Xero returned an invalid attachment response.", { statusCode: 502 }); }
375
+ catch { throw new ConnectorError("connector_response_invalid", "Xero returned an invalid attachment response.", { statusCode: 502, messageCode: "connectors.xero.returnedInvalidAttachmentResponse" }); }
376
376
  }
377
377
  const result = await request(address, options);
378
378
  const url = new URL(address);
@@ -381,15 +381,15 @@ const xeroProvider = Object.freeze({
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
383
  if (!uuid(returnedId) || returnedId.toLowerCase() !== record[2].toLowerCase()) {
384
- throw new ConnectorError("connector_response_invalid", "Xero returned a different record.", { statusCode: 502 });
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
  }
387
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 });
388
+ throw new ConnectorError("connector_response_invalid", "Xero returned a different organisation.", { statusCode: 502, messageCode: "connectors.xero.returnedDifferentOrganisation" });
389
389
  }
390
390
  const records = url.pathname.endsWith("/Contacts") ? result?.Contacts : url.pathname.endsWith("/Invoices") ? result?.Invoices : url.pathname.endsWith("/Payments") ? result?.Payments : null;
391
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 });
392
+ throw new ConnectorError("connector_response_invalid", "Xero returned more records than requested.", { statusCode: 502, messageCode: "connectors.shared.tooManyRecords", messageParams: { provider: "Xero" } });
393
393
  }
394
394
  return result;
395
395
  }
@@ -22,11 +22,11 @@ function booksList(resource, identifier) {
22
22
  request(input, settings = {}) {
23
23
  const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
24
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 });
25
+ throw new ConnectorError("connector_input_invalid", "This integration is configured for a different organisation.", { statusCode: 422, messageCode: "connectors.zohoBooks.integrationConfiguredDifferentOrganisation" });
26
26
  }
27
27
  values.organization_id = settings.organizationId || values.organization_id;
28
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 });
29
+ throw new ConnectorError("connector_input_invalid", "Choose an organisation ID from organizations.list or configure one for this integration.", { statusCode: 422, messageCode: "connectors.zohoBooks.chooseOrganisationIdOrganizationsList" });
30
30
  }
31
31
  const url = new URL(`${apiOrigin(settings)}/books/v3/${resource}`);
32
32
  for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
@@ -56,31 +56,31 @@ const zohoBooksProvider = Object.freeze({
56
56
  headers.set("Authorization", headers.get("Authorization").replace(/^Bearer /u, "Zoho-oauthtoken "));
57
57
  const response = await fetchImpl(address, { ...options, headers, credentials: "omit" });
58
58
  if (!response.ok) {
59
- if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Books resource is not available.", { statusCode: 404 });
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
60
  throw Object.assign(new Error("Zoho Books request failed."), { status: response.status });
61
61
  }
62
62
  let value;
63
63
  try { value = await response.json(); } catch { /* validated below */ }
64
64
  if (response.status !== 200 || value?.code !== 0) {
65
- throw new ConnectorError("connector_response_invalid", "Zoho Books returned an unexpected response.", { statusCode: 502 });
65
+ throw new ConnectorError("connector_response_invalid", "Zoho Books returned an unexpected response.", { statusCode: 502, messageCode: "connectors.shared.unexpectedResponse", messageParams: { provider: "Zoho Books" } });
66
66
  }
67
67
  const url = new URL(address);
68
68
  if (url.pathname.endsWith("/organizations")) {
69
- if (!validOrganizations(value)) throw new ConnectorError("connector_response_invalid", "Zoho Books returned invalid organisations.", { statusCode: 502 });
69
+ if (!validOrganizations(value)) throw new ConnectorError("connector_response_invalid", "Zoho Books returned invalid organisations.", { statusCode: 502, messageCode: "connectors.zohoBooks.returnedInvalidOrganisations" });
70
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 });
71
+ throw new ConnectorError("connector_permission_denied", "The configured Books organisation is not active and accessible to this account.", { statusCode: 403, messageCode: "connectors.zohoBooks.configuredOrganisationNotActiveAccessible" });
72
72
  }
73
73
  } else {
74
74
  const resource = url.pathname.split("/").at(-1);
75
75
  // Books documents invoice page_context as a singleton array; other lists use an object.
76
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 });
77
+ if (value.page_context.length !== 1) throw new ConnectorError("connector_response_invalid", "Zoho Books returned ambiguous pagination.", { statusCode: 502, messageCode: "connectors.zohoBooks.returnedAmbiguousPagination" });
78
78
  value.page_context = value.page_context[0];
79
79
  }
80
80
  if (value[resource]?.length > Number(url.searchParams.get("per_page")) ||
81
81
  value.page_context?.per_page !== Number(url.searchParams.get("per_page")) ||
82
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 });
83
+ throw new ConnectorError("connector_response_invalid", "Zoho Books returned an unexpected page.", { statusCode: 502, messageCode: "connectors.shared.unexpectedPage", messageParams: { provider: "Zoho Books" } });
84
84
  }
85
85
  }
86
86
  return value;
@@ -25,7 +25,7 @@ function recordsList(module, defaults) {
25
25
  request(input, settings) {
26
26
  const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
27
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 });
28
+ throw new ConnectorError("connector_input_invalid", "Use either a page number or a page token.", { statusCode: 422, messageCode: "connectors.zohoCrm.useEitherPageNumberPage" });
29
29
  }
30
30
  if (!values.page_token && values.page === undefined) values.page = 1;
31
31
  const url = new URL(`${apiOrigin(settings)}/crm/v8/${module}`);
@@ -69,18 +69,18 @@ const zohoCrmProvider = Object.freeze({
69
69
  if (response.status === 204 && list) return { data: [], info: { count: 0, per_page: perPage, more_records: false } };
70
70
  let value;
71
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 });
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, messageCode: "connectors.zohoCrm.connectAgainPermissionsNeededOperation" });
73
73
  if (!response.ok) {
74
- if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This CRM resource is not available.", { statusCode: 404 });
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
76
  throw Object.assign(new Error("Zoho CRM request failed."), { status: response.status });
77
77
  }
78
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 });
79
+ throw new ConnectorError("connector_response_invalid", "Zoho CRM returned an unexpected response.", { statusCode: 502, messageCode: "connectors.shared.unexpectedResponse", messageParams: { provider: "Zoho CRM" } });
80
80
  }
81
81
  if (list && (value.data?.length > perPage || value.info?.per_page !== perPage ||
82
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 });
83
+ throw new ConnectorError("connector_response_invalid", "Zoho CRM returned an unexpected page.", { statusCode: 502, messageCode: "connectors.shared.unexpectedPage", messageParams: { provider: "Zoho CRM" } });
84
84
  }
85
85
  return value;
86
86
  }
@@ -13,7 +13,7 @@ async function normalizeZohoTokenResponse(response, expectedApiOrigin) {
13
13
  return Response.json({ error: value.error === "invalid_code" ? "invalid_grant" : value.error }, { status: 400 });
14
14
  }
15
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 });
16
+ throw new ConnectorError("connector_response_invalid", "Zoho returned a token for a different data center or environment.", { statusCode: 502, messageCode: "connectors.zoho.returnedTokenDifferentDataCenter" });
17
17
  }
18
18
  return response;
19
19
  }
package/src/shared/ai.js CHANGED
@@ -1,11 +1,18 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import snapshot from "./ai-models.json" with { type: "json" };
3
3
 
4
+ // Message keys; render them with the app's text helper (useJskitText().text).
4
5
  const aiAccessLabels = Object.freeze({
5
- "free-no-setup": "Free · no account or key",
6
- "free-with-connection": "Free · connection required",
7
- paid: "Paid · connection required",
8
- unknown: "Pricing unknown · check provider"
6
+ "free-no-setup": "jskit.connectors.ai.access.freeNoSetup",
7
+ "free-with-connection": "jskit.connectors.ai.access.freeWithConnection",
8
+ paid: "jskit.connectors.ai.access.paid",
9
+ unknown: "jskit.connectors.ai.access.unknown"
10
+ });
11
+ const selectedModelHints = Object.freeze({
12
+ "free-no-setup": "jskit.connectors.ai.fields.model.selectedHint.freeNoSetup",
13
+ "free-with-connection": "jskit.connectors.ai.fields.model.selectedHint.freeWithConnection",
14
+ paid: "jskit.connectors.ai.fields.model.selectedHint.paid",
15
+ unknown: "jskit.connectors.ai.fields.model.selectedHint.unknown"
9
16
  });
10
17
  // These direct API-key routes need no coding subscription or cloud identity
11
18
  // exchange. Other upstream entries remain available as framework metadata.
@@ -15,8 +22,7 @@ const aiCatalogue = Object.freeze({ ...snapshot, providers: Object.freeze(snapsh
15
22
  ...provider, connection: directProviders.has(provider.id) ? "api-key" : "framework",
16
23
  models: Object.freeze(provider.models.map((model) => Object.freeze({ ...model, key: `${provider.id}/${model.id}`, providerId: provider.id })))
17
24
  }))) });
18
- const catalogueDate = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" })
19
- .format(new Date(snapshot.extractedAt));
25
+ const catalogueDateFormat = Object.freeze({ day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
20
26
  const models = new Map(aiCatalogue.providers.flatMap((provider) => provider.models.map((model) => [model.key, model])));
21
27
 
22
28
  function getAiModel(key) { return models.get(key); }
@@ -34,21 +40,26 @@ const modelItems = Object.freeze(listAiModels({ configurableOnly: true }).map((m
34
40
  value: model.key, title: `${model.name} (${model.providerId})`, props: { subtitle: aiAccessLabels[model.access] }
35
41
  })));
36
42
  const aiDefinition = Object.freeze({
37
- id: "ai", name: "AI", description: "Choose an app AI provider, model and whose account pays. No model is preselected.",
43
+ id: "ai", name: "AI", description: "jskit.connectors.ai.description",
38
44
  accountModes: ["shared", "per-user"], authenticationMethods: ["api-key", "none"], scopes: [],
39
45
  authenticationMethodsForSettings: (settings = {}) => getAiModel(settings.model)?.access === "free-no-setup"
40
46
  ? ["none", "api-key"] : ["api-key"],
41
- authenticationLabels: { none: "Free access · no account or key", "api-key": "Use a provider API key" },
42
- authenticationHint: "Your app calls the provider directly. A shared account belongs to the app administrator; individual accounts belong to each signed-in app user.",
47
+ authenticationLabels: { none: "jskit.connectors.ai.authenticationLabels.none", "api-key": "jskit.connectors.ai.authenticationLabels.apiKey" },
48
+ authenticationHint: "jskit.connectors.ai.authenticationHint",
43
49
  settingsSchema: createSchema({ model: { type: "string", required: true,
44
50
  enum: modelItems.map((item) => item.value) } }),
45
- settingsFields: [{ name: "model", label: "AI model", searchable: true, items: modelItems,
51
+ settingsFields: [{ name: "model", label: "jskit.connectors.ai.fields.model.label", searchable: true, items: modelItems,
46
52
  credentialScope: (key) => getAiModel(key)?.providerId,
47
- hint: (settings = {}) => {
53
+ // The catalogue date is display text: formatDate (useJskitText's) uses the app's active
54
+ // locale; without an app language it keeps its fixed English form.
55
+ hint: (settings = {}, { formatDate } = {}) => {
48
56
  const model = getAiModel(settings.model);
49
- return `${model ? `${model.name}: ${aiAccessLabels[model.access]}. ` : "Choose a provider and model. "}Search by provider or model. Catalogue dated ${catalogueDate}; availability can change. No automatic paid fallback.`;
57
+ const date = formatDate ? formatDate(snapshot.extractedAt, catalogueDateFormat)
58
+ : new Date(snapshot.extractedAt).toLocaleDateString("en-GB", catalogueDateFormat);
59
+ return model ? { key: selectedModelHints[model.access], params: { model: model.name, date } }
60
+ : { key: "jskit.connectors.ai.fields.model.hint", params: { date } };
50
61
  } }],
51
- apiKeyReferenceHint: "Use an environment reference for an administrator key. For individual accounts, use a private reference your app resolves for the authenticated user. Never put a key in this file.",
62
+ apiKeyReferenceHint: "jskit.connectors.ai.apiKeyReferenceHint",
52
63
  setup: {
53
64
  url: "https://opencode.ai/docs/zen/",
54
65
  steps: [
@@ -2,7 +2,7 @@ import { createSchema } from "json-rest-schema";
2
2
  import { secretReference } from "@jskit-ai/connectors-core/shared/configuration";
3
3
 
4
4
  const algoliaDefinition = Object.freeze({
5
- id: "algolia", name: "Algolia", description: "Search and maintain index records, and check indexing completion.",
5
+ id: "algolia", name: "Algolia", description: "jskit.connectors.algolia.description",
6
6
  accountModes: ["shared", "assistant"], authenticationMethods: ["api-key"], scopes: [],
7
7
  settingsSchema: createSchema({
8
8
  applicationId: { type: "string", required: true, minLength: 1, maxLength: 63,
@@ -10,9 +10,9 @@ const algoliaDefinition = Object.freeze({
10
10
  publicApiKeyRef: { ...secretReference, required: false }
11
11
  }),
12
12
  settingsFields: [
13
- { name: "applicationId", label: "Application ID", hint: "Copy the ID from your Algolia application's API Keys page. This ID can be public." },
14
- { name: "publicApiKeyRef", environmentCredential: { label: "Public frontend API key", public: true }, label: "Public API key reference (optional)", placeholder: "env:ALGOLIA_SEARCH_KEY",
15
- hint: "Optional search-only key for your frontend. Enter its reference; your app decides whether to expose its value." }
13
+ { name: "applicationId", label: "jskit.connectors.algolia.fields.applicationId.label", hint: "jskit.connectors.algolia.fields.applicationId.hint" },
14
+ { name: "publicApiKeyRef", environmentCredential: { label: "jskit.connectors.algolia.fields.publicApiKeyRef.environmentCredential.label", public: true }, label: "jskit.connectors.algolia.fields.publicApiKeyRef.label", placeholder: "env:ALGOLIA_SEARCH_KEY",
15
+ hint: "jskit.connectors.algolia.fields.publicApiKeyRef.hint" }
16
16
  ],
17
17
  setup: {
18
18
  url: "https://www.algolia.com/doc/guides/security/api-keys",