@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
package/src/server/n8n.js CHANGED
@@ -12,7 +12,7 @@ const n8nProvider = Object.freeze({
12
12
  oauth: (settings) => {
13
13
  const discovery = settings.oauthDiscovery;
14
14
  if (!validN8nOAuthDiscovery(discovery) || discovery.resource !== new URL(settings.serverUrl).href) {
15
- throw new ConnectorError("connector_discovery_required", "Discover OAuth settings for this exact n8n MCP URL before connecting.");
15
+ throw new ConnectorError("connector_discovery_required", "Discover OAuth settings for this exact n8n MCP URL before connecting.", { messageCode: "connectors.n8n.discoverOauthSettingsExactMcp" });
16
16
  }
17
17
  return discovery.oauth;
18
18
  },
@@ -51,8 +51,8 @@ async function discoverN8nOAuth({ serverUrl }, { fetchImpl = globalThis.fetch, s
51
51
  }
52
52
  return discovery;
53
53
  } catch {
54
- if (requestSignal.aborted) throw new ConnectorError("connector_discovery_interrupted", "n8n OAuth discovery was interrupted.");
55
- throw new ConnectorError("connector_discovery_failed", "n8n OAuth discovery failed. Check the MCP URL, advertised authority and supported server version.", { statusCode: 502 });
54
+ if (requestSignal.aborted) throw new ConnectorError("connector_discovery_interrupted", "n8n OAuth discovery was interrupted.", { messageCode: "connectors.n8n.oauthDiscoveryInterrupted" });
55
+ throw new ConnectorError("connector_discovery_failed", "n8n OAuth discovery failed. Check the MCP URL, advertised authority and supported server version.", { statusCode: 502, messageCode: "connectors.n8n.oauthDiscoveryFailedCheckMcp" });
56
56
  }
57
57
  }
58
58
 
@@ -69,7 +69,7 @@ async function registerN8nClient(input, options) {
69
69
  const values = validateSchemaPayload({ schema: registrationSchema, mode: "replace" }, input, { statusCode: 422 });
70
70
  const discovery = await discoverN8nOAuth({ serverUrl: values.serverUrl }, options);
71
71
  if (values.scopes.some((scope) => !discovery.scopes.includes(scope))) {
72
- throw new ConnectorError("connector_scope_unavailable", "Select only permissions advertised by this n8n MCP server.", { statusCode: 422 });
72
+ throw new ConnectorError("connector_scope_unavailable", "Select only permissions advertised by this n8n MCP server.", { statusCode: 422, messageCode: "connectors.n8n.selectPermissionsAdvertisedMcpServer" });
73
73
  }
74
74
  const client = await registerMcpClient("n8n", discovery.oauth, values, options);
75
75
  return { ...client, ...discovery, requestedScopes: [...values.scopes] };
@@ -19,7 +19,7 @@ function collection(endpoint, scope = "daily", timed = false) {
19
19
  const request = operation.request(input, settings);
20
20
  const query = new URL(request.url).searchParams;
21
21
  if (query.has(start) && query.has(end) && Date.parse(query.get(start)) > Date.parse(query.get(end)))
22
- throw new ConnectorError("connector_input_invalid", "Start must be before or equal to end.", { statusCode: 422 });
22
+ throw new ConnectorError("connector_input_invalid", "Start must be before or equal to end.", { statusCode: 422, messageCode: "connectors.oura.startNotAfterEnd" });
23
23
  return request;
24
24
  } };
25
25
  }
