@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.
- package/CHANGELOG.md +1722 -0
- package/README.md +11 -0
- package/locales/en.json +3139 -0
- package/package.json +7 -6
- package/src/server/ai.js +4 -4
- package/src/server/amazon-redshift.js +5 -5
- package/src/server/apify.js +1 -1
- package/src/server/apollo-io.js +1 -1
- package/src/server/asana.js +3 -3
- package/src/server/ashby.js +3 -3
- package/src/server/attention.js +2 -2
- package/src/server/aws-athena.js +3 -3
- package/src/server/aws-s3.js +1 -1
- package/src/server/awsTransport.js +9 -9
- package/src/server/brevo.js +3 -3
- package/src/server/calendly.js +2 -2
- package/src/server/chargebee.js +1 -1
- package/src/server/databricks.js +2 -2
- package/src/server/dbt-semantic-layer.js +8 -8
- package/src/server/elevenlabs.js +4 -4
- package/src/server/firebase-cloud-messaging.js +8 -8
- package/src/server/firecrawl.js +2 -2
- package/src/server/gatewayapi.js +4 -4
- package/src/server/github-api.js +3 -3
- package/src/server/gitlab-api.js +1 -1
- package/src/server/gmail.js +1 -1
- package/src/server/gong.js +4 -4
- package/src/server/google-ads-search.js +16 -16
- package/src/server/google-ads.js +7 -7
- package/src/server/google-drive.js +8 -6
- package/src/server/google-maps-platform.js +5 -5
- package/src/server/google-search-console.js +1 -1
- package/src/server/google-sheets.js +3 -2
- package/src/server/google.js +3 -2
- package/src/server/granola.js +5 -5
- package/src/server/hubspot.js +5 -3
- package/src/server/inngest.js +4 -4
- package/src/server/klipy.js +1 -1
- package/src/server/lightspeed.js +4 -4
- package/src/server/linear.js +2 -2
- package/src/server/linkedin.js +6 -6
- package/src/server/mapbox.js +1 -1
- package/src/server/mcpRegistration.js +3 -2
- package/src/server/mcpTransport.js +1 -1
- package/src/server/microsoft-fabric.js +3 -3
- package/src/server/microsoft-onedrive.js +1 -1
- package/src/server/microsoft-onenote.js +1 -1
- package/src/server/microsoft-outlook.js +1 -1
- package/src/server/microsoft.js +4 -3
- package/src/server/n8n.js +4 -4
- package/src/server/oura.js +1 -1
- package/src/server/paddle.js +1 -1
- package/src/server/pipedrive.js +4 -4
- package/src/server/posthog.js +3 -3
- package/src/server/salesforce.js +8 -8
- package/src/server/semrush.js +25 -25
- package/src/server/shopify.js +6 -6
- package/src/server/slack.js +10 -10
- package/src/server/snowflake.js +5 -5
- package/src/server/tally.js +1 -1
- package/src/server/telegram.js +1 -1
- package/src/server/tiktok.js +14 -14
- package/src/server/twilio.js +3 -3
- package/src/server/twitch.js +22 -21
- package/src/server/wave.js +34 -34
- package/src/server/wix.js +73 -73
- package/src/server/wiz.js +18 -18
- package/src/server/woocommerce.js +23 -23
- package/src/server/wordpress-com.js +3 -3
- package/src/server/wordpress-self-hosted.js +2 -2
- package/src/server/workday.js +3 -3
- package/src/server/x-twitter.js +6 -6
- package/src/server/xero.js +15 -15
- package/src/server/zoho-books.js +8 -8
- package/src/server/zoho-crm.js +5 -5
- package/src/server/zohoOAuth.js +1 -1
- package/src/shared/ai.js +24 -13
- package/src/shared/algolia.js +4 -4
- package/src/shared/amazon-redshift.js +16 -16
- package/src/shared/amplitude.js +6 -6
- package/src/shared/atlassian.js +19 -19
- package/src/shared/aws.js +18 -18
- package/src/shared/canva.js +18 -18
- package/src/shared/chargebee.js +3 -3
- package/src/shared/clickhouse.js +9 -9
- package/src/shared/confidence.js +6 -6
- package/src/shared/databricks.js +8 -8
- package/src/shared/dbt-semantic-layer.js +7 -7
- package/src/shared/english-ai.js +6 -0
- package/src/shared/english-definitions.js +34 -0
- package/src/shared/english-texts.js +7 -0
- package/src/shared/figma.js +2 -2
- package/src/shared/firebase-cloud-messaging.js +11 -11
- package/src/shared/firecrawl.js +1 -1
- package/src/shared/gemini-enterprise.js +7 -7
- package/src/shared/gong.js +5 -5
- package/src/shared/google-ads.js +5 -5
- package/src/shared/google-maps-platform.js +7 -7
- package/src/shared/google.js +40 -40
- package/src/shared/granola.js +5 -5
- package/src/shared/hex.js +7 -7
- package/src/shared/inngest.js +8 -8
- package/src/shared/lightspeed.js +52 -52
- package/src/shared/linkedin.js +6 -6
- package/src/shared/logo-dev.js +3 -3
- package/src/shared/mapbox.js +6 -6
- package/src/shared/mcp.js +9 -9
- package/src/shared/microsoft-fabric.js +9 -9
- package/src/shared/microsoft.js +52 -52
- package/src/shared/miro.js +5 -5
- package/src/shared/oura.js +5 -5
- package/src/shared/posthog.js +9 -9
- package/src/shared/prestashop.js +5 -5
- package/src/shared/resend.js +1 -1
- package/src/shared/salesforce.js +8 -8
- package/src/shared/semrush.js +6 -6
- package/src/shared/shopify.js +26 -26
- package/src/shared/slack.js +61 -61
- package/src/shared/snowflake.js +15 -15
- package/src/shared/tiktok.js +7 -7
- package/src/shared/tokens.js +126 -126
- package/src/shared/twilio.js +7 -7
- package/src/shared/twitch.js +24 -24
- package/src/shared/wave.js +33 -33
- package/src/shared/wix.js +5 -5
- package/src/shared/wiz.js +14 -14
- package/src/shared/wordpress-com.js +9 -9
- package/src/shared/wordpress.js +11 -11
- package/src/shared/workday.js +8 -8
- package/src/shared/x-twitter.js +5 -5
- package/src/shared/xero.js +25 -25
- package/src/shared/zoho-books.js +10 -10
- package/src/shared/zoho-crm.js +16 -16
- package/src/shared/zoho-regions.js +8 -8
- package/test/englishDefinitions.test.js +37 -0
- package/test/i18nCatalog.test.js +16 -0
- package/test/shopify.test.js +3 -1
package/src/server/gong.js
CHANGED
|
@@ -14,9 +14,9 @@ const recordsValid = value => ["totalRecords", "currentPageSize", "currentPageNu
|
|
|
14
14
|
function callOperation(path, field, extensive = false) {
|
|
15
15
|
const schema = createSchema({ ...callFields, ...(extensive ? { includeMedia: { type: "boolean", defaultTo: false } } : {}) });
|
|
16
16
|
return { scopes: [], request(input, settings) {
|
|
17
|
-
if (Array.isArray(input.callIds) && input.callIds.some(id => typeof id !== "string") || input.workspaceId !== undefined && typeof input.workspaceId !== "string") throw new ConnectorError("connector_input_invalid", "Supply Gong IDs as strings to preserve all digits.", { statusCode: 422 });
|
|
17
|
+
if (Array.isArray(input.callIds) && input.callIds.some(id => typeof id !== "string") || input.workspaceId !== undefined && typeof input.workspaceId !== "string") throw new ConnectorError("connector_input_invalid", "Supply Gong IDs as strings to preserve all digits.", { statusCode: 422, messageCode: "connectors.gong.supplyIdsStringsPreserveAll" });
|
|
18
18
|
const { cursor, includeMedia, ...filter } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
19
|
-
if ((!filter.fromDateTime !== !filter.toDateTime) || (filter.fromDateTime && Date.parse(filter.fromDateTime) >= Date.parse(filter.toDateTime)) || (!filter.callIds && !filter.fromDateTime)) throw new ConnectorError("connector_input_invalid", "Supply call IDs or an ordered timestamp range.", { statusCode: 422 });
|
|
19
|
+
if ((!filter.fromDateTime !== !filter.toDateTime) || (filter.fromDateTime && Date.parse(filter.fromDateTime) >= Date.parse(filter.toDateTime)) || (!filter.callIds && !filter.fromDateTime)) throw new ConnectorError("connector_input_invalid", "Supply call IDs or an ordered timestamp range.", { statusCode: 422, messageCode: "connectors.gong.supplyCallIdsOrderedTimestamp" });
|
|
20
20
|
const body = { filter, ...(cursor ? { cursor } : {}) };
|
|
21
21
|
if (extensive) body.contentSelector = { context: "Extended", contextTiming: ["Now", "TimeOfCall"], exposedFields: {
|
|
22
22
|
parties: true, content: { brief: true, outline: true, topics: true, keyPoints: true, callOutcome: true },
|
|
@@ -36,9 +36,9 @@ const gongProvider = Object.freeze({
|
|
|
36
36
|
"calls.extensive": callOperation("/v2/calls/extensive", "calls", true),
|
|
37
37
|
"calls.transcripts": callOperation("/v2/calls/transcript", "callTranscripts"),
|
|
38
38
|
"stats.interaction": { scopes: [], request(input, settings) {
|
|
39
|
-
if (Array.isArray(input.userIds) && input.userIds.some(id => typeof id !== "string")) throw new ConnectorError("connector_input_invalid", "Supply Gong IDs as strings to preserve all digits.", { statusCode: 422 });
|
|
39
|
+
if (Array.isArray(input.userIds) && input.userIds.some(id => typeof id !== "string")) throw new ConnectorError("connector_input_invalid", "Supply Gong IDs as strings to preserve all digits.", { statusCode: 422, messageCode: "connectors.gong.supplyIdsStringsPreserveAll" });
|
|
40
40
|
const { cursor, ...filter } = validateSchemaPayload({ schema: statsSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
41
|
-
if (filter.fromDate >= filter.toDate) throw new ConnectorError("connector_input_invalid", "The end date must follow the start date.", { statusCode: 422 });
|
|
41
|
+
if (filter.fromDate >= filter.toDate) throw new ConnectorError("connector_input_invalid", "The end date must follow the start date.", { statusCode: 422, messageCode: "connectors.gong.endDateMustFollowStart" });
|
|
42
42
|
return { method: "POST", url: `${origin(settings)}/v2/stats/interaction`, body: { filter, ...(cursor ? { cursor } : {}) } };
|
|
43
43
|
}, validateResult: value => Array.isArray(value?.peopleInteractionStats) && recordsValid(value) },
|
|
44
44
|
"users.list": jsonOperation((settings) => `${origin(settings)}/v2/users`, {
|
|
@@ -9,7 +9,7 @@ const api = "https://googleads.googleapis.com/v25";
|
|
|
9
9
|
const scope = "https://www.googleapis.com/auth/adwords";
|
|
10
10
|
const customerId = { type: "string", required: true, validator: value => /^[0-9]{10}$/.test(value) || "Use a ten-digit customer ID." };
|
|
11
11
|
const id = { type: "string", required: true, validator: value => /^[0-9]{1,20}$/.test(value) || "Use a numeric campaign ID." };
|
|
12
|
-
const error = message => { throw new ConnectorError("connector_input_invalid", message, { statusCode: 422 }); };
|
|
12
|
+
const error = (message, codes) => { throw new ConnectorError("connector_input_invalid", message, { statusCode: 422, ...codes }); };
|
|
13
13
|
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14
14
|
function requestOperation(fields, build, validateResult) {
|
|
15
15
|
return validatedOperation(fields, (values, settings) => (
|
|
@@ -50,7 +50,7 @@ const googleAdsSearchOperations = {
|
|
|
50
50
|
// Application composition: this service has no editor protocol, command runner or storage.
|
|
51
51
|
function createGoogleAdsSearchService({ connections, configuration, context, integrationId }) {
|
|
52
52
|
const slot = configuration.integrations?.[integrationId];
|
|
53
|
-
if (slot?.provider !== "google-ads") error("Choose a Google Ads integration.");
|
|
53
|
+
if (slot?.provider !== "google-ads") error("Choose a Google Ads integration.", { messageCode: "connectors.googleAdsSearch.chooseIntegration" });
|
|
54
54
|
const invoke = (operation, input) => connections.invoke({ context, integrationId, operation, input });
|
|
55
55
|
const query = async (customer, query) => (await invoke("reports.search", { customerId: customer, query })).results || [];
|
|
56
56
|
const customer = value => validateSchemaPayload({ schema: createSchema({ customerId }), mode: "replace" }, { customerId: value }, { statusCode: 422 }).customerId;
|
|
@@ -59,14 +59,14 @@ function createGoogleAdsSearchService({ connections, configuration, context, int
|
|
|
59
59
|
const review = data => hash([context.applicationId, context.subjectId, integrationId, slot, plan(), data]);
|
|
60
60
|
async function account(id) {
|
|
61
61
|
const rows = await query(customer(id), "SELECT customer.id, customer.descriptive_name, customer.currency_code, customer.time_zone, customer.status, customer.manager FROM customer LIMIT 1");
|
|
62
|
-
if (rows.length !== 1) error("Google did not return the selected account.");
|
|
62
|
+
if (rows.length !== 1) error("Google did not return the selected account.", { messageCode: "connectors.googleAdsSearch.didNotReturnSelectedAccount" });
|
|
63
63
|
return rows[0].customer;
|
|
64
64
|
}
|
|
65
65
|
async function preview() {
|
|
66
66
|
const p = plan(), selected = await account(p.customerId);
|
|
67
|
-
if (selected.manager || selected.status !== "ENABLED" || selected.currencyCode !== p.currency) error("Choose an active client account and match its currency.");
|
|
67
|
+
if (selected.manager || selected.status !== "ENABLED" || selected.currencyCode !== p.currency) error("Choose an active client account and match its currency.", { messageCode: "connectors.googleAdsSearch.chooseActiveClientAccountMatch" });
|
|
68
68
|
const goals = await query(p.customerId, `SELECT conversion_action.id, conversion_action.name, conversion_action.status, conversion_action.type FROM conversion_action WHERE conversion_action.id = ${p.conversionActionId} LIMIT 1`);
|
|
69
|
-
if (goals[0]?.conversionAction?.status !== "ENABLED" || goals[0]?.conversionAction?.type !== "WEBPAGE") error("Choose an enabled website conversion action.");
|
|
69
|
+
if (goals[0]?.conversionAction?.status !== "ENABLED" || goals[0]?.conversionAction?.type !== "WEBPAGE") error("Choose an enabled website conversion action.", { messageCode: "connectors.googleAdsSearch.chooseEnabledWebsiteConversionAction" });
|
|
70
70
|
await invoke("search.validate", p);
|
|
71
71
|
const data = { plan: p, account: selected, conversion: goals[0].conversionAction };
|
|
72
72
|
return { ...data, reviewId: review(data) };
|
|
@@ -74,7 +74,7 @@ function createGoogleAdsSearchService({ connections, configuration, context, int
|
|
|
74
74
|
async function campaign(id) {
|
|
75
75
|
const p = plan(), selected = campaignId(id);
|
|
76
76
|
const rows = await query(p.customerId, `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.bidding_strategy_type, campaign.network_settings.target_google_search, campaign.network_settings.target_search_network, campaign.network_settings.target_content_network, campaign.network_settings.target_partner_search_network, campaign.geo_target_type_setting.positive_geo_target_type, campaign.geo_target_type_setting.negative_geo_target_type, campaign.contains_eu_political_advertising, campaign_budget.amount_micros, campaign_budget.id FROM campaign WHERE campaign.id = ${selected} LIMIT 1`);
|
|
77
|
-
if (rows.length !== 1 || rows[0].campaign?.advertisingChannelType !== "SEARCH") error("Select an accessible Search campaign.");
|
|
77
|
+
if (rows.length !== 1 || rows[0].campaign?.advertisingChannelType !== "SEARCH") error("Select an accessible Search campaign.", { messageCode: "connectors.googleAdsSearch.selectAccessibleSearchCampaign" });
|
|
78
78
|
const details = {
|
|
79
79
|
customerId: p.customerId, account: await account(p.customerId), ...rows[0],
|
|
80
80
|
ads: await query(p.customerId, `SELECT ad_group_ad.ad.id, ad_group_ad.ad.final_urls, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.status, ad_group_ad.policy_summary.approval_status, ad_group.id, ad_group.status, ad_group.cpc_bid_micros FROM ad_group_ad WHERE campaign.id = ${selected} LIMIT 21`),
|
|
@@ -84,10 +84,10 @@ function createGoogleAdsSearchService({ connections, configuration, context, int
|
|
|
84
84
|
};
|
|
85
85
|
const goalResource = details.goals[0]?.conversionGoalCampaignConfig?.customConversionGoal;
|
|
86
86
|
if (goalResource) {
|
|
87
|
-
if (!new RegExp(`^customers/${p.customerId}/customConversionGoals/[0-9]+$`).test(goalResource)) error("Google returned an invalid campaign goal.");
|
|
87
|
+
if (!new RegExp(`^customers/${p.customerId}/customConversionGoals/[0-9]+$`).test(goalResource)) error("Google returned an invalid campaign goal.", { messageCode: "connectors.googleAdsSearch.returnedInvalidCampaignGoal" });
|
|
88
88
|
details.customGoals = await query(p.customerId, `SELECT custom_conversion_goal.name, custom_conversion_goal.status, custom_conversion_goal.conversion_actions FROM custom_conversion_goal WHERE custom_conversion_goal.resource_name = '${goalResource}' LIMIT 1`);
|
|
89
89
|
}
|
|
90
|
-
if (details.ads.length > 20 || details.targets.length > 30 || details.keywords.length > 30) error("This campaign exceeds the bounded Search review. Manage it in Google Ads.");
|
|
90
|
+
if (details.ads.length > 20 || details.targets.length > 30 || details.keywords.length > 30) error("This campaign exceeds the bounded Search review. Manage it in Google Ads.", { messageCode: "connectors.googleAdsSearch.campaignExceedsBoundedSearchReview" });
|
|
91
91
|
return { ...details, reviewId: review(details) };
|
|
92
92
|
}
|
|
93
93
|
return {
|
|
@@ -101,14 +101,14 @@ function createGoogleAdsSearchService({ connections, configuration, context, int
|
|
|
101
101
|
},
|
|
102
102
|
async targets(id, name) {
|
|
103
103
|
customer(id);
|
|
104
|
-
if (typeof name !== "string" || !/^[\p{L}\p{N} .-]{2,80}$/u.test(name)) error("Enter a location name using letters, numbers, spaces, dots or hyphens.");
|
|
104
|
+
if (typeof name !== "string" || !/^[\p{L}\p{N} .-]{2,80}$/u.test(name)) error("Enter a location name using letters, numbers, spaces, dots or hyphens.", { messageCode: "connectors.googleAdsSearch.enterLocationNameUsingLetters" });
|
|
105
105
|
return { locations: await query(id, `SELECT geo_target_constant.id, geo_target_constant.canonical_name, geo_target_constant.status FROM geo_target_constant WHERE geo_target_constant.name LIKE '%${name}%' AND geo_target_constant.status = 'ENABLED' LIMIT 20`),
|
|
106
106
|
languages: await query(id, "SELECT language_constant.id, language_constant.name FROM language_constant WHERE language_constant.targetable = TRUE LIMIT 100") };
|
|
107
107
|
},
|
|
108
108
|
preview,
|
|
109
109
|
async create(reviewId) {
|
|
110
110
|
const checked = await preview();
|
|
111
|
-
if (reviewId !== checked.reviewId) error("The Search plan changed. Review it again before creation.");
|
|
111
|
+
if (reviewId !== checked.reviewId) error("The Search plan changed. Review it again before creation.", { messageCode: "connectors.googleAdsSearch.searchPlanChangedReviewAgain" });
|
|
112
112
|
const response = await invoke("search.createPaused", checked.plan);
|
|
113
113
|
const resource = response.mutateOperationResponses.find(item => item.campaignResult)?.campaignResult.resourceName;
|
|
114
114
|
return { campaignId: resource.split("/").at(-1), status: "PAUSED" };
|
|
@@ -117,13 +117,13 @@ function createGoogleAdsSearchService({ connections, configuration, context, int
|
|
|
117
117
|
campaign,
|
|
118
118
|
async launch({ campaignId, reviewId, trackingConfirmed, billingConfirmed }) {
|
|
119
119
|
const current = await campaign(campaignId);
|
|
120
|
-
if (current.reviewId !== reviewId) error("The campaign changed. Inspect and approve it again.");
|
|
121
|
-
if (trackingConfirmed !== true || billingConfirmed !== true) error("Confirm installed conversion tracking and billing/advertiser readiness before launch.");
|
|
122
|
-
if (current.account.manager || current.account.status !== "ENABLED") error("Choose an active client account.");
|
|
120
|
+
if (current.reviewId !== reviewId) error("The campaign changed. Inspect and approve it again.", { messageCode: "connectors.googleAdsSearch.campaignChangedInspectApproveAgain" });
|
|
121
|
+
if (trackingConfirmed !== true || billingConfirmed !== true) error("Confirm installed conversion tracking and billing/advertiser readiness before launch.", { messageCode: "connectors.googleAdsSearch.confirmInstalledConversionTrackingBilling" });
|
|
122
|
+
if (current.account.manager || current.account.status !== "ENABLED") error("Choose an active client account.", { messageCode: "connectors.googleAdsSearch.chooseActiveClientAccount" });
|
|
123
123
|
if (current.campaign.containsEuPoliticalAdvertising !== "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING" ||
|
|
124
|
-
!current.customGoals?.length || current.customGoals[0].customConversionGoal?.status !== "ENABLED") error("Review a non-political Search campaign with an enabled custom conversion goal.");
|
|
125
|
-
if (!current.ads.length || !current.keywords.length || current.campaign.biddingStrategyType !== "MANUAL_CPC") error("This screen launches only Search campaigns with manual CPC, ads and keywords. Manage other campaigns in Google Ads.");
|
|
126
|
-
if (current.campaign.status !== "PAUSED") error("Only a paused campaign can be launched here.");
|
|
124
|
+
!current.customGoals?.length || current.customGoals[0].customConversionGoal?.status !== "ENABLED") error("Review a non-political Search campaign with an enabled custom conversion goal.", { messageCode: "connectors.googleAdsSearch.reviewNonPoliticalSearchCampaign" });
|
|
125
|
+
if (!current.ads.length || !current.keywords.length || current.campaign.biddingStrategyType !== "MANUAL_CPC") error("This screen launches only Search campaigns with manual CPC, ads and keywords. Manage other campaigns in Google Ads.", { messageCode: "connectors.googleAdsSearch.screenLaunchesSearchCampaignsManual" });
|
|
126
|
+
if (current.campaign.status !== "PAUSED") error("Only a paused campaign can be launched here.", { messageCode: "connectors.googleAdsSearch.pausedCampaignCanLaunchedHere" });
|
|
127
127
|
return invoke("campaigns.setStatus", { customerId: current.customerId, campaignId, status: "ENABLED" });
|
|
128
128
|
},
|
|
129
129
|
pause: id => invoke("campaigns.setStatus", { customerId: plan().customerId, campaignId: campaignId(id), status: "PAUSED" }),
|
package/src/server/google-ads.js
CHANGED
|
@@ -57,22 +57,22 @@ const googleAdsProvider = Object.freeze({
|
|
|
57
57
|
const errors = details.flatMap((detail) => Array.isArray(detail?.errors) ? detail.errors : []);
|
|
58
58
|
const codes = errors.flatMap((error) => record(error?.errorCode) ? Object.values(error.errorCode) : []);
|
|
59
59
|
if (response.status === 429 || value?.error?.status === "RESOURCE_EXHAUSTED" || errors.some((error) => error?.errorCode?.quotaError)) {
|
|
60
|
-
throw new ConnectorError("connector_rate_limited", "Google Ads API capacity is exhausted. Check the Cloud project and account quotas.", { statusCode: 429 });
|
|
60
|
+
throw new ConnectorError("connector_rate_limited", "Google Ads API capacity is exhausted. Check the Cloud project and account quotas.", { statusCode: 429, messageCode: "connectors.googleAds.apiCapacityExhaustedCheckCloud" });
|
|
61
61
|
}
|
|
62
62
|
if (details.some((detail) => detail?.reason === "ACCESS_TOKEN_SCOPE_INSUFFICIENT")) {
|
|
63
|
-
throw new ConnectorError("connector_scope_missing", "Connect Google Ads again with its required permission.", { statusCode: 403 });
|
|
63
|
+
throw new ConnectorError("connector_scope_missing", "Connect Google Ads again with its required permission.", { statusCode: 403, messageCode: "connectors.googleAds.connectAgainRequiredPermission" });
|
|
64
64
|
}
|
|
65
65
|
if (codes.some((code) => ["CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION"].includes(code))) {
|
|
66
|
-
throw new ConnectorError("connector_api_access_invalid", "Check Google Ads API access approval for the Cloud project that owns this OAuth client.", { statusCode: 403 });
|
|
66
|
+
throw new ConnectorError("connector_api_access_invalid", "Check Google Ads API access approval for the Cloud project that owns this OAuth client.", { statusCode: 403, messageCode: "connectors.googleAds.checkApiAccessApprovalCloud" });
|
|
67
67
|
}
|
|
68
68
|
if (codes.some((code) => ["OAUTH_TOKEN_INVALID", "OAUTH_TOKEN_EXPIRED", "OAUTH_TOKEN_REVOKED"].includes(code))) {
|
|
69
|
-
throw new ConnectorError("connector_reconnect_required", "Connect this Google Ads account again.", { statusCode: 401 });
|
|
69
|
+
throw new ConnectorError("connector_reconnect_required", "Connect this Google Ads account again.", { statusCode: 401, messageCode: "connectors.shared.reconnectAccount", messageParams: { provider: "Google Ads" } });
|
|
70
70
|
}
|
|
71
|
-
if (response.status === 400) throw new ConnectorError("connector_input_invalid", "Google Ads rejected the operation. Check campaign fields, policy requirements and account permissions.", { statusCode: 422 });
|
|
72
|
-
if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Google Ads resource is unavailable.", { statusCode: 404 });
|
|
71
|
+
if (response.status === 400) throw new ConnectorError("connector_input_invalid", "Google Ads rejected the operation. Check campaign fields, policy requirements and account permissions.", { statusCode: 422, messageCode: "connectors.googleAds.rejectedOperationCheckCampaignFields" });
|
|
72
|
+
if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Google Ads resource is unavailable.", { statusCode: 404, messageCode: "connectors.googleAds.resourceUnavailable" });
|
|
73
73
|
throw Object.assign(new Error("Google Ads request failed."), { status: response.ok ? 502 : response.status });
|
|
74
74
|
}
|
|
75
|
-
if (response.status !== 200 || !record(value)) throw new ConnectorError("connector_response_invalid", "Google Ads returned an unexpected response.", { statusCode: 502 });
|
|
75
|
+
if (response.status !== 200 || !record(value)) throw new ConnectorError("connector_response_invalid", "Google Ads returned an unexpected response.", { statusCode: 502, messageCode: "connectors.shared.unexpectedResponse", messageParams: { provider: "Google Ads" } });
|
|
76
76
|
return value;
|
|
77
77
|
}
|
|
78
78
|
});
|
|
@@ -20,7 +20,7 @@ const baseProvider = googleProvider(googleDriveDefinition, origin, "files.list",
|
|
|
20
20
|
"files.export": googleRead(contentScopes, { fileId, mimeType: { type: "string", required: true, minLength: 1, maxLength: 200 } }, ({ fileId, mimeType }) => ({ url: url(fileId, { mimeType }, "/export") }), validBytes),
|
|
21
21
|
"files.create": googleOperation(writeScopes, { name: { type: "string", required: true, minLength: 1, maxLength: 1024 }, parentId: { ...fileId, required: false }, folder: { type: "boolean", defaultTo: false } }, ({ name, parentId, folder }) => ({ method: "POST", url: url(undefined, { supportsAllDrives: true, fields }), body: { name, ...(parentId ? { parents: [parentId] } : {}), ...(folder ? { mimeType: "application/vnd.google-apps.folder" } : {}) } }), validFile),
|
|
22
22
|
"files.update": googleOperation([...writeScopes, "drive.metadata"], { fileId, name: { type: "string", minLength: 1, maxLength: 1024 }, description: { type: "string", maxLength: 16384 }, trashed: { type: "boolean" } }, ({ fileId, ...body }) => {
|
|
23
|
-
if (!Object.keys(body).length) throw inputInvalid("Supply a name, description or trashed value.");
|
|
23
|
+
if (!Object.keys(body).length) throw inputInvalid("Supply a name, description or trashed value.", { messageCode: "connectors.googleDrive.metadataChangeRequired" });
|
|
24
24
|
return { method: "PATCH", url: url(fileId, { supportsAllDrives: true, fields }), body };
|
|
25
25
|
}, validFile),
|
|
26
26
|
"files.upload": googleOperation(writeScopes, {
|
|
@@ -28,11 +28,13 @@ const baseProvider = googleProvider(googleDriveDefinition, origin, "files.list",
|
|
|
28
28
|
parentId: { ...fileId, required: false }, contentType: { type: "string", required: true, minLength: 3, maxLength: 200 },
|
|
29
29
|
bodyBase64: { type: "string", required: true, maxLength: 6666668, noTrim: true }
|
|
30
30
|
}, ({ fileId, name, parentId, contentType, bodyBase64 }) => {
|
|
31
|
-
if (!/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(contentType)) throw inputInvalid("Use a MIME type without header parameters.");
|
|
31
|
+
if (!/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(contentType)) throw inputInvalid("Use a MIME type without header parameters.", { messageCode: "connectors.googleDrive.mimeTypeInvalid" });
|
|
32
32
|
const data = Buffer.from(bodyBase64, "base64");
|
|
33
|
-
if (data.toString("base64") !== bodyBase64) throw inputInvalid("Use standard padded base64 file data.");
|
|
34
|
-
if (data.byteLength > 5_000_000) throw inputInvalid("Uploads above 5 MB require the framework's native resumable client.",
|
|
35
|
-
|
|
33
|
+
if (data.toString("base64") !== bodyBase64) throw inputInvalid("Use standard padded base64 file data.", { messageCode: "connectors.googleDrive.base64Invalid" });
|
|
34
|
+
if (data.byteLength > 5_000_000) throw inputInvalid("Uploads above 5 MB require the framework's native resumable client.",
|
|
35
|
+
{ statusCode: 413, messageCode: "connectors.googleDrive.uploadTooLarge" });
|
|
36
|
+
if (fileId && parentId) throw inputInvalid("parentId applies to new files; moving files requires native addParents/removeParents wiring.",
|
|
37
|
+
{ messageCode: "connectors.googleDrive.parentIdNewFilesOnly" });
|
|
36
38
|
const boundary = `drive_${randomUUID()}`;
|
|
37
39
|
const metadata = { name, ...(parentId ? { parents: [parentId] } : {}) };
|
|
38
40
|
const body = Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${JSON.stringify(metadata)}\r\n--${boundary}\r\nContent-Type: ${contentType}\r\n\r\n`), data, Buffer.from(`\r\n--${boundary}--\r\n`)]);
|
|
@@ -60,7 +62,7 @@ const googleDriveProvider = Object.freeze({ ...baseProvider,
|
|
|
60
62
|
if (reader) try { while (true) {
|
|
61
63
|
const { done, value } = await reader.read(); if (done) break;
|
|
62
64
|
size += value.byteLength;
|
|
63
|
-
if (size > 8 * 1024 * 1024) { await reader.cancel(); throw new ConnectorError("connector_response_too_large", "Use the framework's native Drive client for downloads or exports above 8 MiB.", { statusCode: 413 }); }
|
|
65
|
+
if (size > 8 * 1024 * 1024) { await reader.cancel(); throw new ConnectorError("connector_response_too_large", "Use the framework's native Drive client for downloads or exports above 8 MiB.", { statusCode: 413, messageCode: "connectors.googleDrive.useFrameworkNativeDriveClient" }); }
|
|
64
66
|
chunks.push(value);
|
|
65
67
|
} } finally { reader.releaseLock(); }
|
|
66
68
|
const body = Buffer.concat(chunks);
|
|
@@ -16,15 +16,15 @@ const reverseSchema = createSchema({
|
|
|
16
16
|
function validateGeocoding(result) {
|
|
17
17
|
switch (result?.status) {
|
|
18
18
|
case "OVER_QUERY_LIMIT":
|
|
19
|
-
throw new ConnectorError("connector_rate_limited", "The Google Maps request quota was reached.", { statusCode: 429 });
|
|
19
|
+
throw new ConnectorError("connector_rate_limited", "The Google Maps request quota was reached.", { statusCode: 429, messageCode: "connectors.googleMapsPlatform.requestQuotaReached" });
|
|
20
20
|
case "OVER_DAILY_LIMIT":
|
|
21
|
-
throw new ConnectorError("connector_configuration_invalid", "Check the Google Maps server key, billing and usage cap.");
|
|
21
|
+
throw new ConnectorError("connector_configuration_invalid", "Check the Google Maps server key, billing and usage cap.", { messageCode: "connectors.googleMapsPlatform.checkServerKeyBillingUsage" });
|
|
22
22
|
case "REQUEST_DENIED":
|
|
23
|
-
throw new ConnectorError("connector_permission_denied", "Google Maps denied access. Check the enabled API and key restrictions.", { statusCode: 403 });
|
|
23
|
+
throw new ConnectorError("connector_permission_denied", "Google Maps denied access. Check the enabled API and key restrictions.", { statusCode: 403, messageCode: "connectors.googleMapsPlatform.deniedAccessCheckEnabledApi" });
|
|
24
24
|
case "INVALID_REQUEST":
|
|
25
|
-
throw new ConnectorError("connector_input_invalid", "Google Maps could not accept this geocoding request.", { statusCode: 422 });
|
|
25
|
+
throw new ConnectorError("connector_input_invalid", "Google Maps could not accept this geocoding request.", { statusCode: 422, messageCode: "connectors.googleMapsPlatform.couldNotAcceptGeocodingRequest" });
|
|
26
26
|
case "UNKNOWN_ERROR":
|
|
27
|
-
throw new ConnectorError("connector_provider_failed", "Google Maps could not process the request. Try again.", { statusCode: 502 });
|
|
27
|
+
throw new ConnectorError("connector_provider_failed", "Google Maps could not process the request. Try again.", { statusCode: 502, messageCode: "connectors.googleMapsPlatform.couldNotProcessRequestTry" });
|
|
28
28
|
case "ZERO_RESULTS":
|
|
29
29
|
return Array.isArray(result.results) && result.results.length === 0;
|
|
30
30
|
case "OK":
|
|
@@ -17,7 +17,7 @@ const googleSearchConsoleProvider = googleProvider(googleSearchConsoleDefinition
|
|
|
17
17
|
type: { type: "string", enum: ["web", "image", "video", "news", "discover", "googleNews"], defaultTo: "web" },
|
|
18
18
|
rowLimit: { type: "integer", min: 1, max: 25000, defaultTo: 1000 }, startRow: { type: "integer", min: 0, defaultTo: 0 }
|
|
19
19
|
}, ({ siteUrl, filters, ...body }) => {
|
|
20
|
-
if (body.startDate > body.endDate) throw inputInvalid("startDate must not follow endDate.");
|
|
20
|
+
if (body.startDate > body.endDate) throw inputInvalid("startDate must not follow endDate.", { messageCode: "connectors.googleSearchConsole.datesOrder" });
|
|
21
21
|
return { method: "POST", url: `${propertyPath(siteUrl)}/searchAnalytics/query`, body: { ...body, dataState: "final", ...(filters?.length ? { dimensionFilterGroups: [{ groupType: "and", filters }] } : {}) } };
|
|
22
22
|
}, value => plain(value) && (value.rows === undefined || Array.isArray(value.rows))),
|
|
23
23
|
"urlInspection.inspect": googleOperation(readScopes, { siteUrl, inspectionUrl: { ...feedpath }, languageCode: { type: "string", minLength: 2, maxLength: 35 } }, body => ({ method: "POST", url: `${origin}/v1/urlInspection/index:inspect`, body }), value => plain(value?.inspectionResult)),
|
|
@@ -13,7 +13,8 @@ const sheetUrl = id => `${origin}/v4/spreadsheets/${encodeURIComponent(id)}`;
|
|
|
13
13
|
const get = googleRead(scopes, { spreadsheetId: documentId }, ({ spreadsheetId }) => ({ url: sheetUrl(spreadsheetId), query: { includeGridData: false } }), validSheet);
|
|
14
14
|
function checkValues(rows) {
|
|
15
15
|
if (!rows.length || rows.length > 10000 || rows.some(row => !Array.isArray(row) || row.some(cell => cell !== null && typeof cell !== "string" && typeof cell !== "boolean" && !(typeof cell === "number" && Number.isFinite(cell)))) || rows.reduce((count, row) => count + row.length, 0) > 10000 || Buffer.byteLength(JSON.stringify(rows)) > 1048576) {
|
|
16
|
-
throw inputInvalid("Supply a matrix of at most 10,000 cells and 1 MiB, containing strings, finite numbers, booleans or null."
|
|
16
|
+
throw inputInvalid("Supply a matrix of at most 10,000 cells and 1 MiB, containing strings, finite numbers, booleans or null.",
|
|
17
|
+
{ messageCode: "connectors.googleSheets.valuesMatrixInvalid" });
|
|
17
18
|
}
|
|
18
19
|
return rows;
|
|
19
20
|
}
|
|
@@ -38,7 +39,7 @@ const base = googleProvider(googleSheetsDefinition, origin, "connection.verify",
|
|
|
38
39
|
"values.clear": googleOperation(writable, { spreadsheetId: documentId, range }, ({ spreadsheetId, range }) => ({ method: "POST", url: `${sheetUrl(spreadsheetId)}/values/${encodeURIComponent(range)}:clear`, body: {} }), value => typeof value?.spreadsheetId === "string" && typeof value.clearedRange === "string"),
|
|
39
40
|
"values.batchUpdate": googleOperation(writable, { spreadsheetId: documentId, data: { type: "array", required: true }, valueInputOption: inputOption }, ({ spreadsheetId, data, valueInputOption }) => {
|
|
40
41
|
const schema = createSchema({ range, values });
|
|
41
|
-
if (!data.length || data.length > 100 || Buffer.byteLength(JSON.stringify(data)) > 1048576) throw inputInvalid("Supply 1–100 ranges within 1 MiB.");
|
|
42
|
+
if (!data.length || data.length > 100 || Buffer.byteLength(JSON.stringify(data)) > 1048576) throw inputInvalid("Supply 1–100 ranges within 1 MiB.", { messageCode: "connectors.googleSheets.rangesInvalid" });
|
|
42
43
|
const ranges = data.map(value => {
|
|
43
44
|
const parsed = validateSchemaPayload({ schema, mode: "replace" }, value, { statusCode: 422 });
|
|
44
45
|
return { ...parsed, majorDimension: "ROWS", values: checkValues(parsed.values) };
|
package/src/server/google.js
CHANGED
|
@@ -13,14 +13,15 @@ const documentId = { type: "string", required: true, minLength: 1, maxLength: 10
|
|
|
13
13
|
const pageToken = { type: "string", minLength: 1, maxLength: 4096 };
|
|
14
14
|
|
|
15
15
|
// Request builders report rejected input as the caller's mistake, not a provider outage.
|
|
16
|
-
const inputInvalid = (message, statusCode = 422) => new ConnectorError("connector_input_invalid", message, { statusCode });
|
|
16
|
+
const inputInvalid = (message, { statusCode = 422, ...codes } = {}) => new ConnectorError("connector_input_invalid", message, { statusCode, ...codes });
|
|
17
17
|
const nativeRequest = request => request && typeof request === "object" && !Array.isArray(request) && Object.keys(request).length === 1 &&
|
|
18
18
|
/^[a-zA-Z][a-zA-Z0-9]+$/.test(Object.keys(request)[0]) && Boolean(Object.values(request)[0]) &&
|
|
19
19
|
typeof Object.values(request)[0] === "object" && !Array.isArray(Object.values(request)[0]);
|
|
20
20
|
// Docs, Slides and Sheets batchUpdate share one limit: 1-100 single-operation request objects within 1 MiB.
|
|
21
21
|
function googleBatchRequests(requests, product) {
|
|
22
22
|
if (!requests.length || requests.length > 100 || Buffer.byteLength(JSON.stringify(requests)) > 1048576 || !requests.every(nativeRequest)) {
|
|
23
|
-
throw inputInvalid(`Supply 1–100 native ${product} request objects, one operation per object, within 1 MiB
|
|
23
|
+
throw inputInvalid(`Supply 1–100 native ${product} request objects, one operation per object, within 1 MiB.`,
|
|
24
|
+
{ messageCode: "connectors.google.batchRequestsInvalid", messageParams: { product } });
|
|
24
25
|
}
|
|
25
26
|
return requests;
|
|
26
27
|
}
|
package/src/server/granola.js
CHANGED
|
@@ -42,7 +42,7 @@ const granolaApiProvider = Object.freeze({
|
|
|
42
42
|
...granolaDefinition, apiOrigins: [apiOrigin], checkOperation: "notes.list",
|
|
43
43
|
apiKey: { headers: (key) => {
|
|
44
44
|
if (!key.startsWith("grn_") || key.length <= 4 || /[\s\p{Cc}]/u.test(key)) {
|
|
45
|
-
throw new ConnectorError("connector_binding_missing", "Use the Granola grn_ API key without whitespace or control characters.");
|
|
45
|
+
throw new ConnectorError("connector_binding_missing", "Use the Granola grn_ API key without whitespace or control characters.", { messageCode: "connectors.granola.useGrnApiKeyWithout" });
|
|
46
46
|
}
|
|
47
47
|
return { Authorization: `Bearer ${key}` };
|
|
48
48
|
} },
|
|
@@ -66,18 +66,18 @@ const granolaApiProvider = Object.freeze({
|
|
|
66
66
|
},
|
|
67
67
|
async exchange(address, options, { fetchImpl }) {
|
|
68
68
|
const response = await fetchImpl(address, { ...options, credentials: "omit" });
|
|
69
|
-
if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "The Granola note is unavailable, inaccessible or still being processed.", { statusCode: 404 });
|
|
70
|
-
if (response.status === 413) throw new ConnectorError("connector_response_too_large", "Read the transcript in pages with transcripts.list.", { statusCode: 413 });
|
|
69
|
+
if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "The Granola note is unavailable, inaccessible or still being processed.", { statusCode: 404, messageCode: "connectors.granola.noteUnavailable" });
|
|
70
|
+
if (response.status === 413) throw new ConnectorError("connector_response_too_large", "Read the transcript in pages with transcripts.list.", { statusCode: 413, messageCode: "connectors.granola.readTranscriptPagesTranscriptsList" });
|
|
71
71
|
if (!response.ok) throw Object.assign(new Error("Granola request failed."), { status: response.status });
|
|
72
72
|
let value;
|
|
73
73
|
try { value = await response.json(); } catch { /* checked below */ }
|
|
74
74
|
if (response.status !== 200 || !value || typeof value !== "object" || value.error) {
|
|
75
|
-
throw new ConnectorError("connector_response_invalid", "Granola returned an unexpected response.", { statusCode: 502 });
|
|
75
|
+
throw new ConnectorError("connector_response_invalid", "Granola returned an unexpected response.", { statusCode: 502, messageCode: "connectors.shared.unexpectedResponse", messageParams: { provider: "Granola" } });
|
|
76
76
|
}
|
|
77
77
|
const url = new URL(address);
|
|
78
78
|
const list = url.pathname === "/v1/notes" ? "notes" : url.pathname === "/v1/folders" ? "folders" : url.pathname.endsWith("/transcript") ? "transcript" : null;
|
|
79
79
|
if (list ? value[list]?.length > Number(url.searchParams.get("page_size")) : value.id !== url.pathname.split("/").at(-1)) {
|
|
80
|
-
throw new ConnectorError("connector_response_invalid", "Granola returned data that does not match this request.", { statusCode: 502 });
|
|
80
|
+
throw new ConnectorError("connector_response_invalid", "Granola returned data that does not match this request.", { statusCode: 502, messageCode: "connectors.granola.returnedDataDoesNotMatch" });
|
|
81
81
|
}
|
|
82
82
|
return value;
|
|
83
83
|
}
|
package/src/server/hubspot.js
CHANGED
|
@@ -39,7 +39,9 @@ function objectOperations(object) {
|
|
|
39
39
|
const body = validateSchemaPayload({ schema: create, mode: "replace" }, input, { statusCode: 422 });
|
|
40
40
|
if (object === "contacts" ? !["email", "firstname", "lastname"].some(key => body.properties[key]?.trim()) :
|
|
41
41
|
!body.properties.dealname?.trim() || !body.properties.dealstage?.trim()) {
|
|
42
|
-
throw
|
|
42
|
+
throw object === "contacts"
|
|
43
|
+
? new ConnectorError("connector_input_invalid", "Supply email, firstname or lastname.", { statusCode: 422, messageCode: "connectors.hubspot.contactIdentityRequired" })
|
|
44
|
+
: new ConnectorError("connector_input_invalid", "Supply dealname and the internal dealstage ID.", { statusCode: 422, messageCode: "connectors.hubspot.dealNameStageRequired" });
|
|
43
45
|
}
|
|
44
46
|
return { method: "POST", url: `${base}/${object}`, body };
|
|
45
47
|
}, validateResult: validRecord },
|
|
@@ -65,10 +67,10 @@ const hubspotProvider = Object.freeze({
|
|
|
65
67
|
if (!response.ok) return response;
|
|
66
68
|
let value;
|
|
67
69
|
try { value = await response.clone().json(); } catch {
|
|
68
|
-
throw new ConnectorError("connector_response_invalid", "HubSpot returned an invalid token response.", { statusCode: 502 });
|
|
70
|
+
throw new ConnectorError("connector_response_invalid", "HubSpot returned an invalid token response.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenResponse", messageParams: { provider: "HubSpot" } });
|
|
69
71
|
}
|
|
70
72
|
if (!Array.isArray(value?.scopes) || !value.scopes.every((scope) => typeof scope === "string" && /^[^\s]+$/u.test(scope))) {
|
|
71
|
-
throw new ConnectorError("connector_response_invalid", "HubSpot returned an invalid permission grant.", { statusCode: 502 });
|
|
73
|
+
throw new ConnectorError("connector_response_invalid", "HubSpot returned an invalid permission grant.", { statusCode: 502, messageCode: "connectors.shared.invalidPermissionGrant", messageParams: { provider: "HubSpot" } });
|
|
72
74
|
}
|
|
73
75
|
return Response.json({ ...value, scope: value.scopes.join(" ") }, { status: response.status });
|
|
74
76
|
},
|
package/src/server/inngest.js
CHANGED
|
@@ -44,7 +44,7 @@ const inngestProvider = Object.freeze({
|
|
|
44
44
|
key = await resolveReference(eventKeyRef);
|
|
45
45
|
if (typeof key !== "string" || !key.trim() || /[\p{Cc}]/u.test(key)) throw new Error("Invalid Event Key.");
|
|
46
46
|
} catch {
|
|
47
|
-
throw new ConnectorError("connector_binding_missing", "The Inngest Event Key binding is missing or invalid.");
|
|
47
|
+
throw new ConnectorError("connector_binding_missing", "The Inngest Event Key binding is missing or invalid.", { messageCode: "connectors.inngest.eventKeyBindingMissingInvalid" });
|
|
48
48
|
}
|
|
49
49
|
options.signal.throwIfAborted();
|
|
50
50
|
// The Event API uses only the Event Key in its path. Never forward the Signing Key.
|
|
@@ -73,9 +73,9 @@ const inngestProvider = Object.freeze({
|
|
|
73
73
|
body: { event, eventKeyRef: settings.eventKeyRef, branchEnvironment: settings.branchEnvironment } };
|
|
74
74
|
},
|
|
75
75
|
validateResult(result) {
|
|
76
|
-
if (result?.status === 401) throw new ConnectorError("connector_reconnect_required", "Configure a valid Inngest Event Key and reconnect.", { statusCode: 401 });
|
|
77
|
-
if (result?.status === 403) throw new ConnectorError("connector_permission_denied", "Inngest denied this event.", { statusCode: 403 });
|
|
78
|
-
if (result?.status === 429) throw new ConnectorError("connector_rate_limited", "Inngest's event limit was reached.", { statusCode: 429 });
|
|
76
|
+
if (result?.status === 401) throw new ConnectorError("connector_reconnect_required", "Configure a valid Inngest Event Key and reconnect.", { statusCode: 401, messageCode: "connectors.inngest.configureValidEventKeyReconnect" });
|
|
77
|
+
if (result?.status === 403) throw new ConnectorError("connector_permission_denied", "Inngest denied this event.", { statusCode: 403, messageCode: "connectors.inngest.deniedEvent" });
|
|
78
|
+
if (result?.status === 429) throw new ConnectorError("connector_rate_limited", "Inngest's event limit was reached.", { statusCode: 429, messageCode: "connectors.inngest.eventLimitReached" });
|
|
79
79
|
return result?.status === 200 && !result.error && Array.isArray(result.ids) && result.ids.length === 1 &&
|
|
80
80
|
result.ids.every((id) => typeof id === "string" && id.length > 0);
|
|
81
81
|
}
|
package/src/server/klipy.js
CHANGED
|
@@ -12,7 +12,7 @@ const mediaFields = {
|
|
|
12
12
|
|
|
13
13
|
function mediaResult(response) {
|
|
14
14
|
if (response?.result === false) {
|
|
15
|
-
throw new ConnectorError("connector_provider_failed", "KLIPY rejected the operation.", { statusCode: 502 });
|
|
15
|
+
throw new ConnectorError("connector_provider_failed", "KLIPY rejected the operation.", { statusCode: 502, messageCode: "connectors.klipy.rejectedOperation" });
|
|
16
16
|
}
|
|
17
17
|
return response?.result === true && Array.isArray(response.data?.data);
|
|
18
18
|
}
|
package/src/server/lightspeed.js
CHANGED
|
@@ -26,7 +26,7 @@ function collection(resource) {
|
|
|
26
26
|
const request = operation.request(input, settings);
|
|
27
27
|
const query = new URL(request.url).searchParams;
|
|
28
28
|
if (query.has("after") && query.has("before") && Number(query.get("after")) >= Number(query.get("before"))) {
|
|
29
|
-
throw new ConnectorError("connector_input_invalid", "The lower version must be below the upper version.", { statusCode: 422 });
|
|
29
|
+
throw new ConnectorError("connector_input_invalid", "The lower version must be below the upper version.", { statusCode: 422, messageCode: "connectors.lightspeed.lowerVersionMustBelowUpper" });
|
|
30
30
|
}
|
|
31
31
|
return request;
|
|
32
32
|
}
|
|
@@ -55,7 +55,7 @@ const lightspeedProvider = Object.freeze({
|
|
|
55
55
|
if (value?.domain_prefix !== settings.domainPrefix || typeof value.refresh_token !== "string" || !value.refresh_token.trim() ||
|
|
56
56
|
!Number.isSafeInteger(value.expires_in) || value.expires_in <= 0 ||
|
|
57
57
|
typeof value.scope !== "string" || !/^[^\s]+(?: [^\s]+)*$/u.test(value.scope)) {
|
|
58
|
-
throw new ConnectorError("connector_response_invalid", "Lightspeed returned an invalid token grant or a different store.", { statusCode: 502 });
|
|
58
|
+
throw new ConnectorError("connector_response_invalid", "Lightspeed returned an invalid token grant or a different store.", { statusCode: 502, messageCode: "connectors.lightspeed.returnedInvalidTokenGrantDifferent" });
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
return response;
|
|
@@ -65,7 +65,7 @@ const lightspeedProvider = Object.freeze({
|
|
|
65
65
|
const request = inventoryRead.request(input, settings);
|
|
66
66
|
const { after, before, variants, product_id } = request.body;
|
|
67
67
|
if (after !== undefined && before !== undefined && after >= before || variants && !product_id)
|
|
68
|
-
throw new ConnectorError("connector_input_invalid", "Use ordered inventory version bounds and a product ID when requesting variants.", { statusCode: 422 });
|
|
68
|
+
throw new ConnectorError("connector_input_invalid", "Use ordered inventory version bounds and a product ID when requesting variants.", { statusCode: 422, messageCode: "connectors.lightspeed.useOrderedInventoryVersionBounds" });
|
|
69
69
|
return request;
|
|
70
70
|
} }
|
|
71
71
|
},
|
|
@@ -74,7 +74,7 @@ const lightspeedProvider = Object.freeze({
|
|
|
74
74
|
const inventory = new URL(address).pathname.endsWith("/inventory");
|
|
75
75
|
const limit = inventory ? options.body.size : Number(new URL(address).searchParams.get("page_size"));
|
|
76
76
|
if ((inventory ? result?.length : result?.data?.length) > limit) {
|
|
77
|
-
throw new ConnectorError("connector_response_invalid", "Lightspeed returned more records than requested.", { statusCode: 502 });
|
|
77
|
+
throw new ConnectorError("connector_response_invalid", "Lightspeed returned more records than requested.", { statusCode: 502, messageCode: "connectors.shared.tooManyRecords", messageParams: { provider: "Lightspeed" } });
|
|
78
78
|
}
|
|
79
79
|
return result;
|
|
80
80
|
}
|
package/src/server/linear.js
CHANGED
|
@@ -20,12 +20,12 @@ const linearProvider = Object.freeze({
|
|
|
20
20
|
if (!response.ok) return response;
|
|
21
21
|
let value;
|
|
22
22
|
try { value = await response.clone().json(); } catch {
|
|
23
|
-
throw new ConnectorError("connector_response_invalid", "Linear returned an invalid token response.", { statusCode: 502 });
|
|
23
|
+
throw new ConnectorError("connector_response_invalid", "Linear returned an invalid token response.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenResponse", messageParams: { provider: "Linear" } });
|
|
24
24
|
}
|
|
25
25
|
// Linear accepts commas at authorization but returns space-separated grants.
|
|
26
26
|
const scopes = typeof value?.scope === "string" ? value.scope.split(" ").filter(Boolean) : value?.scope;
|
|
27
27
|
if (!Array.isArray(scopes) || !scopes.every(scope => typeof scope === "string" && /^[^\s,]+$/u.test(scope))) {
|
|
28
|
-
throw new ConnectorError("connector_response_invalid", "Linear returned an invalid permission grant.", { statusCode: 502 });
|
|
28
|
+
throw new ConnectorError("connector_response_invalid", "Linear returned an invalid permission grant.", { statusCode: 502, messageCode: "connectors.shared.invalidPermissionGrant", messageParams: { provider: "Linear" } });
|
|
29
29
|
}
|
|
30
30
|
return Response.json({ ...value, scope: scopes.join(",") }, { status: response.status });
|
|
31
31
|
},
|
package/src/server/linkedin.js
CHANGED
|
@@ -6,21 +6,21 @@ async function normalizeTokenResponse(response, { grantType }) {
|
|
|
6
6
|
let value;
|
|
7
7
|
try { value = await response.clone().json(); } catch {
|
|
8
8
|
if (!response.ok) return response;
|
|
9
|
-
throw new ConnectorError("connector_response_invalid", "LinkedIn returned an invalid token response.", { statusCode: 502 });
|
|
9
|
+
throw new ConnectorError("connector_response_invalid", "LinkedIn returned an invalid token response.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenResponse", messageParams: { provider: "LinkedIn" } });
|
|
10
10
|
}
|
|
11
11
|
if (!response.ok) {
|
|
12
12
|
if (grantType === "refresh_token" && response.status === 400 && value?.error === "invalid_request") {
|
|
13
|
-
throw new ConnectorError("connector_reconnect_required", "Connect this LinkedIn account again.", { statusCode: 401 });
|
|
13
|
+
throw new ConnectorError("connector_reconnect_required", "Connect this LinkedIn account again.", { statusCode: 401, messageCode: "connectors.shared.reconnectAccount", messageParams: { provider: "LinkedIn" } });
|
|
14
14
|
}
|
|
15
15
|
return response;
|
|
16
16
|
}
|
|
17
17
|
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
18
18
|
!Number.isSafeInteger(value.expires_in) || value.expires_in <= 0 ||
|
|
19
19
|
(value.scope !== undefined && (typeof value.scope !== "string" || !/^[^\s]+(?: [^\s]+)*$/u.test(value.scope)))) {
|
|
20
|
-
throw new ConnectorError("connector_response_invalid", "LinkedIn returned an invalid permission grant.", { statusCode: 502 });
|
|
20
|
+
throw new ConnectorError("connector_response_invalid", "LinkedIn returned an invalid permission grant.", { statusCode: 502, messageCode: "connectors.shared.invalidPermissionGrant", messageParams: { provider: "LinkedIn" } });
|
|
21
21
|
}
|
|
22
22
|
if (value.scope !== undefined && !["openid", "profile"].every((scope) => value.scope.split(" ").includes(scope))) {
|
|
23
|
-
throw new ConnectorError("connector_reconnect_required", "Connect LinkedIn with OpenID and profile permissions.", { statusCode: 401 });
|
|
23
|
+
throw new ConnectorError("connector_reconnect_required", "Connect LinkedIn with OpenID and profile permissions.", { statusCode: 401, messageCode: "connectors.linkedin.connectOpenidProfilePermissions" });
|
|
24
24
|
}
|
|
25
25
|
// LinkedIn documents bearer responses without token_type. Identity tokens are
|
|
26
26
|
// unused: this adapter reads userinfo with the access token, never logs users in.
|
|
@@ -47,7 +47,7 @@ const linkedinProvider = Object.freeze({
|
|
|
47
47
|
if (address !== "https://api.linkedin.com/v2/ugcPosts") return request(address, options);
|
|
48
48
|
const member = await request("https://api.linkedin.com/v2/userinfo", { ...options, method: "GET", body: undefined });
|
|
49
49
|
if (typeof member?.sub !== "string" || !/^[a-zA-Z0-9_-]{1,1024}$/u.test(member.sub))
|
|
50
|
-
throw new ConnectorError("connector_response_invalid", "LinkedIn returned an invalid member identity.", { statusCode: 502 });
|
|
50
|
+
throw new ConnectorError("connector_response_invalid", "LinkedIn returned an invalid member identity.", { statusCode: 502, messageCode: "connectors.linkedin.returnedInvalidMemberIdentity" });
|
|
51
51
|
const response = await fetchImpl(address, { ...options, credentials: "omit", redirect: "error",
|
|
52
52
|
headers: { ...options.headers, "Content-Type": "application/json", "X-Restli-Protocol-Version": "2.0.0" },
|
|
53
53
|
body: JSON.stringify({ author: `urn:li:person:${member.sub}`, lifecycleState: "PUBLISHED",
|
|
@@ -55,7 +55,7 @@ const linkedinProvider = Object.freeze({
|
|
|
55
55
|
visibility: { "com.linkedin.ugc.MemberNetworkVisibility": options.body.visibility } }) });
|
|
56
56
|
await response.body?.cancel();
|
|
57
57
|
if (!response.ok) throw Object.assign(new Error("LinkedIn publishing failed."), { status: response.status });
|
|
58
|
-
if (response.status !== 201) throw new ConnectorError("connector_response_invalid", "LinkedIn did not confirm post creation.", { statusCode: 502 });
|
|
58
|
+
if (response.status !== 201) throw new ConnectorError("connector_response_invalid", "LinkedIn did not confirm post creation.", { statusCode: 502, messageCode: "connectors.linkedin.didNotConfirmPostCreation" });
|
|
59
59
|
return { id: response.headers.get("x-restli-id") };
|
|
60
60
|
},
|
|
61
61
|
operations: {
|
package/src/server/mapbox.js
CHANGED
|
@@ -25,7 +25,7 @@ const mapboxProvider = Object.freeze({
|
|
|
25
25
|
Number.isFinite(route.distance) && route.distance >= 0 && Number.isFinite(route.duration) && route.duration >= 0 && route.geometry?.type === "LineString" && Array.isArray(route.geometry.coordinates)) },
|
|
26
26
|
"token.read": jsonOperation("https://api.mapbox.com/tokens/v2", {}, (result) => {
|
|
27
27
|
if (["TokenMalformed", "TokenInvalid", "TokenExpired", "TokenRevoked"].includes(result?.code)) {
|
|
28
|
-
throw new ConnectorError("connector_reconnect_required", "Configure a valid Mapbox token and connect again.", { statusCode: 401 });
|
|
28
|
+
throw new ConnectorError("connector_reconnect_required", "Configure a valid Mapbox token and connect again.", { statusCode: 401, messageCode: "connectors.mapbox.configureValidTokenConnectAgain" });
|
|
29
29
|
}
|
|
30
30
|
return result?.code === "TokenValid" && ["pk", "sk", "tk"].includes(result.token?.usage) && typeof result.token.user === "string";
|
|
31
31
|
}),
|
|
@@ -33,8 +33,9 @@ async function registerMcpClient(providerName, metadata, { clientName, callbackU
|
|
|
33
33
|
return { clientId: client.client_id, clientSecret: client.client_secret,
|
|
34
34
|
...(client.client_secret_expires_at === undefined ? {} : { clientSecretExpiresAt: client.client_secret_expires_at }) };
|
|
35
35
|
} catch {
|
|
36
|
-
if (requestSignal.aborted) throw new ConnectorError("connector_registration_interrupted", "Registration was interrupted. Check the provider before creating another client.");
|
|
37
|
-
throw new ConnectorError("connector_registration_failed", `${providerName} client registration failed. Check the provider before creating another client.`,
|
|
36
|
+
if (requestSignal.aborted) throw new ConnectorError("connector_registration_interrupted", "Registration was interrupted. Check the provider before creating another client.", { messageCode: "connectors.mcp.registrationInterrupted" });
|
|
37
|
+
throw new ConnectorError("connector_registration_failed", `${providerName} client registration failed. Check the provider before creating another client.`,
|
|
38
|
+
{ statusCode: 502, messageCode: "connectors.mcp.registrationFailed", messageParams: { provider: providerName } });
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -28,7 +28,7 @@ async function exchangeMcp(endpoint, { headers, signal, body }, { fetchImpl }) {
|
|
|
28
28
|
reconnectionOptions: { initialReconnectionDelay: 1000, maxReconnectionDelay: 1000, reconnectionDelayGrowFactor: 1, maxRetries: 0 },
|
|
29
29
|
fetch: async (address, init = {}) => {
|
|
30
30
|
if (new URL(String(address)).href !== endpoint) {
|
|
31
|
-
throw new ConnectorError("connector_destination_invalid", "The MCP transport requested an unexpected destination.");
|
|
31
|
+
throw new ConnectorError("connector_destination_invalid", "The MCP transport requested an unexpected destination.", { messageCode: "connectors.mcp.transportRequestedUnexpectedDestination" });
|
|
32
32
|
}
|
|
33
33
|
const requestHeaders = new Headers(init.headers);
|
|
34
34
|
// Keep MCP's Accept/protocol/session headers, and bind credentials to this endpoint only.
|
|
@@ -31,7 +31,7 @@ const microsoftFabricProvider = Object.freeze({
|
|
|
31
31
|
let error;
|
|
32
32
|
try { error = (await response.clone().json())?.error; } catch { /* OAuth handles the response */ }
|
|
33
33
|
if (["interaction_required", "login_required", "consent_required"].includes(error)) {
|
|
34
|
-
throw new ConnectorError("connector_reconnect_required", "Microsoft requires a new sign-in or consent.", { statusCode: 401 });
|
|
34
|
+
throw new ConnectorError("connector_reconnect_required", "Microsoft requires a new sign-in or consent.", { statusCode: 401, messageCode: "connectors.microsoftFabric.requiresNewSignConsent" });
|
|
35
35
|
}
|
|
36
36
|
return response;
|
|
37
37
|
}
|
|
@@ -39,7 +39,7 @@ const microsoftFabricProvider = Object.freeze({
|
|
|
39
39
|
try { value = await response.clone().json(); } catch { /* rejected below */ }
|
|
40
40
|
if (!Number.isSafeInteger(value?.expires_in) || value.expires_in <= 30 ||
|
|
41
41
|
(value.scope !== undefined && (typeof value.scope !== "string" || !/^[^\s]+(?: [^\s]+)*$/u.test(value.scope)))) {
|
|
42
|
-
throw new ConnectorError("connector_response_invalid", "Microsoft returned an invalid token grant.", { statusCode: 502 });
|
|
42
|
+
throw new ConnectorError("connector_response_invalid", "Microsoft returned an invalid token grant.", { statusCode: 502, messageCode: "connectors.shared.invalidTokenGrant", messageParams: { provider: "Microsoft" } });
|
|
43
43
|
}
|
|
44
44
|
// Entra may return the permission name without the resource URI requested in consent.
|
|
45
45
|
if (value.scope?.split(" ").includes("GraphQLApi.Execute.All")) {
|
|
@@ -72,7 +72,7 @@ const microsoftFabricProvider = Object.freeze({
|
|
|
72
72
|
const result = await request(address, options);
|
|
73
73
|
if (Array.isArray(result?.errors) && result.errors.length > 0) {
|
|
74
74
|
// A mutation can have taken effect before partial errors arrive. Do not replay or expose provider text.
|
|
75
|
-
throw new ConnectorError("connector_provider_failed", "Fabric could not complete the GraphQL document. Check its schema and permissions before retrying.", { statusCode: 502 });
|
|
75
|
+
throw new ConnectorError("connector_provider_failed", "Fabric could not complete the GraphQL document. Check its schema and permissions before retrying.", { statusCode: 502, messageCode: "connectors.microsoftFabric.couldNotCompleteGraphqlDocument" });
|
|
76
76
|
}
|
|
77
77
|
return result;
|
|
78
78
|
}
|
|
@@ -37,7 +37,7 @@ const base = microsoftProvider(microsoftOneDriveDefinition, "common", "items.lis
|
|
|
37
37
|
const { parentId, name, bodyBase64, conflictBehavior } = validateSchemaPayload({ schema: uploadSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
38
38
|
const body = Buffer.from(bodyBase64, "base64");
|
|
39
39
|
if (body.toString("base64") !== bodyBase64 || body.length > 5_000_000) {
|
|
40
|
-
throw new ConnectorError("connector_input_invalid", "Use standard padded base64 up to 5 MB; larger files need a native upload session.", { statusCode: 422 });
|
|
40
|
+
throw new ConnectorError("connector_input_invalid", "Use standard padded base64 up to 5 MB; larger files need a native upload session.", { statusCode: 422, messageCode: "connectors.microsoftOnedrive.useStandardPaddedBase64Up" });
|
|
41
41
|
}
|
|
42
42
|
const url = new URL(`https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentId)}:/${encodeURIComponent(name)}:/content`);
|
|
43
43
|
url.searchParams.set("@microsoft.graph.conflictBehavior", conflictBehavior);
|
|
@@ -43,7 +43,7 @@ const microsoftOneNoteProvider = Object.freeze({ ...base,
|
|
|
43
43
|
if (reader) try { while (true) {
|
|
44
44
|
const { done, value } = await reader.read(); if (done) break;
|
|
45
45
|
size += value.byteLength;
|
|
46
|
-
if (size > 1048576) { await reader.cancel(); throw new ConnectorError("connector_response_too_large", "Use the native OneNote client for pages above 1 MiB.", { statusCode: 413 }); }
|
|
46
|
+
if (size > 1048576) { await reader.cancel(); throw new ConnectorError("connector_response_too_large", "Use the native OneNote client for pages above 1 MiB.", { statusCode: 413, messageCode: "connectors.microsoftOnenote.useNativeOnenoteClientPages" }); }
|
|
47
47
|
chunks.push(value);
|
|
48
48
|
} } finally { reader.releaseLock(); }
|
|
49
49
|
return { html: Buffer.concat(chunks).toString("utf8") };
|
|
@@ -31,7 +31,7 @@ const base = microsoftProvider(microsoftOutlookDefinition, "common", "folders.li
|
|
|
31
31
|
"calendars.list": { ...graphRead("Calendars.Read", {}, () => ({ pathname: "/me/calendars" })), scopes: ["Calendars.Read", "Calendars.ReadWrite"] },
|
|
32
32
|
"events.list": { ...graphRead("Calendars.Read", { calendarId: id, pageSize }, ({ calendarId, pageSize }) => ({ pathname: `/me/calendars/${encodeURIComponent(calendarId)}/events`, query: { $top: pageSize } })), scopes: ["Calendars.Read", "Calendars.ReadWrite"] },
|
|
33
33
|
"events.create": operation(["Calendars.ReadWrite"], { calendarId: id, subject: { ...text, maxLength: 255 }, start: instant, end: instant }, ({ calendarId, subject, start, end }) => {
|
|
34
|
-
if (Date.parse(end) <= Date.parse(start)) throw new ConnectorError("connector_input_invalid", "End must follow start.", { statusCode: 422 });
|
|
34
|
+
if (Date.parse(end) <= Date.parse(start)) throw new ConnectorError("connector_input_invalid", "End must follow start.", { statusCode: 422, messageCode: "connectors.microsoftOutlook.endMustFollowStart" });
|
|
35
35
|
return { method: "POST", url: url(`/calendars/${encodeURIComponent(calendarId)}/events`), body: { subject, start: { dateTime: start.slice(0, -1), timeZone: "UTC" }, end: { dateTime: end.slice(0, -1), timeZone: "UTC" } } };
|
|
36
36
|
}),
|
|
37
37
|
"folders.list": graphRead("Mail.Read", {
|
package/src/server/microsoft.js
CHANGED
|
@@ -76,12 +76,13 @@ function microsoftDocumentProvider(definition, extensions) {
|
|
|
76
76
|
const result = await request(address, options);
|
|
77
77
|
if (new URL(address).pathname.endsWith("/children")) {
|
|
78
78
|
if (!provider.operations["items.list"].validateResult(result)) {
|
|
79
|
-
throw new ConnectorError("connector_response_invalid", "Microsoft returned an invalid file list.", { statusCode: 502 });
|
|
79
|
+
throw new ConnectorError("connector_response_invalid", "Microsoft returned an invalid file list.", { statusCode: 502, messageCode: "connectors.microsoft.returnedInvalidFileList" });
|
|
80
80
|
}
|
|
81
81
|
return { ...result, value: result.value.filter((item) => item.folder || matches(item)) };
|
|
82
82
|
}
|
|
83
|
-
if (!metadata(result)) throw new ConnectorError("connector_response_invalid", "Microsoft returned invalid file metadata.", { statusCode: 502 });
|
|
84
|
-
if (!matches(result)) throw new ConnectorError("connector_document_type_invalid", `Choose a ${definition.name} file
|
|
83
|
+
if (!metadata(result)) throw new ConnectorError("connector_response_invalid", "Microsoft returned invalid file metadata.", { statusCode: 502, messageCode: "connectors.microsoft.returnedInvalidFileMetadata" });
|
|
84
|
+
if (!matches(result)) throw new ConnectorError("connector_document_type_invalid", `Choose a ${definition.name} file.`,
|
|
85
|
+
{ messageCode: "connectors.microsoft.chooseDocumentFile", messageParams: { product: definition.name } });
|
|
85
86
|
return result;
|
|
86
87
|
} });
|
|
87
88
|
}
|