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