@@ -49,7 +49,7 @@ const paddleProvider = Object.freeze({
49
49
  request(input, settings) {
50
50
  const { notification_setting_id, ...body } = validateSchemaPayload(
51
51
  { schema: webhookUpdateSchema, mode: "replace" }, input, { statusCode: 422 });
52
- if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Choose at least one notification setting to change.", { statusCode: 422 });
52
+ if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Choose at least one notification setting to change.", { statusCode: 422, messageCode: "connectors.paddle.chooseLeastNotificationSettingChange" });
53
53
  return { method: "PATCH", url: `${origins[settings.environment]}/notification-settings/${notification_setting_id}`, body };
54
54
  },
55
55
  validateResult: (result) => /^ntfset_[a-z0-9]{26}$/.test(result?.data?.id || "") && typeof result.data.active === "boolean"
@@ -6,7 +6,7 @@ import { ConnectorError } from "@jskit-ai/connectors-core/server";
6
6
 
7
7
  function companyApiDomain(value) {
8
8
  if (typeof value !== "string" || !/^https:\/\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.pipedrive\.com$/.test(value)) {
9
- throw new ConnectorError("connector_response_invalid", "Pipedrive returned an invalid company API address.", { statusCode: 502 });
9
+ throw new ConnectorError("connector_response_invalid", "Pipedrive returned an invalid company API address.", { statusCode: 502, messageCode: "connectors.pipedrive.returnedInvalidCompanyApiAddress" });
10
10
  }
11
11
  return value;
12
12
  }
@@ -34,8 +34,8 @@ function crmOperation(collection, action) {
34
34
  return { scopes: [], request(input, settings = {}, data) {
35
35
  const { id, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
36
36
  if (action === "update" && !Object.keys(values).length || lead && action === "create" && !values.person_id && !values.organization_id)
37
- throw new ConnectorError("connector_input_invalid", "Supply changed fields; a new lead also needs a person or organization.", { statusCode: 422 });
38
- if (!data && !settings.companyDomain) throw new ConnectorError("connector_binding_missing", "Set the Pipedrive Company domain for API-token CRM operations.", { statusCode: 422 });
37
+ throw new ConnectorError("connector_input_invalid", "Supply changed fields; a new lead also needs a person or organization.", { statusCode: 422, messageCode: "connectors.pipedrive.supplyChangedFieldsNewLead" });
38
+ if (!data && !settings.companyDomain) throw new ConnectorError("connector_binding_missing", "Set the Pipedrive Company domain for API-token CRM operations.", { statusCode: 422, messageCode: "connectors.pipedrive.setCompanyDomainApiToken" });
39
39
  const origin = data ? companyApiDomain(data.apiDomain) : companyApiDomain(`https://${settings.companyDomain}.pipedrive.com`);
40
40
  const url = new URL(`${origin}/api/${lead ? "v1" : "v2"}/${collection}${id === undefined ? "" : `/${id}`}`);
41
41
  if (!write) for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
@@ -56,7 +56,7 @@ const pipedriveProvider = Object.freeze({
56
56
  dataFromTokenResponse(response, previous) {
57
57
  const apiDomain = companyApiDomain(response.api_domain);
58
58
  if (previous && apiDomain !== previous.apiDomain) {
59
- throw new ConnectorError("connector_reconnect_required", "The Pipedrive company address changed. Connect this account again.", { statusCode: 401 });
59
+ throw new ConnectorError("connector_reconnect_required", "The Pipedrive company address changed. Connect this account again.", { statusCode: 401, messageCode: "connectors.pipedrive.companyAddressChangedConnectAccount" });
60
60
  }
61
61
  return { apiDomain };
62
62
  },
@@ -9,7 +9,7 @@ const posthogProvider = Object.freeze({
9
9
  apiOrigins: (settings) => [origin(settings)],
10
10
  apiKey: { bodyParameter: "api_key", headers: (token) => {
11
11
  if (!/^phc_[a-zA-Z0-9_-]+$/u.test(token)) {
12
- throw new ConnectorError("connector_binding_missing", "Use the public PostHog project token starting with phc_.");
12
+ throw new ConnectorError("connector_binding_missing", "Use the public PostHog project token starting with phc_.", { messageCode: "connectors.posthog.usePublicProjectTokenStarting" });
13
13
  }
14
14
  return { "User-Agent": "jskit-connectors/0.1.0 (posthog-node/compatible)" };
15
15
  } },
@@ -24,10 +24,10 @@ const posthogProvider = Object.freeze({
24
24
  typeof result.errorsWhileComputingFlags !== "boolean" ||
25
25
  (result.quotaLimited !== undefined && (!Array.isArray(result.quotaLimited) || !result.quotaLimited.every((item) => typeof item === "string")))) return false;
26
26
  if (result.quotaLimited?.includes("feature_flags")) {
27
- throw new ConnectorError("connector_quota_limited", "PostHog's feature flag quota has been reached.", { statusCode: 429 });
27
+ throw new ConnectorError("connector_quota_limited", "PostHog's feature flag quota has been reached.", { statusCode: 429, messageCode: "connectors.posthog.featureFlagQuotaReached" });
28
28
  }
29
29
  if (result.errorsWhileComputingFlags) {
30
- throw new ConnectorError("connector_provider_failed", "PostHog could not evaluate all requested flags.", { statusCode: 502 });
30
+ throw new ConnectorError("connector_provider_failed", "PostHog could not evaluate all requested flags.", { statusCode: 502, messageCode: "connectors.posthog.couldNotEvaluateAllRequested" });
31
31
  }
32
32
  return Object.values(result.flags).every((flag) => flag && typeof flag.enabled === "boolean" &&
33
33
  (flag.variant === undefined || typeof flag.variant === "string"));
@@ -28,7 +28,7 @@ const salesforceProvider = Object.freeze({
28
28
  let value;
29
29
  try { value = await response.clone().json(); } catch { /* reported below */ }
30
30
  if (!record(value) || ![origin(settings), origin(settings) + "/"].includes(value.instance_url) || typeof value.scope !== "string" || !value.scope.trim()) {
31
- throw new ConnectorError("connector_response_invalid", "Salesforce returned an invalid grant or a different organisation.", { statusCode: 502 });
31
+ throw new ConnectorError("connector_response_invalid", "Salesforce returned an invalid grant or a different organisation.", { statusCode: 502, messageCode: "connectors.salesforce.returnedInvalidGrantDifferentOrganisation" });
32
32
  }
33
33
  // Salesforce can omit expiry. Renew after five minutes of use as a local
34
34
  // policy; this is not a claim about the org's actual session lifetime.
@@ -65,17 +65,17 @@ const salesforceProvider = Object.freeze({
65
65
  try { value = await response.json(); } catch { /* checked after status */ }
66
66
  const code = Array.isArray(value) ? value[0]?.errorCode : null;
67
67
  if (!response.ok) {
68
- if (code === "REQUEST_LIMIT_EXCEEDED") throw new ConnectorError("connector_rate_limited", "The Salesforce organisation's API request limit was reached.", { statusCode: 429 });
69
- if (code === "INVALID_SESSION_ID") throw new ConnectorError("connector_reconnect_required", "Connect this Salesforce account again.", { statusCode: 401 });
70
- if (code === "INVALID_QUERY_LOCATOR") throw new ConnectorError("connector_cursor_expired", "Start the Salesforce query again; its result cursor is no longer valid.", { statusCode: 410 });
71
- if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Salesforce resource is not available.", { statusCode: 404 });
72
- if (response.status === 400) throw new ConnectorError("connector_query_invalid", "Salesforce rejected the query or object. Check its API names and the user's permissions.", { statusCode: 422 });
68
+ if (code === "REQUEST_LIMIT_EXCEEDED") throw new ConnectorError("connector_rate_limited", "The Salesforce organisation's API request limit was reached.", { statusCode: 429, messageCode: "connectors.salesforce.organisationApiRequestLimitReached" });
69
+ if (code === "INVALID_SESSION_ID") throw new ConnectorError("connector_reconnect_required", "Connect this Salesforce account again.", { statusCode: 401, messageCode: "connectors.shared.reconnectAccount", messageParams: { provider: "Salesforce" } });
70
+ if (code === "INVALID_QUERY_LOCATOR") throw new ConnectorError("connector_cursor_expired", "Start the Salesforce query again; its result cursor is no longer valid.", { statusCode: 410, messageCode: "connectors.salesforce.startQueryAgainResultCursor" });
71
+ if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Salesforce resource is not available.", { statusCode: 404, messageCode: "connectors.shared.resourceUnavailable", messageParams: { provider: "Salesforce" } });
72
+ if (response.status === 400) throw new ConnectorError("connector_query_invalid", "Salesforce rejected the query or object. Check its API names and the user's permissions.", { statusCode: 422, messageCode: "connectors.salesforce.rejectedQueryObjectCheckApi" });
73
73
  throw Object.assign(new Error("Salesforce request failed."), { status: response.status });
74
74
  }
75
- if (response.status !== 200 || !record(value)) throw new ConnectorError("connector_response_invalid", "Salesforce returned an unexpected response.", { statusCode: 502 });
75
+ if (response.status !== 200 || !record(value)) throw new ConnectorError("connector_response_invalid", "Salesforce returned an unexpected response.", { statusCode: 502, messageCode: "connectors.shared.unexpectedResponse", messageParams: { provider: "Salesforce" } });
76
76
  const url = new URL(address);
77
77
  if (url.pathname.endsWith("/describe") && value.name !== url.pathname.split("/").at(-2)) {
78
- throw new ConnectorError("connector_response_invalid", "Salesforce described a different object.", { statusCode: 502 });
78
+ throw new ConnectorError("connector_response_invalid", "Salesforce described a different object.", { statusCode: 502, messageCode: "connectors.salesforce.describedDifferentObject" });
79
79
  }
80
80
  return value;
81
81
  }
@@ -44,11 +44,11 @@ function semrushKeywordReport(target, kind) {
44
44
  return {
45
45
  scopes: [],
46
46
  request(input, settings) {
47
- if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the separate Semrush V3 API key reference for keyword reports.");
47
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the separate Semrush V3 API key reference for keyword reports.", { messageCode: "connectors.semrush.configureSeparateV3ApiKey" });
48
48
  const result = operation.request(input);
49
49
  const url = new URL(result.url);
50
50
  const limit = Number(url.searchParams.get("limit")), offset = Number(url.searchParams.get("offset"));
51
- if (limit + offset > 4_000_000) throw new ConnectorError("connector_input_invalid", "The requested Semrush page exceeds the provider's result window.", { statusCode: 422 });
51
+ if (limit + offset > 4_000_000) throw new ConnectorError("connector_input_invalid", "The requested Semrush page exceeds the provider's result window.", { statusCode: 422, messageCode: "connectors.semrush.requestedPageExceedsProviderResult" });
52
52
  url.searchParams.delete("limit"); url.searchParams.delete("offset");
53
53
  url.searchParams.set("display_limit", String(limit + offset));
54
54
  url.searchParams.set("display_offset", String(offset));
@@ -82,13 +82,13 @@ function semrushTrackingReport(type, positions = false) {
82
82
  Object.values(value.data).length <= Number(url.searchParams.get("display_limit")) &&
83
83
  Object.values(value.data).every((entry) => typeof entry?.Ph === "string" && typeof entry.Pi === "string" && entry.Dt !== null && typeof entry.Dt === "object"));
84
84
  }, request(input, settings) {
85
- if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for position tracking.");
85
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for position tracking.", { messageCode: "connectors.semrush.configureV3KeyPositionTracking" });
86
86
  const result = operation.request(input);
87
87
  const url = new URL(result.url);
88
88
  url.pathname = `/reports/v1/projects/${url.searchParams.get("campaignId")}/tracking/`;
89
89
  url.searchParams.delete("campaignId");
90
90
  if (url.searchParams.has("date_begin") && url.searchParams.has("date_end") && url.searchParams.get("date_begin") > url.searchParams.get("date_end")) {
91
- throw new ConnectorError("connector_input_invalid", "The tracking start date must not follow its end date.", { statusCode: 422 });
91
+ throw new ConnectorError("connector_input_invalid", "The tracking start date must not follow its end date.", { statusCode: 422, messageCode: "connectors.semrush.trackingStartDateMustNot" });
92
92
  }
93
93
  url.searchParams.set("action", "report"); url.searchParams.set("type", type);
94
94
  return { ...result, url: url.href };
@@ -109,11 +109,11 @@ function parseSemrushCsv(text) {
109
109
  row.push(field); field = ""; closed = false;
110
110
  if (char !== ";") { rows.push(row); row = []; if (char === "\r" && text[i + 1] === "\n") i++; }
111
111
  } else {
112
- if (closed || char === '"') throw new ConnectorError("connector_response_invalid", "Semrush returned malformed CSV.", { statusCode: 502 });
112
+ if (closed || char === '"') throw new ConnectorError("connector_response_invalid", "Semrush returned malformed CSV.", { statusCode: 502, messageCode: "connectors.semrush.returnedMalformedCsv" });
113
113
  field += char;
114
114
  }
115
115
  }
116
- if (quoted) throw new ConnectorError("connector_response_invalid", "Semrush returned incomplete CSV.", { statusCode: 502 });
116
+ if (quoted) throw new ConnectorError("connector_response_invalid", "Semrush returned incomplete CSV.", { statusCode: 502, messageCode: "connectors.semrush.returnedIncompleteCsv" });
117
117
  if (field !== "" || closed || row.length) { row.push(field); rows.push(row); }
118
118
  return rows;
119
119
  }
@@ -157,7 +157,7 @@ const semrushProvider = Object.freeze({
157
157
  ...semrushDefinition, apiOrigins: ["https://api.semrush.com"],
158
158
  apiKey: { headers: (key) => {
159
159
  if (/[\s\p{Cc}]/u.test(key)) {
160
- throw new ConnectorError("connector_binding_missing", "Use the Semrush V4 API key without spaces or control characters.");
160
+ throw new ConnectorError("connector_binding_missing", "Use the Semrush V4 API key without spaces or control characters.", { messageCode: "connectors.semrush.useV4ApiKeyWithout" });
161
161
  }
162
162
  return { Authorization: `Apikey ${key}` };
163
163
  } },
@@ -169,7 +169,7 @@ const semrushProvider = Object.freeze({
169
169
  typeof campaign.url === "string" && typeof campaign.device === "string" && typeof campaign.isGathering === "boolean"),
170
170
  request(input, settings) {
171
171
  const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
172
- if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for campaign discovery.");
172
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for campaign discovery.", { messageCode: "connectors.semrush.configureV3KeyCampaignDiscovery" });
173
173
  return { method: "GET", url: `https://api.semrush.com/management/v1/projects/${projectId}/tracking/campaigns` };
174
174
  } },
175
175
  "tracking.dates": semrushTrackingReport("tracking_campaign_dates"),
@@ -184,7 +184,7 @@ const semrushProvider = Object.freeze({
184
184
  "domains.overview": {
185
185
  scopes: [],
186
186
  request(input, settings) {
187
- if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the separate Semrush V3 API key reference for domain reports.");
187
+ if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the separate Semrush V3 API key reference for domain reports.", { messageCode: "connectors.semrush.configureSeparateV3ApiKeyReferenceDomainReports" });
188
188
  const operation = jsonOperation("https://api.semrush.com/", {
189
189
  domain: { type: "string", required: true, minLength: 1, maxLength: 253, pattern: /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/iu },
190
190
  database: { type: "string", required: true, pattern: /^[a-z]{2}$/u }
@@ -214,7 +214,7 @@ const semrushProvider = Object.freeze({
214
214
  const url = new URL(result.url);
215
215
  const month = url.searchParams.get("month");
216
216
  if (month && (month < "2012-01" || month > new Date().toISOString().slice(0, 7))) {
217
- throw new ConnectorError("connector_input_invalid", "Choose a month from 2012-01 through the current month.", { statusCode: 422 });
217
+ throw new ConnectorError("connector_input_invalid", "Choose a month from 2012-01 through the current month.", { statusCode: 422, messageCode: "connectors.semrush.chooseMonth201201Through" });
218
218
  }
219
219
  url.searchParams.set("format", "json");
220
220
  return { ...result, url: url.href };
@@ -274,10 +274,10 @@ const semrushProvider = Object.freeze({
274
274
  if (path === "/" || path.startsWith("/management/v1/projects/") || path.startsWith("/reports/v1/projects/")) {
275
275
  let key;
276
276
  try { key = await resolveReference(settings.v3ApiKeyRef); } catch {
277
- throw new ConnectorError("connector_binding_missing", "Provide the Semrush V3 API key through its private Env reference.");
277
+ throw new ConnectorError("connector_binding_missing", "Provide the Semrush V3 API key through its private Env reference.", { messageCode: "connectors.semrush.provideV3ApiKeyThrough" });
278
278
  }
279
279
  if (typeof key !== "string" || !key || key === "MISSING" || /[\s\p{Cc}]/u.test(key)) {
280
- throw new ConnectorError("connector_binding_missing", "Provide a valid Semrush V3 API key.");
280
+ throw new ConnectorError("connector_binding_missing", "Provide a valid Semrush V3 API key.", { messageCode: "connectors.semrush.provideValidV3ApiKey" });
281
281
  }
282
282
  const url = new URL(address);
283
283
  url.searchParams.set("key", key);
@@ -291,12 +291,12 @@ const semrushProvider = Object.freeze({
291
291
  options.signal.throwIfAborted();
292
292
  const status = response?.status;
293
293
  throw new ConnectorError(status === 429 ? "connector_rate_limited" : status === 403 ? "connector_permission_denied" : "connector_provider_failed",
294
- "Semrush could not return the V3 report.", { statusCode: status === 429 ? 429 : status === 403 ? 403 : 502 });
294
+ "Semrush could not return the V3 report.", { statusCode: status === 429 ? 429 : status === 403 ? 403 : 502, messageCode: "connectors.semrush.couldNotReturnV3Report" });
295
295
  }
296
- if (text.length > 2_000_000) throw new ConnectorError("connector_response_invalid", "Semrush returned an oversized V3 report.", { statusCode: 502 });
296
+ if (text.length > 2_000_000) throw new ConnectorError("connector_response_invalid", "Semrush returned an oversized V3 report.", { statusCode: 502, messageCode: "connectors.semrush.returnedOversizedV3Report" });
297
297
  if (path !== "/") {
298
298
  try { return JSON.parse(text); } catch {
299
- throw new ConnectorError("connector_response_invalid", "Semrush returned invalid tracking JSON.", { statusCode: 502 });
299
+ throw new ConnectorError("connector_response_invalid", "Semrush returned invalid tracking JSON.", { statusCode: 502, messageCode: "connectors.semrush.returnedInvalidTrackingJson" });
300
300
  }
301
301
  }
302
302
  const columns = url.searchParams.get("type") === "domain_rank" ? domainColumns : keywordColumns;
@@ -305,15 +305,15 @@ const semrushProvider = Object.freeze({
305
305
  if (error) {
306
306
  if (error[1] === "50") return { columns, rows: [] };
307
307
  const code = Number(error[1]);
308
- if ([110, 120].includes(code)) throw new ConnectorError("connector_binding_missing", "Replace the separate Semrush V3 key in Env.");
309
- if ([130, 133, 135].includes(code)) throw new ConnectorError("connector_permission_denied", "Check Semrush V3 API, database and report entitlement.", { statusCode: 403 });
310
- if ([131, 132, 134].includes(code)) throw new ConnectorError("connector_quota_limited", "Check Semrush V3 API units and report limits.", { statusCode: 429 });
311
- throw new ConnectorError("connector_provider_failed", "Semrush rejected the V3 report. Check its inputs and account access.", { statusCode: 502 });
308
+ if ([110, 120].includes(code)) throw new ConnectorError("connector_binding_missing", "Replace the separate Semrush V3 key in Env.", { messageCode: "connectors.semrush.replaceSeparateV3KeyEnv" });
309
+ if ([130, 133, 135].includes(code)) throw new ConnectorError("connector_permission_denied", "Check Semrush V3 API, database and report entitlement.", { statusCode: 403, messageCode: "connectors.semrush.checkV3ApiDatabaseReport" });
310
+ if ([131, 132, 134].includes(code)) throw new ConnectorError("connector_quota_limited", "Check Semrush V3 API units and report limits.", { statusCode: 429, messageCode: "connectors.semrush.checkV3ApiUnitsReport" });
311
+ throw new ConnectorError("connector_provider_failed", "Semrush rejected the V3 report. Check its inputs and account access.", { statusCode: 502, messageCode: "connectors.semrush.rejectedV3ReportCheckInputs" });
312
312
  }
313
313
  const rows = parseSemrushCsv(text);
314
314
  if (JSON.stringify(rows.shift()) !== JSON.stringify(columns) || rows.length > rowLimit ||
315
315
  rows.some((row) => row.length !== columns.length)) {
316
- throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected V3 report columns or rows.", { statusCode: 502 });
316
+ throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected V3 report columns or rows.", { statusCode: 502, messageCode: "connectors.semrush.returnedUnexpectedV3ReportColumns" });
317
317
  }
318
318
  return { columns, rows };
319
319
  }
@@ -321,18 +321,18 @@ const semrushProvider = Object.freeze({
321
321
  if (result?.meta?.success === false) {
322
322
  const code = result.error?.code;
323
323
  if ([70, 120, 121, 122].includes(code)) {
324
- throw new ConnectorError("connector_reconnect_required", "Replace the Semrush V4 key and connect again.", { statusCode: 401 });
324
+ throw new ConnectorError("connector_reconnect_required", "Replace the Semrush V4 key and connect again.", { statusCode: 401, messageCode: "connectors.semrush.replaceV4KeyConnectAgain" });
325
325
  }
326
326
  if (code === 130) {
327
- throw new ConnectorError("connector_permission_denied", "Semrush API access is disabled for this account.", { statusCode: 403 });
327
+ throw new ConnectorError("connector_permission_denied", "Semrush API access is disabled for this account.", { statusCode: 403, messageCode: "connectors.semrush.apiAccessDisabledAccount" });
328
328
  }
329
329
  if ([131, 132, 134].includes(code)) {
330
- throw new ConnectorError("connector_quota_limited", "Semrush API capacity is exhausted. Check the account's subscription and available units.", { statusCode: 429 });
330
+ throw new ConnectorError("connector_quota_limited", "Semrush API capacity is exhausted. Check the account's subscription and available units.", { statusCode: 429, messageCode: "connectors.semrush.apiCapacityExhaustedCheckAccount" });
331
331
  }
332
332
  if (code === 512) {
333
- throw new ConnectorError("connector_resource_not_found", "The Semrush project was not found or is not accessible.", { statusCode: 404 });
333
+ throw new ConnectorError("connector_resource_not_found", "The Semrush project was not found or is not accessible.", { statusCode: 404, messageCode: "connectors.semrush.projectNotFoundNotAccessible" });
334
334
  }
335
- throw new ConnectorError("connector_provider_failed", "Semrush could not complete this request.", { statusCode: 502 });
335
+ throw new ConnectorError("connector_provider_failed", "Semrush could not complete this request.", { statusCode: 502, messageCode: "connectors.semrush.couldNotCompleteRequest" });
336
336
  }
337
337
  return result;
338
338
  }
@@ -29,7 +29,7 @@ function productMutation(name, fields) {
29
29
  return { ...operation, scopes: ["write_products"], request(input, settings) {
30
30
  const request = operation.request(input, settings);
31
31
  if (name === "Update" && Object.keys(request.body.variables.product).length === 1) {
32
- throw new ConnectorError("connector_input_invalid", "Select at least one product detail to update.", { statusCode: 422 });
32
+ throw new ConnectorError("connector_input_invalid", "Select at least one product detail to update.", { statusCode: 422, messageCode: "connectors.shopify.selectLeastProductDetailUpdate" });
33
33
  }
34
34
  return request;
35
35
  } };
@@ -48,7 +48,7 @@ const shopifyProvider = Object.freeze({
48
48
  if (typeof token?.access_token !== "string" || !token.access_token.trim() ||
49
49
  !Number.isSafeInteger(token.expires_in) || token.expires_in <= 0 ||
50
50
  typeof token.scope !== "string" || !/^[a-z_]+(?:,[a-z_]+)*$/u.test(token.scope)) {
51
- throw new ConnectorError("connector_response_invalid", "Shopify returned an invalid token grant.", { statusCode: 502 });
51
+ throw new ConnectorError("connector_response_invalid", "Shopify returned an invalid token grant.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenGrant", messageParams: { provider: "Shopify" } });
52
52
  }
53
53
  // Shopify's documented client-credentials response omits the standard token_type.
54
54
  return Response.json({ ...token, token_type: token.token_type ?? "Bearer" });
@@ -80,17 +80,17 @@ const shopifyProvider = Object.freeze({
80
80
  const result = await request(address, { ...options, headers });
81
81
  if (Array.isArray(result?.errors) && result.errors.length) {
82
82
  const codes = result.errors.map((error) => error?.extensions?.code);
83
- if (codes.includes("THROTTLED")) throw new ConnectorError("connector_rate_limited", "Shopify's request limit was reached.", { statusCode: 429 });
84
- if (codes.includes("ACCESS_DENIED")) throw new ConnectorError("connector_permission_denied", "Shopify denied this operation.", { statusCode: 403 });
83
+ if (codes.includes("THROTTLED")) throw new ConnectorError("connector_rate_limited", "Shopify's request limit was reached.", { statusCode: 429, messageCode: "connectors.shared.requestLimitReached", messageParams: { provider: "Shopify" } });
84
+ if (codes.includes("ACCESS_DENIED")) throw new ConnectorError("connector_permission_denied", "Shopify denied this operation.", { statusCode: 403, messageCode: "connectors.shared.operationDenied", messageParams: { provider: "Shopify" } });
85
85
  }
86
86
  for (const name of ["productCreate", "productUpdate", "productDelete"]) {
87
87
  const payload = result?.data?.[name];
88
88
  if (Array.isArray(payload?.userErrors) && payload.userErrors.length) {
89
- throw new ConnectorError("connector_operation_rejected", "Shopify rejected the product change. Check the product values and permissions before retrying.", { statusCode: 422 });
89
+ throw new ConnectorError("connector_operation_rejected", "Shopify rejected the product change. Check the product values and permissions before retrying.", { statusCode: 422, messageCode: "connectors.shopify.rejectedProductChangeCheckProduct" });
90
90
  }
91
91
  }
92
92
  if (result?.data?.products?.nodes?.length > options.body.variables.first) {
93
- throw new ConnectorError("connector_response_invalid", "Shopify returned more products than requested.", { statusCode: 502 });
93
+ throw new ConnectorError("connector_response_invalid", "Shopify returned more products than requested.", { statusCode: 502, messageCode: "connectors.shopify.returnedMoreProductsRequested" });
94
94
  }
95
95
  return result;
96
96
  }
@@ -6,41 +6,41 @@ import { jsonOperation } from "./jsonOperation.js";
6
6
  function checkReply(value) {
7
7
  if (value?.ok === true) return value;
8
8
  if (value?.ok !== false || typeof value.error !== "string") {
9
- throw new ConnectorError("connector_response_invalid", "Slack returned an invalid response.", { statusCode: 502 });
9
+ throw new ConnectorError("connector_response_invalid", "Slack returned an invalid response.", { statusCode: 502, messageCode: "connectors.shared.invalidResponse", messageParams: { provider: "Slack" } });
10
10
  }
11
11
  if (["invalid_auth", "not_authed", "token_revoked", "token_expired", "account_inactive", "invalid_refresh_token", "invalid_grant"].includes(value.error)) {
12
- throw new ConnectorError("connector_reconnect_required", "Connect this Slack account again.", { statusCode: 401 });
12
+ throw new ConnectorError("connector_reconnect_required", "Connect this Slack account again.", { statusCode: 401, messageCode: "connectors.shared.reconnectAccount", messageParams: { provider: "Slack" } });
13
13
  }
14
14
  if (["missing_scope", "not_allowed_token_type", "access_denied", "no_permission", "not_in_channel", "restricted_action"].includes(value.error)) {
15
- throw new ConnectorError("connector_permission_denied", "Slack denied this operation.", { statusCode: 403 });
15
+ throw new ConnectorError("connector_permission_denied", "Slack denied this operation.", { statusCode: 403, messageCode: "connectors.shared.operationDenied", messageParams: { provider: "Slack" } });
16
16
  }
17
17
  if (["ratelimited", "rate_limited"].includes(value.error)) {
18
- throw new ConnectorError("connector_rate_limited", "Slack's request limit was reached.", { statusCode: 429 });
18
+ throw new ConnectorError("connector_rate_limited", "Slack's request limit was reached.", { statusCode: 429, messageCode: "connectors.shared.requestLimitReached", messageParams: { provider: "Slack" } });
19
19
  }
20
20
  if (["invalid_client_id", "bad_client_secret", "bad_redirect_uri"].includes(value.error)) {
21
- throw new ConnectorError("connector_registration_invalid", "Check the Slack client registration and callback.", { statusCode: 422 });
21
+ throw new ConnectorError("connector_registration_invalid", "Check the Slack client registration and callback.", { statusCode: 422, messageCode: "connectors.slack.checkClientRegistrationCallback" });
22
22
  }
23
- throw new ConnectorError("connector_provider_failed", "The Slack request failed. Try again.", { statusCode: 502 });
23
+ throw new ConnectorError("connector_provider_failed", "The Slack request failed. Try again.", { statusCode: 502, messageCode: "connectors.slack.requestFailedTryAgain" });
24
24
  }
25
25
 
26
26
  async function normalizeTokenResponse(response, { settings, grantType }) {
27
27
  if ([401, 403, 429].includes(response.status)) {
28
28
  throw new ConnectorError(
29
29
  { 401: "connector_reconnect_required", 403: "connector_permission_denied", 429: "connector_rate_limited" }[response.status],
30
- "Slack could not authorize this request.", { statusCode: response.status }
30
+ "Slack could not authorize this request.", { statusCode: response.status, messageCode: "connectors.slack.couldNotAuthorizeRequest" }
31
31
  );
32
32
  }
33
33
  let value;
34
34
  try { value = await response.clone().json(); } catch {
35
35
  if (!response.ok) return response;
36
- throw new ConnectorError("connector_response_invalid", "Slack returned an invalid token response.", { statusCode: 502 });
36
+ throw new ConnectorError("connector_response_invalid", "Slack returned an invalid token response.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenResponse", messageParams: { provider: "Slack" } });
37
37
  }
38
38
  checkReply(value);
39
39
  if (!response.ok) return response;
40
40
  const actor = settings.actor || "user";
41
41
  const token = grantType === "authorization_code" && actor === "user" ? value.authed_user : value;
42
42
  if (token?.token_type !== actor || typeof token.scope !== "string" || !/^[^\s,]+(?:,[^\s,]+)*$/u.test(token.scope)) {
43
- throw new ConnectorError("connector_response_invalid", "Slack returned a token for the wrong identity or an invalid permission grant.", { statusCode: 502 });
43
+ throw new ConnectorError("connector_response_invalid", "Slack returned a token for the wrong identity or an invalid permission grant.", { statusCode: 502, messageCode: "connectors.slack.returnedTokenWrongIdentityInvalid" });
44
44
  }
45
45
  // Retain exactly one identity; a response may contain both user and bot tokens.
46
46
  return Response.json({ access_token: token.access_token, token_type: "Bearer", scope: token.scope,
@@ -109,7 +109,7 @@ const slackProvider = Object.freeze({
109
109
  });
110
110
  // The app supplies raw bytes before body parsing and binds the expected installation.
111
111
  function verifySlackRequest({ rawBody, signature, timestamp, secret, contentType, appId, teamId, now = Date.now() }) {
112
- const reject = () => { throw new ConnectorError("connector_webhook_invalid", "Invalid Slack callback.", { statusCode: 401 }); };
112
+ const reject = () => { throw new ConnectorError("connector_webhook_invalid", "Invalid Slack callback.", { statusCode: 401, messageCode: "connectors.shared.invalidCallback", messageParams: { provider: "Slack" } }); };
113
113
  if (!(rawBody instanceof Uint8Array) || rawBody.byteLength > 1048576 ||
114
114
  typeof secret !== "string" || !secret || typeof signature !== "string" || !/^v0=[a-f0-9]{64}$/u.test(signature) ||
115
115
  typeof timestamp !== "string" || !/^[0-9]{1,12}$/u.test(timestamp) || !Number.isFinite(now) ||
@@ -78,7 +78,7 @@ const snowflakeProvider = Object.freeze({
78
78
  if (!Number.isSafeInteger(value?.expires_in) || value.expires_in <= 0 ||
79
79
  (value.scope !== undefined && (typeof value.scope !== "string" || !/^[^\s]+(?: [^\s]+)*$/u.test(value.scope))) ||
80
80
  (value.refresh_token !== undefined && (typeof value.refresh_token !== "string" || !value.refresh_token.trim()))) {
81
- throw new ConnectorError("connector_response_invalid", "Snowflake returned an invalid token grant.", { statusCode: 502 });
81
+ throw new ConnectorError("connector_response_invalid", "Snowflake returned an invalid token grant.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenGrant", messageParams: { provider: "Snowflake" } });
82
82
  }
83
83
  }
84
84
  return response;
@@ -89,7 +89,7 @@ const snowflakeProvider = Object.freeze({
89
89
  ...(options.body !== undefined ? { body: JSON.stringify(options.body), headers: { ...options.headers, "Content-Type": "application/json" } } : {}) });
90
90
  const url = new URL(address);
91
91
  const sql = url.pathname.startsWith("/api/v2/statements");
92
- if (sql && response.status === 422) throw new ConnectorError("connector_operation_rejected", "Snowflake rejected the statement. Inspect query history with the authorized role.", { statusCode: 422 });
92
+ if (sql && response.status === 422) throw new ConnectorError("connector_operation_rejected", "Snowflake rejected the statement. Inspect query history with the authorized role.", { statusCode: 422, messageCode: "connectors.snowflake.rejectedStatementInspectQueryHistory" });
93
93
  if (!response.ok) throw Object.assign(new Error("Snowflake request failed."), { status: response.status });
94
94
  const reader = response.body?.getReader();
95
95
  const chunks = [];
@@ -101,7 +101,7 @@ const snowflakeProvider = Object.freeze({
101
101
  size += value.byteLength;
102
102
  if (size > 16 * 1024 * 1024) {
103
103
  await reader.cancel();
104
- throw new ConnectorError("connector_response_too_large", "Snowflake response exceeds 16 MiB. Reduce the query result or use a native streaming client.", { statusCode: 413 });
104
+ throw new ConnectorError("connector_response_too_large", "Snowflake response exceeds 16 MiB. Reduce the query result or use a native streaming client.", { statusCode: 413, messageCode: "connectors.snowflake.responseExceeds16MibReduce" });
105
105
  }
106
106
  chunks.push(value);
107
107
  }
@@ -109,7 +109,7 @@ const snowflakeProvider = Object.freeze({
109
109
  let result;
110
110
  try { result = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { /* rejected below */ }
111
111
  if (sql) {
112
- const reject = () => { throw new ConnectorError("connector_response_invalid", "Snowflake returned an invalid statement response.", { statusCode: 502 }); };
112
+ const reject = () => { throw new ConnectorError("connector_response_invalid", "Snowflake returned an invalid statement response.", { statusCode: 502, messageCode: "connectors.snowflake.returnedInvalidStatementResponse" }); };
113
113
  if (!object(result)) reject();
114
114
  const requestedHandle = url.pathname.split("/")[4];
115
115
  const handle = result.statementHandle ?? requestedHandle;
@@ -131,7 +131,7 @@ const snowflakeProvider = Object.freeze({
131
131
  ...(url.searchParams.has("partition") ? { partition: Number(url.searchParams.get("partition")) } : {}) };
132
132
  }
133
133
  if (response.status !== 200 || !Array.isArray(result) || result.length > Number(url.searchParams.get("showLimit"))) {
134
- throw new ConnectorError("connector_response_invalid", "Snowflake returned an unexpected database page.", { statusCode: 502 });
134
+ throw new ConnectorError("connector_response_invalid", "Snowflake returned an unexpected database page.", { statusCode: 502, messageCode: "connectors.snowflake.returnedUnexpectedDatabasePage" });
135
135
  }
136
136
  return result;
137
137
  }
@@ -49,7 +49,7 @@ const tallyProvider = Object.freeze({
49
49
  scopes: [],
50
50
  request(input) {
51
51
  const { formId, ...body } = validateSchemaPayload({ schema: formUpdateSchema, mode: "replace" }, input, { statusCode: 422 });
52
- if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Choose a name, status or block change.", { statusCode: 422 });
52
+ if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Choose a name, status or block change.", { statusCode: 422, messageCode: "connectors.tally.chooseNameStatusBlockChange" });
53
53
  return { method: "PATCH", url: `https://api.tally.so/forms/${formId}`, body };
54
54
  },
55
55
  validateResult: formResult
@@ -19,7 +19,7 @@ const telegramProvider = Object.freeze({
19
19
  apiOrigins: ["https://api.telegram.org"],
20
20
  apiKey: { pathPrefix(key) {
21
21
  if (!/^\d+:[A-Za-z0-9_-]+$/.test(key)) {
22
- throw new ConnectorError("connector_binding_invalid", "Configure a Telegram bot token in the referenced environment variable.");
22
+ throw new ConnectorError("connector_binding_invalid", "Configure a Telegram bot token in the referenced environment variable.", { messageCode: "connectors.telegram.configureBotTokenReferencedEnvironment" });
23
23
  }
24
24
  return `/bot${key}`;
25
25
  } },
@@ -8,13 +8,13 @@ const count = (value) => Number.isSafeInteger(value) && value >= 0;
8
8
  function checkReply(value) {
9
9
  const code = value?.error?.code ?? value?.code;
10
10
  if (code === "ok") return value;
11
- if (typeof code !== "string" || !code) throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid response.", { statusCode: 502 });
12
- if (code === "access_token_invalid") throw new ConnectorError("connector_reconnect_required", "Connect this TikTok account again.", { statusCode: 401 });
11
+ if (typeof code !== "string" || !code) throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid response.", { statusCode: 502, messageCode: "connectors.shared.invalidResponse", messageParams: { provider: "TikTok" } });
12
+ if (code === "access_token_invalid") throw new ConnectorError("connector_reconnect_required", "Connect this TikTok account again.", { statusCode: 401, messageCode: "connectors.shared.reconnectAccount", messageParams: { provider: "TikTok" } });
13
13
  if (["scope_not_authorized", "scope_permission_missed"].includes(code)) {
14
- throw new ConnectorError("connector_scope_missing", "Connect TikTok with the permission required for this operation.", { statusCode: 403 });
14
+ throw new ConnectorError("connector_scope_missing", "Connect TikTok with the permission required for this operation.", { statusCode: 403, messageCode: "connectors.tiktok.connectPermissionRequiredOperation" });
15
15
  }
16
- if (code === "rate_limit_exceeded") throw new ConnectorError("connector_rate_limited", "TikTok's request limit was reached.", { statusCode: 429 });
17
- throw new ConnectorError("connector_provider_failed", "TikTok could not complete the request.", { statusCode: 502 });
16
+ if (code === "rate_limit_exceeded") throw new ConnectorError("connector_rate_limited", "TikTok's request limit was reached.", { statusCode: 429, messageCode: "connectors.shared.requestLimitReached", messageParams: { provider: "TikTok" } });
17
+ throw new ConnectorError("connector_provider_failed", "TikTok could not complete the request.", { statusCode: 502, messageCode: "connectors.tiktok.couldNotCompleteRequest" });
18
18
  }
19
19
 
20
20
  function profileOperation(fields, scope, validateUser) {
@@ -31,21 +31,21 @@ const tiktokProvider = Object.freeze({
31
31
  let value;
32
32
  try { value = await response.clone().json(); } catch {
33
33
  if (!response.ok) return response;
34
- throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid token response.", { statusCode: 502 });
34
+ throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid token response.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenResponse", messageParams: { provider: "TikTok" } });
35
35
  }
36
36
  if (!response.ok) return response;
37
37
  if (value?.error) {
38
- if (value.error === "invalid_grant") throw new ConnectorError("connector_reconnect_required", "Connect this TikTok account again.", { statusCode: 401 });
39
- throw new ConnectorError("connector_provider_failed", "TikTok could not authorize this connection.", { statusCode: 502 });
38
+ if (value.error === "invalid_grant") throw new ConnectorError("connector_reconnect_required", "Connect this TikTok account again.", { statusCode: 401, messageCode: "connectors.shared.reconnectAccount", messageParams: { provider: "TikTok" } });
39
+ throw new ConnectorError("connector_provider_failed", "TikTok could not authorize this connection.", { statusCode: 502, messageCode: "connectors.tiktok.couldNotAuthorizeConnection" });
40
40
  }
41
41
  if (!textId(value?.open_id) || !Number.isSafeInteger(value.expires_in) || value.expires_in <= 0 ||
42
42
  !Number.isSafeInteger(value.refresh_expires_in) || value.refresh_expires_in <= 0 || !textId(value.refresh_token) ||
43
43
  typeof value.scope !== "string" || !/^[^\s,]+(?:,[^\s,]+)*$/u.test(value.scope)) {
44
- throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid permission grant.", { statusCode: 502 });
44
+ throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid permission grant.", { statusCode: 502, messageCode: "connectors.shared.invalidPermissionGrant", messageParams: { provider: "TikTok" } });
45
45
  }
46
46
  const scopes = value.scope.split(",");
47
- if (new Set(scopes).size !== scopes.length) throw new ConnectorError("connector_response_invalid", "TikTok returned duplicate permissions.", { statusCode: 502 });
48
- if (!scopes.includes("user.info.basic")) throw new ConnectorError("connector_scope_missing", "Connect TikTok with basic profile permission.", { statusCode: 403 });
47
+ if (new Set(scopes).size !== scopes.length) throw new ConnectorError("connector_response_invalid", "TikTok returned duplicate permissions.", { statusCode: 502, messageCode: "connectors.tiktok.returnedDuplicatePermissions" });
48
+ if (!scopes.includes("user.info.basic")) throw new ConnectorError("connector_scope_missing", "Connect TikTok with basic profile permission.", { statusCode: 403, messageCode: "connectors.tiktok.connectBasicProfilePermission" });
49
49
  return response;
50
50
  },
51
51
  apiOrigins: ["https://open.tiktokapis.com"], checkOperation: "profile.read",
@@ -72,12 +72,12 @@ const tiktokProvider = Object.freeze({
72
72
  ...(options.body ? { body: JSON.stringify(options.body) } : {}) });
73
73
  let value;
74
74
  try { value = await response.json(); } catch {
75
- throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid response.", { statusCode: 502 });
75
+ throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid response.", { statusCode: 502, messageCode: "connectors.shared.invalidResponse", messageParams: { provider: "TikTok" } });
76
76
  }
77
77
  checkReply(value);
78
- if (!response.ok) throw new ConnectorError("connector_provider_failed", "TikTok could not complete the request.", { statusCode: 502 });
78
+ if (!response.ok) throw new ConnectorError("connector_provider_failed", "TikTok could not complete the request.", { statusCode: 502, messageCode: "connectors.tiktok.couldNotCompleteRequest" });
79
79
  if (options.body?.max_count !== undefined && value.data?.videos?.length > options.body.max_count) {
80
- throw new ConnectorError("connector_response_invalid", "TikTok returned too many videos.", { statusCode: 502 });
80
+ throw new ConnectorError("connector_response_invalid", "TikTok returned too many videos.", { statusCode: 502, messageCode: "connectors.tiktok.returnedTooManyVideos" });
81
81
  }
82
82
  return value;
83
83
  }
@@ -39,7 +39,7 @@ const twilioProvider = Object.freeze({
39
39
  To: phone, From: { ...phone, required: false }, MessagingServiceSid: { ...sid("MG"), required: false },
40
40
  Body: { type: "string", required: true, noTrim: true, minLength: 1, maxLength: 1600 }, StatusCallback: callbackUrl
41
41
  }, values => {
42
- if (Boolean(values.From) === Boolean(values.MessagingServiceSid)) throw new ConnectorError("connector_input_invalid", "Choose one sender number or Messaging Service SID.", { statusCode: 422 });
42
+ if (Boolean(values.From) === Boolean(values.MessagingServiceSid)) throw new ConnectorError("connector_input_invalid", "Choose one sender number or Messaging Service SID.", { statusCode: 422, messageCode: "connectors.twilio.chooseSenderNumberMessagingService" });
43
43
  }),
44
44
  "messages.get": resourceOperation("Messages", "GET", { resourceSid: sid("SM") }),
45
45
  "calls.create": resourceOperation("Calls", "POST", {
@@ -47,7 +47,7 @@ const twilioProvider = Object.freeze({
47
47
  Twiml: { type: "string", noTrim: true, minLength: 1, maxLength: 4000 },
48
48
  StatusCallback: callbackUrl, Timeout: { type: "integer", min: 5, max: 600, defaultTo: 60 }
49
49
  }, values => {
50
- if (Boolean(values.Url) === Boolean(values.Twiml)) throw new ConnectorError("connector_input_invalid", "Supply either a call-control URL or inline TwiML.", { statusCode: 422 });
50
+ if (Boolean(values.Url) === Boolean(values.Twiml)) throw new ConnectorError("connector_input_invalid", "Supply either a call-control URL or inline TwiML.", { statusCode: 422, messageCode: "connectors.twilio.supplyEitherCallControlUrl" });
51
51
  }),
52
52
  "calls.get": resourceOperation("Calls", "GET", { resourceSid: sid("CA") }),
53
53
  "calls.list": jsonOperation((settings) => `${origins[settings.region]}/2010-04-01/Accounts/${settings.accountSid}/Calls.json`, {
@@ -65,7 +65,7 @@ const twilioProvider = Object.freeze({
65
65
  // POST form callbacks only. The caller supplies the exact public URL configured
66
66
  // at Twilio, never a URL reconstructed from untrusted forwarded headers.
67
67
  function verifyTwilioRequest({ rawBody, signature, authToken, url, accountSid, contentType }) {
68
- const reject = () => { throw new ConnectorError("connector_webhook_invalid", "Invalid Twilio callback.", { statusCode: 401 }); };
68
+ const reject = () => { throw new ConnectorError("connector_webhook_invalid", "Invalid Twilio callback.", { statusCode: 401, messageCode: "connectors.shared.invalidCallback", messageParams: { provider: "Twilio" } }); };
69
69
  if (!(rawBody instanceof Uint8Array) || rawBody.byteLength > 1048576 ||
70
70
  typeof signature !== "string" || !/^[A-Za-z0-9+/]{27}=$/u.test(signature) ||
71
71
  typeof authToken !== "string" || !authToken ||