@jskit-ai/connectors-catalog 0.1.63 → 0.1.65
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/README.md +12 -17
- package/docs/ai.md +5 -17
- package/docs/amazon-redshift.md +6 -8
- package/docs/apollo-io.md +0 -8
- package/docs/ashby.md +0 -8
- package/docs/attention.md +0 -8
- package/docs/aws-athena.md +2 -2
- package/docs/aws-credentials.md +1 -1
- package/docs/aws-s3.md +1 -1
- package/docs/bigquery.md +2 -4
- package/docs/calendly.md +4 -14
- package/docs/chargebee.md +0 -9
- package/docs/clickhouse.md +1 -1
- package/docs/confidence-oauth.md +3 -9
- package/docs/contentful.md +4 -13
- package/docs/databricks.md +35 -3
- package/docs/dbt-semantic-layer.md +2 -8
- package/docs/figma.md +5 -5
- package/docs/firebase-cloud-messaging.md +3 -3
- package/docs/firecrawl.md +1 -1
- package/docs/fireflies.md +7 -10
- package/docs/gemini-enterprise.md +33 -3
- package/docs/gong.md +0 -9
- package/docs/google-ads.md +53 -11
- package/docs/google-analytics.md +2 -2
- package/docs/google-maps-platform.md +3 -9
- package/docs/granola.md +1 -1
- package/docs/hubspot.md +25 -0
- package/docs/klipy.md +0 -13
- package/docs/lexware.md +0 -8
- package/docs/lightspeed.md +32 -3
- package/docs/linear.md +24 -7
- package/docs/linkedin.md +16 -16
- package/docs/logo-dev.md +7 -25
- package/docs/mailgun.md +1 -14
- package/docs/mapbox.md +0 -8
- package/docs/microsoft-fabric.md +50 -13
- package/docs/microsoft-oauth.md +33 -0
- package/docs/microsoft-powerpoint.md +2 -13
- package/docs/microsoft-word.md +2 -13
- package/docs/miro.md +2 -2
- package/docs/oura.md +4 -3
- package/docs/polar.md +3 -5
- package/docs/posthog.md +2 -17
- package/docs/prestashop.md +5 -9
- package/docs/salesforce.md +41 -12
- package/docs/sentry.md +1 -12
- package/docs/sevdesk.md +2 -13
- package/docs/shopify.md +35 -2
- package/docs/slack.md +39 -14
- package/docs/snowflake.md +27 -2
- package/docs/telegram.md +0 -10
- package/docs/tiktok.md +19 -1
- package/docs/twilio.md +1 -1
- package/docs/twitch.md +34 -4
- package/docs/wave.md +36 -15
- package/docs/wix.md +4 -17
- package/docs/wiz.md +4 -4
- package/docs/wordpress-com.md +4 -13
- package/docs/workday.md +28 -9
- package/docs/x-twitter.md +0 -9
- package/docs/xero.md +39 -11
- package/docs/zoho-books.md +41 -10
- package/docs/zoho-crm.md +36 -12
- package/package.json +3 -3
- package/patterns/oauth-connection/PATTERN.md +27 -610
- package/patterns/paddle-catalogue/example/create-products.js +2 -1
- package/patterns/redshift-queries/PATTERN.md +1 -1
- package/src/server/attention.js +8 -11
- package/src/server/bigquery.js +7 -11
- package/src/server/brevo.js +7 -10
- package/src/server/calendly.js +7 -9
- package/src/server/chargebee.js +3 -6
- package/src/server/firecrawl.js +3 -3
- package/src/server/gatewayapi.js +4 -4
- package/src/server/github-api.js +1 -1
- package/src/server/gitlab-api.js +1 -1
- package/src/server/gmail.js +6 -11
- package/src/server/gong.js +4 -4
- package/src/server/google-ads-search.js +4 -5
- package/src/server/google-docs.js +10 -22
- package/src/server/google-drive.js +6 -6
- package/src/server/google-maps-platform.js +4 -8
- package/src/server/google-search-console.js +2 -2
- package/src/server/google-sheets.js +12 -18
- package/src/server/google-slides.js +10 -16
- package/src/server/google.js +17 -9
- package/src/server/jsonOperation.js +17 -18
- package/src/server/lexware.js +4 -9
- package/src/server/resend.js +8 -14
- package/src/server/semrush.js +70 -89
- package/src/server/wix.js +152 -222
- package/src/server/x-twitter.js +21 -18
- package/src/shared/ai.js +3 -1
- package/src/shared/definitions.js +2 -34
- package/src/shared/firecrawl.js +17 -0
- package/src/shared/gemini-enterprise.js +1 -1
- package/src/shared/google-ads.js +1 -1
- package/src/shared/google.js +2 -2
- package/src/shared/logo-dev.js +1 -1
- package/src/shared/mapbox.js +2 -2
- package/src/shared/mcp.js +2 -5
- package/src/shared/microsoft-fabric.js +1 -1
- package/src/shared/microsoft.js +12 -24
- package/src/shared/resend.js +20 -0
- package/src/shared/siteUrl.js +1 -1
- package/src/shared/tokens.js +4 -6
- package/src/shared/workday.js +1 -1
- package/test/firecrawl.test.js +1 -1
- package/test/github-api.test.js +2 -1
- package/test/gitlab-api.test.js +2 -1
- package/test/gong.test.js +2 -2
- package/test/google.test.js +20 -10
- package/test/setupGuides.test.js +18 -0
- package/test/tokens.test.js +1 -1
- package/test/wix.test.js +13 -0
- package/test/wiz.test.js +9 -3
|
@@ -1,31 +1,25 @@
|
|
|
1
|
-
import { createSchema } from "json-rest-schema";
|
|
2
|
-
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
1
|
import { googleSlidesDefinition } from "../shared/google.js";
|
|
4
|
-
import { googleRead, googleProvider, googleFileVerification, documentId } from "./google.js";
|
|
2
|
+
import { googleOperation, googleBatchRequests, googleRead, googleProvider, googleFileVerification, documentId } from "./google.js";
|
|
5
3
|
const origin = "https://slides.googleapis.com";
|
|
6
4
|
const readScopes = ["presentations.readonly", "presentations", "drive.readonly", "drive.file", "drive"];
|
|
7
|
-
const writable = ["presentations", "drive.file", "drive"]
|
|
5
|
+
const writable = ["presentations", "drive.file", "drive"];
|
|
8
6
|
const validPresentation = value => typeof value?.presentationId === "string" && typeof value.title === "string";
|
|
9
7
|
const get = googleRead(readScopes, { presentationId: documentId }, ({ presentationId }) => ({
|
|
10
8
|
url: `${origin}/v1/presentations/${encodeURIComponent(presentationId)}`
|
|
11
9
|
}), validPresentation);
|
|
12
10
|
const pageId = { ...documentId, validator: value => /^[a-zA-Z0-9_][a-zA-Z0-9_:-]*$/.test(value) || "Enter a slide object ID, not its URL." };
|
|
13
|
-
const create = createSchema({ title: { type: "string", required: true, minLength: 1, maxLength: 1024 } });
|
|
14
|
-
const batch = createSchema({ presentationId: documentId, requests: { type: "array", required: true }, requiredRevisionId: { type: "string", minLength: 1, maxLength: 1024 } });
|
|
15
11
|
const base = googleProvider(googleSlidesDefinition, origin, "connection.verify", {
|
|
16
12
|
"connection.verify": googleFileVerification(get, "presentationId"),
|
|
17
13
|
"presentations.get": get,
|
|
18
|
-
"presentations.create": {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const { presentationId, requests, requiredRevisionId } = validateSchemaPayload({ schema: batch, mode: "replace" }, input, { statusCode: 422 });
|
|
25
|
-
if (!requests.length || requests.length > 100 || Buffer.byteLength(JSON.stringify(requests)) > 1048576 || requests.some(request => !request || typeof request !== "object" || Array.isArray(request) || Object.keys(request).length !== 1 || !/^[a-zA-Z][a-zA-Z0-9]+$/.test(Object.keys(request)[0]) || !Object.values(request)[0] || typeof Object.values(request)[0] !== "object" || Array.isArray(Object.values(request)[0]))) throw Object.assign(new Error("Supply 1–100 native Slides request objects, one operation per object, within 1 MiB."), { statusCode: 422 });
|
|
14
|
+
"presentations.create": googleOperation(writable, { title: { type: "string", required: true, minLength: 1, maxLength: 1024 } },
|
|
15
|
+
body => ({ method: "POST", url: `${origin}/v1/presentations`, body }), validPresentation),
|
|
16
|
+
"presentations.batchUpdate": googleOperation(writable,
|
|
17
|
+
{ presentationId: documentId, requests: { type: "array", required: true }, requiredRevisionId: { type: "string", minLength: 1, maxLength: 1024 } },
|
|
18
|
+
({ presentationId, requests, requiredRevisionId }) => {
|
|
19
|
+
googleBatchRequests(requests, "Slides");
|
|
26
20
|
return { method: "POST", url: `${origin}/v1/presentations/${encodeURIComponent(presentationId)}:batchUpdate`, body: { requests, ...(requiredRevisionId ? { writeControl: { requiredRevisionId } } : {}) } };
|
|
27
|
-
}
|
|
28
|
-
|
|
21
|
+
},
|
|
22
|
+
value => typeof value?.presentationId === "string" && (value.replies === undefined || Array.isArray(value.replies))),
|
|
29
23
|
"pages.get": googleRead(readScopes, { presentationId: documentId, pageObjectId: pageId }, ({ presentationId, pageObjectId }) => ({ url: `${origin}/v1/presentations/${encodeURIComponent(presentationId)}/pages/${encodeURIComponent(pageObjectId)}` }), value => typeof value?.objectId === "string"),
|
|
30
24
|
"pages.getThumbnail": googleRead(readScopes, { presentationId: documentId, pageObjectId: pageId, size: { type: "string", enum: ["SMALL", "MEDIUM", "LARGE"] } }, ({ presentationId, pageObjectId, size }) => ({ url: `${origin}/v1/presentations/${encodeURIComponent(presentationId)}/pages/${encodeURIComponent(pageObjectId)}/thumbnail`, query: { "thumbnailProperties.mimeType": "PNG", "thumbnailProperties.thumbnailSize": size } }), value => {
|
|
31
25
|
if (typeof value?.contentUrl !== "string" || !Number.isFinite(value.width) || !Number.isFinite(value.height) || value.width <= 0 || value.height <= 0) return false;
|
package/src/server/google.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSchema } from "json-rest-schema";
|
|
2
2
|
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
4
|
+
import { validatedOperation } from "./jsonOperation.js";
|
|
3
5
|
|
|
4
6
|
const oauth = Object.freeze({
|
|
5
7
|
issuer: "https://accounts.google.com",
|
|
@@ -10,15 +12,21 @@ const googleScope = (name) => name.startsWith("https:") ? name : `https://www.go
|
|
|
10
12
|
const documentId = { type: "string", required: true, minLength: 1, maxLength: 1024, validator: (value) => /^[a-zA-Z0-9_-]+$/u.test(value) || "Enter the document ID from its URL." };
|
|
11
13
|
const pageToken = { type: "string", minLength: 1, maxLength: 4096 };
|
|
12
14
|
|
|
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 });
|
|
17
|
+
const nativeRequest = request => request && typeof request === "object" && !Array.isArray(request) && Object.keys(request).length === 1 &&
|
|
18
|
+
/^[a-zA-Z][a-zA-Z0-9]+$/.test(Object.keys(request)[0]) && Boolean(Object.values(request)[0]) &&
|
|
19
|
+
typeof Object.values(request)[0] === "object" && !Array.isArray(Object.values(request)[0]);
|
|
20
|
+
// Docs, Slides and Sheets batchUpdate share one limit: 1-100 single-operation request objects within 1 MiB.
|
|
21
|
+
function googleBatchRequests(requests, product) {
|
|
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.`);
|
|
24
|
+
}
|
|
25
|
+
return requests;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
function googleOperation(scopes, fields, request, validateResult) {
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
scopes: scopes.map(scope => `https://www.googleapis.com/auth/${scope}`),
|
|
17
|
-
validateResult,
|
|
18
|
-
request(input) {
|
|
19
|
-
return request(validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 }));
|
|
20
|
-
}
|
|
21
|
-
};
|
|
29
|
+
return validatedOperation(fields, request, validateResult, scopes.map(googleScope));
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
function googleRead(scopes, fields, destination, validateResult) {
|
|
@@ -58,4 +66,4 @@ function googleFileVerification(get, field) {
|
|
|
58
66
|
validateResult: value => get.validateResult(value) || Array.isArray(value?.files)
|
|
59
67
|
};
|
|
60
68
|
}
|
|
61
|
-
export { googleOperation, googleRead, googleProvider, googleFileVerification, documentId, pageToken };
|
|
69
|
+
export { googleScope, googleOperation, googleBatchRequests, inputInvalid, googleRead, googleProvider, googleFileVerification, documentId, pageToken };
|
|
@@ -3,32 +3,31 @@ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
|
3
3
|
|
|
4
4
|
// Providers with custom paths or bodies share input validation, while retaining
|
|
5
5
|
// ownership of their request construction and response checks.
|
|
6
|
-
function validatedOperation(fields, build, validateResult) {
|
|
6
|
+
function validatedOperation(fields, build, validateResult, scopes = []) {
|
|
7
7
|
const schema = createSchema(fields);
|
|
8
8
|
return {
|
|
9
|
-
scopes
|
|
10
|
-
request(input) {
|
|
11
|
-
return build(validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 }));
|
|
9
|
+
scopes,
|
|
10
|
+
request(input, settings = {}, providerData) {
|
|
11
|
+
return build(validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 }), settings, providerData);
|
|
12
12
|
},
|
|
13
13
|
validateResult
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Copies validated values into a URL's query string.
|
|
18
|
+
function withQuery(url, values) {
|
|
19
|
+
const destination = new URL(url);
|
|
20
|
+
for (const [name, value] of Object.entries(values)) destination.searchParams.set(name, String(value));
|
|
21
|
+
return destination.href;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
// Provider endpoints with either query parameters or a JSON request body.
|
|
18
25
|
function jsonOperation(url, fields, validateResult, method = "GET") {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const endpoint = typeof url === "function" ? url(settings) : url;
|
|
25
|
-
if (method === "POST") return { method, url: endpoint, body: values };
|
|
26
|
-
const destination = new URL(endpoint);
|
|
27
|
-
for (const [name, value] of Object.entries(values)) destination.searchParams.set(name, String(value));
|
|
28
|
-
return { method, url: destination.href };
|
|
29
|
-
},
|
|
30
|
-
validateResult
|
|
31
|
-
};
|
|
26
|
+
return validatedOperation(fields, (values, settings) => {
|
|
27
|
+
const endpoint = typeof url === "function" ? url(settings) : url;
|
|
28
|
+
if (method === "POST") return { method, url: endpoint, body: values };
|
|
29
|
+
return { method, url: withQuery(endpoint, values) };
|
|
30
|
+
}, validateResult);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export { jsonOperation, validatedOperation };
|
|
33
|
+
export { jsonOperation, validatedOperation, withQuery };
|
package/src/server/lexware.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createSchema } from "json-rest-schema";
|
|
2
|
-
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
2
|
import { lexwareDefinition } from "../shared/tokens.js";
|
|
4
|
-
import { jsonOperation } from "./jsonOperation.js";
|
|
3
|
+
import { jsonOperation, validatedOperation } from "./jsonOperation.js";
|
|
5
4
|
|
|
6
5
|
const id = { type: "string", required: true, validator: value => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value) || "Use a Lexware resource UUID." };
|
|
7
6
|
const text = { type: "string", minLength: 1, maxLength: 1000 };
|
|
@@ -10,10 +9,6 @@ const paging = { page: { type: "integer", min: 0, max: 100000, defaultTo: 0 }, s
|
|
|
10
9
|
const pageResult = result => Array.isArray(result?.content) && typeof result.first === "boolean" && typeof result.last === "boolean" &&
|
|
11
10
|
["totalPages", "totalElements", "numberOfElements", "number"].every(field => Number.isInteger(result[field]) && result[field] >= 0) && Number.isInteger(result.size) && result.size > 0;
|
|
12
11
|
const resourceResult = result => typeof result?.id === "string";
|
|
13
|
-
function operation(fields, build, validateResult = resourceResult) {
|
|
14
|
-
const schema = createSchema(fields);
|
|
15
|
-
return { scopes: [], request(input) { return build(validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 })); }, validateResult };
|
|
16
|
-
}
|
|
17
12
|
const line = createSchema({
|
|
18
13
|
name: { ...text, required: true }, description: { ...text, maxLength: 5000 },
|
|
19
14
|
quantity: { type: "number", required: true, min: 0.000001, max: 1000000 },
|
|
@@ -31,7 +26,7 @@ const lexwareProvider = Object.freeze({
|
|
|
31
26
|
...paging, customer: { type: "boolean" }, vendor: { type: "boolean" }
|
|
32
27
|
}, pageResult),
|
|
33
28
|
...Object.fromEntries(["contacts", "articles", "invoices", "vouchers"].map(resource => [
|
|
34
|
-
`${resource}.get`,
|
|
29
|
+
`${resource}.get`, validatedOperation({ id }, ({ id }) => ({ method: "GET", url: `https://api.lexware.io/v1/${resource}/${id}` }), resourceResult)
|
|
35
30
|
])),
|
|
36
31
|
"articles.list": jsonOperation("https://api.lexware.io/v1/articles", {
|
|
37
32
|
...paging, articleNumber: text, gtin: text, type: { type: "string", enum: ["PRODUCT", "SERVICE"] }
|
|
@@ -42,7 +37,7 @@ const lexwareProvider = Object.freeze({
|
|
|
42
37
|
voucherStatus: { type: "string", required: true, enum: ["any", "draft", "open", "overdue", "paid", "paidoff", "voided", "transferred", "sepadebit", "unchecked"] },
|
|
43
38
|
contactId: { ...id, required: false }, archived: { type: "boolean" }
|
|
44
39
|
}, pageResult),
|
|
45
|
-
"invoices.createDraft":
|
|
40
|
+
"invoices.createDraft": validatedOperation({
|
|
46
41
|
contactId: id, voucherDate: date, shippingDate: date,
|
|
47
42
|
shippingType: { type: "string", required: true, enum: ["service", "delivery"] },
|
|
48
43
|
lineItems: { type: "array", required: true, validator: value => value.length > 0 && value.length <= 100 || "Use 1–100 invoice lines.",
|
|
@@ -53,7 +48,7 @@ const lexwareProvider = Object.freeze({
|
|
|
53
48
|
body: { ...other, voucherDate, address: { contactId },
|
|
54
49
|
lineItems: lineItems.map(({ netAmount, taxRatePercentage, ...item }) => ({ ...item, type: "custom", unitPrice: { currency: "EUR", netAmount, taxRatePercentage } })),
|
|
55
50
|
totalPrice: { currency: "EUR" }, taxConditions: { taxType: "net" }, shippingConditions: { shippingDate, shippingType } }
|
|
56
|
-
}))
|
|
51
|
+
}), resourceResult)
|
|
57
52
|
}
|
|
58
53
|
});
|
|
59
54
|
export { lexwareProvider };
|
package/src/server/resend.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSchema } from "json-rest-schema";
|
|
2
2
|
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
-
import { resendDefinition } from "../shared/
|
|
3
|
+
import { resendDefinition } from "../shared/resend.js";
|
|
4
|
+
import { validatedOperation, withQuery } from "./jsonOperation.js";
|
|
4
5
|
|
|
5
6
|
const pagination = createSchema({
|
|
6
7
|
limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
|
|
@@ -32,17 +33,12 @@ const membershipResult = (result) => result?.object === "contact_segment" && typ
|
|
|
32
33
|
const listResult = (result) => result?.object === "list" && Array.isArray(result.data);
|
|
33
34
|
// Fixed provider routes; the application authorizes each named operation before HTTP.
|
|
34
35
|
function marketingOperation(fields, method, destination, validateResult = hasId, draft = false) {
|
|
35
|
-
|
|
36
|
-
return { scopes: [], request(input) {
|
|
37
|
-
const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
36
|
+
return validatedOperation(fields, values => {
|
|
38
37
|
const { id, segmentId, ...body } = values;
|
|
39
|
-
const url =
|
|
40
|
-
if (method === "GET") {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
return { method, url: url.href, ...(method === "DELETE" ? {} : { body: draft ? { ...body, send: false } : body }) };
|
|
45
|
-
}, validateResult };
|
|
38
|
+
const url = `https://api.resend.com/${destination(values)}`;
|
|
39
|
+
if (method === "GET") return { method, url: withQuery(url, body) };
|
|
40
|
+
return { method, url: new URL(url).href, ...(method === "DELETE" ? {} : { body: draft ? { ...body, send: false } : body }) };
|
|
41
|
+
}, validateResult);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
const resendProvider = Object.freeze({
|
|
@@ -82,9 +78,7 @@ const resendProvider = Object.freeze({
|
|
|
82
78
|
scopes: [],
|
|
83
79
|
request(input) {
|
|
84
80
|
const query = validateSchemaPayload({ schema: pagination, mode: "replace" }, input, { statusCode: 422 });
|
|
85
|
-
|
|
86
|
-
for (const [name, value] of Object.entries(query)) url.searchParams.set(name, String(value));
|
|
87
|
-
return { method: "GET", url: url.href };
|
|
81
|
+
return { method: "GET", url: withQuery("https://api.resend.com/domains", query) };
|
|
88
82
|
},
|
|
89
83
|
validateResult: (result) => result?.object === "list" && Array.isArray(result.data) && typeof result.has_more === "boolean"
|
|
90
84
|
}
|
package/src/server/semrush.js
CHANGED
|
@@ -60,6 +60,8 @@ function semrushKeywordReport(target, kind) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const trackingObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value) && value.error === undefined);
|
|
64
|
+
|
|
63
65
|
function semrushTrackingReport(type, positions = false) {
|
|
64
66
|
const operation = jsonOperation("https://api.semrush.com/", {
|
|
65
67
|
campaignId: { type: "string", required: true, pattern: /^\d+(?:_\d+)?$/u, maxLength: 80 },
|
|
@@ -72,7 +74,14 @@ function semrushTrackingReport(type, positions = false) {
|
|
|
72
74
|
display_filter: { type: "string", minLength: 1, maxLength: 4000 }
|
|
73
75
|
} : {})
|
|
74
76
|
});
|
|
75
|
-
return { scopes: [],
|
|
77
|
+
return { scopes: [], validateResult(value, request) {
|
|
78
|
+
const url = new URL(request.url);
|
|
79
|
+
return trackingObject(value) && value.data !== null && typeof value.data === "object" && /^(0|[1-9]\d*)$/u.test(String(value.total)) &&
|
|
80
|
+
(type === "tracking_campaign_dates" ? Object.values(value.data).every((entry) => /^\d{8}$/u.test(String(entry?.Dt))) :
|
|
81
|
+
String(value.limit) === url.searchParams.get("display_limit") && String(value.offset) === url.searchParams.get("display_offset") &&
|
|
82
|
+
Object.values(value.data).length <= Number(url.searchParams.get("display_limit")) &&
|
|
83
|
+
Object.values(value.data).every((entry) => typeof entry?.Ph === "string" && typeof entry.Pi === "string" && entry.Dt !== null && typeof entry.Dt === "object"));
|
|
84
|
+
}, request(input, settings) {
|
|
76
85
|
if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for position tracking.");
|
|
77
86
|
const result = operation.request(input);
|
|
78
87
|
const url = new URL(result.url);
|
|
@@ -118,6 +127,32 @@ function validProject(project) {
|
|
|
118
127
|
Array.isArray(project.tools) && project.tools.every((tool) => typeof tool?.name === "string" && tool.name.length > 0);
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
// Every V4 answer carries a successful envelope; each operation then checks it answered its own request.
|
|
131
|
+
function v4Result(check, status = 200) {
|
|
132
|
+
return (value, request) => value?.meta?.success === true && value.meta.status_code === status &&
|
|
133
|
+
typeof value.meta.request_id === "string" && value.meta.request_id.length > 0 && value.error === undefined &&
|
|
134
|
+
Boolean(check(value, new URL(request.url), request));
|
|
135
|
+
}
|
|
136
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
137
|
+
// Backlink reports keep the requested scope and, when Semrush supplies them, the requested page bounds.
|
|
138
|
+
function backlinkResult(report) {
|
|
139
|
+
return v4Result((value, url) => {
|
|
140
|
+
const { meta, data } = value;
|
|
141
|
+
if (meta.scope !== undefined && meta.scope !== url.searchParams.get("scope")) return false;
|
|
142
|
+
if (report === "overview") return ["backlinks_count", "domains_count", "urls_count", "score"].every((field) => count(data?.[field]));
|
|
143
|
+
// Some report envelopes omit pagination metadata; validate it when supplied.
|
|
144
|
+
return Array.isArray(data) && data.length <= Number(url.searchParams.get("limit")) && data.every((row) => {
|
|
145
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return false;
|
|
146
|
+
if (report === "links") return typeof row.source_url === "string" && typeof row.target_url === "string" &&
|
|
147
|
+
typeof row.anchor === "string" && typeof row.is_nofollow === "boolean";
|
|
148
|
+
if (report === "ref-domains") return typeof row.domain === "string" && count(row.backlinks_count) && count(row.domain_score);
|
|
149
|
+
return typeof row.anchor === "string" && count(row.backlinks_count) && count(row.domains_count);
|
|
150
|
+
}) && ["limit", "offset"].every((field) => meta[field] === undefined || String(meta[field]) === url.searchParams.get(field)) &&
|
|
151
|
+
(meta.total === undefined || count(meta.total));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const requestedProject = (url) => Number(url.pathname.split("/").at(-1));
|
|
155
|
+
|
|
121
156
|
const semrushProvider = Object.freeze({
|
|
122
157
|
...semrushDefinition, apiOrigins: ["https://api.semrush.com"],
|
|
123
158
|
apiKey: { headers: (key) => {
|
|
@@ -128,7 +163,11 @@ const semrushProvider = Object.freeze({
|
|
|
128
163
|
} },
|
|
129
164
|
checkOperation: "projects.list",
|
|
130
165
|
operations: {
|
|
131
|
-
"tracking.campaigns": { scopes: [],
|
|
166
|
+
"tracking.campaigns": { scopes: [], validateResult: (value, request) => trackingObject(value) &&
|
|
167
|
+
String(value.project_id) === new URL(request.url).pathname.split("/")[4] && Array.isArray(value.campaigns) &&
|
|
168
|
+
value.campaigns.every((campaign) => typeof campaign?.id === "string" && /^\d+(?:_\d+)?$/u.test(campaign.id) &&
|
|
169
|
+
typeof campaign.url === "string" && typeof campaign.device === "string" && typeof campaign.isGathering === "boolean"),
|
|
170
|
+
request(input, settings) {
|
|
132
171
|
const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
133
172
|
if (!settings.v3ApiKeyRef) throw new ConnectorError("connector_binding_missing", "Configure the Semrush V3 key for campaign discovery.");
|
|
134
173
|
return { method: "GET", url: `https://api.semrush.com/management/v1/projects/${projectId}/tracking/campaigns` };
|
|
@@ -159,10 +198,10 @@ const semrushProvider = Object.freeze({
|
|
|
159
198
|
return { ...result, url: url.href };
|
|
160
199
|
}
|
|
161
200
|
},
|
|
162
|
-
"backlinks.overview": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/overview", backlinkTarget),
|
|
163
|
-
"backlinks.list": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/links", backlinkPage),
|
|
164
|
-
"backlinks.referringDomains": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/ref-domains", backlinkPage),
|
|
165
|
-
"backlinks.anchors": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/anchors", backlinkPage),
|
|
201
|
+
"backlinks.overview": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/overview", backlinkTarget, backlinkResult("overview")),
|
|
202
|
+
"backlinks.list": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/links", backlinkPage, backlinkResult("links")),
|
|
203
|
+
"backlinks.referringDomains": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/ref-domains", backlinkPage, backlinkResult("ref-domains")),
|
|
204
|
+
"backlinks.anchors": jsonOperation("https://api.semrush.com/apis/v4/backlinks/v1/anchors", backlinkPage, backlinkResult("anchors")),
|
|
166
205
|
"keywords.metrics": {
|
|
167
206
|
scopes: [],
|
|
168
207
|
request(input) {
|
|
@@ -179,39 +218,57 @@ const semrushProvider = Object.freeze({
|
|
|
179
218
|
}
|
|
180
219
|
url.searchParams.set("format", "json");
|
|
181
220
|
return { ...result, url: url.href };
|
|
182
|
-
}
|
|
221
|
+
},
|
|
222
|
+
validateResult: v4Result(({ meta, data }, url) => meta.keyword === url.searchParams.get("keyword") && meta.country === url.searchParams.get("country") &&
|
|
223
|
+
typeof meta.month === "string" && /^\d{4}-(0[1-9]|1[0-2])$/u.test(meta.month) &&
|
|
224
|
+
(!url.searchParams.has("month") || meta.month === url.searchParams.get("month")) &&
|
|
225
|
+
data !== null && typeof data === "object" && !Array.isArray(data) &&
|
|
226
|
+
["cpc", "number_of_results", "search_volume"].every((key) => typeof data[key] === "string" && /^\d+$/u.test(data[key])) &&
|
|
227
|
+
["competitive_density", "keyword_difficulty"].every((key) => Number.isFinite(data[key])) &&
|
|
228
|
+
["intents", "serp_features"].every((key) => Array.isArray(data[key]) && data[key].every((value) => typeof value === "string")) &&
|
|
229
|
+
Array.isArray(data.trends) && data.trends.every(Number.isFinite))
|
|
183
230
|
},
|
|
184
231
|
"projects.create": jsonOperation(projectsUrl, {
|
|
185
232
|
domain: { type: "string", required: true, minLength: 1, maxLength: 253, pattern: /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/iu },
|
|
186
233
|
project_name: projectName
|
|
187
|
-
},
|
|
234
|
+
}, v4Result(({ data }, url, request) => validProject(data) && data.project_name === request.body.project_name &&
|
|
235
|
+
data.domain.toLowerCase() === request.body.domain.toLowerCase(), 201), "POST"),
|
|
188
236
|
"projects.update": {
|
|
189
237
|
scopes: [],
|
|
190
238
|
request(input) {
|
|
191
239
|
const { projectId, project_name } = validateSchemaPayload({ schema: renameSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
192
240
|
return { method: "PATCH", url: `${projectsUrl}/${projectId}`, body: { project_name } };
|
|
193
|
-
}
|
|
241
|
+
},
|
|
242
|
+
validateResult: v4Result(({ data }, url, request) => validProject(data) && data.project_id === requestedProject(url) &&
|
|
243
|
+
data.project_name === request.body.project_name)
|
|
194
244
|
},
|
|
195
245
|
"projects.list": jsonOperation(projectsUrl, {
|
|
196
246
|
scope: { type: "string", enum: ["OWN", "ALL", "SHARED", "CORPORATE"], defaultTo: "OWN" },
|
|
197
247
|
limit: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
|
|
198
248
|
offset: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER, defaultTo: 0 }
|
|
199
|
-
})
|
|
249
|
+
}, v4Result(({ meta, data }, url) => {
|
|
250
|
+
const limit = Number(url.searchParams.get("limit"));
|
|
251
|
+
return meta.scope === url.searchParams.get("scope") && meta.limit === limit && meta.offset === Number(url.searchParams.get("offset")) &&
|
|
252
|
+
count(meta.total_count) && Array.isArray(data) && data.length <= limit && data.length <= meta.total_count && data.every(validProject);
|
|
253
|
+
})),
|
|
200
254
|
"projects.delete": {
|
|
201
255
|
scopes: [],
|
|
202
256
|
request(input) {
|
|
203
257
|
const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
204
258
|
return { method: "DELETE", url: `${projectsUrl}/${projectId}` };
|
|
205
|
-
}
|
|
259
|
+
},
|
|
260
|
+
validateResult: v4Result(({ data }, url) => Number.isSafeInteger(data?.project_id) && data.project_id === requestedProject(url))
|
|
206
261
|
},
|
|
207
262
|
"projects.get": {
|
|
208
263
|
scopes: [],
|
|
209
264
|
request(input) {
|
|
210
265
|
const { projectId } = validateSchemaPayload({ schema: projectSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
211
266
|
return { method: "GET", url: `${projectsUrl}/${projectId}` };
|
|
212
|
-
}
|
|
267
|
+
},
|
|
268
|
+
validateResult: v4Result(({ data }, url) => validProject(data) && data.project_id === requestedProject(url))
|
|
213
269
|
}
|
|
214
270
|
},
|
|
271
|
+
// V3 reports need their own key and CSV/JSON decoding; V4 maps Semrush's error envelope. Operations validate results.
|
|
215
272
|
async exchange(address, options, { request, fetchImpl, settings, resolveReference }) {
|
|
216
273
|
const path = new URL(address).pathname;
|
|
217
274
|
if (path === "/" || path.startsWith("/management/v1/projects/") || path.startsWith("/reports/v1/projects/")) {
|
|
@@ -238,28 +295,9 @@ const semrushProvider = Object.freeze({
|
|
|
238
295
|
}
|
|
239
296
|
if (text.length > 2_000_000) throw new ConnectorError("connector_response_invalid", "Semrush returned an oversized V3 report.", { statusCode: 502 });
|
|
240
297
|
if (path !== "/") {
|
|
241
|
-
|
|
242
|
-
try { result = JSON.parse(text); } catch {
|
|
298
|
+
try { return JSON.parse(text); } catch {
|
|
243
299
|
throw new ConnectorError("connector_response_invalid", "Semrush returned invalid tracking JSON.", { statusCode: 502 });
|
|
244
300
|
}
|
|
245
|
-
let valid = result && typeof result === "object" && !Array.isArray(result) && result.error === undefined;
|
|
246
|
-
if (path.endsWith("/campaigns")) {
|
|
247
|
-
valid &&= String(result.project_id) === path.split("/")[4] && Array.isArray(result.campaigns) &&
|
|
248
|
-
result.campaigns.every((campaign) => typeof campaign?.id === "string" && /^\d+(?:_\d+)?$/u.test(campaign.id) &&
|
|
249
|
-
typeof campaign.url === "string" && typeof campaign.device === "string" && typeof campaign.isGathering === "boolean");
|
|
250
|
-
} else {
|
|
251
|
-
valid &&= result.data !== null && typeof result.data === "object" &&
|
|
252
|
-
/^(0|[1-9]\d*)$/u.test(String(result.total));
|
|
253
|
-
if (url.searchParams.get("type") === "tracking_campaign_dates") {
|
|
254
|
-
valid &&= Object.values(result.data).every((entry) => /^\d{8}$/u.test(String(entry?.Dt)));
|
|
255
|
-
} else {
|
|
256
|
-
valid &&= String(result.limit) === url.searchParams.get("display_limit") && String(result.offset) === url.searchParams.get("display_offset") &&
|
|
257
|
-
Object.values(result.data).length <= Number(url.searchParams.get("display_limit")) &&
|
|
258
|
-
Object.values(result.data).every((entry) => typeof entry?.Ph === "string" && typeof entry.Pi === "string" && entry.Dt !== null && typeof entry.Dt === "object");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (!valid) throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected tracking data.", { statusCode: 502 });
|
|
262
|
-
return result;
|
|
263
301
|
}
|
|
264
302
|
const columns = url.searchParams.get("type") === "domain_rank" ? domainColumns : keywordColumns;
|
|
265
303
|
const rowLimit = columns === domainColumns ? 1 : Number(url.searchParams.get("display_limit")) - Number(url.searchParams.get("display_offset"));
|
|
@@ -296,63 +334,6 @@ const semrushProvider = Object.freeze({
|
|
|
296
334
|
}
|
|
297
335
|
throw new ConnectorError("connector_provider_failed", "Semrush could not complete this request.", { statusCode: 502 });
|
|
298
336
|
}
|
|
299
|
-
const url = new URL(address);
|
|
300
|
-
const meta = result?.meta;
|
|
301
|
-
const creating = options.method === "POST" && url.pathname === "/apis/v4/projects/v1/projects";
|
|
302
|
-
let valid = meta?.success === true && meta.status_code === (creating ? 201 : 200) &&
|
|
303
|
-
typeof meta.request_id === "string" && meta.request_id.length > 0 && result.error === undefined;
|
|
304
|
-
if (url.pathname === "/apis/v4/keywords/v1/metrics") {
|
|
305
|
-
const data = result?.data;
|
|
306
|
-
valid &&= meta.keyword === url.searchParams.get("keyword") && meta.country === url.searchParams.get("country") &&
|
|
307
|
-
typeof meta.month === "string" && /^\d{4}-(0[1-9]|1[0-2])$/u.test(meta.month) &&
|
|
308
|
-
(!url.searchParams.has("month") || meta.month === url.searchParams.get("month")) &&
|
|
309
|
-
data !== null && typeof data === "object" && !Array.isArray(data) &&
|
|
310
|
-
["cpc", "number_of_results", "search_volume"].every((key) => typeof data[key] === "string" && /^\d+$/u.test(data[key])) &&
|
|
311
|
-
["competitive_density", "keyword_difficulty"].every((key) => Number.isFinite(data[key])) &&
|
|
312
|
-
["intents", "serp_features"].every((key) => Array.isArray(data[key]) && data[key].every((value) => typeof value === "string")) &&
|
|
313
|
-
Array.isArray(data.trends) && data.trends.every(Number.isFinite);
|
|
314
|
-
} else if (url.pathname.startsWith("/apis/v4/backlinks/v1/")) {
|
|
315
|
-
const report = url.pathname.split("/").at(-1);
|
|
316
|
-
const count = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
317
|
-
if (report === "overview") {
|
|
318
|
-
valid &&= ["backlinks_count", "domains_count", "urls_count", "score"].every((field) => count(result.data?.[field]));
|
|
319
|
-
} else {
|
|
320
|
-
valid &&= Array.isArray(result.data) && result.data.length <= Number(url.searchParams.get("limit")) &&
|
|
321
|
-
result.data.every((row) => {
|
|
322
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) return false;
|
|
323
|
-
if (report === "links") return typeof row.source_url === "string" && typeof row.target_url === "string" &&
|
|
324
|
-
typeof row.anchor === "string" && typeof row.is_nofollow === "boolean";
|
|
325
|
-
if (report === "ref-domains") return typeof row.domain === "string" && count(row.backlinks_count) && count(row.domain_score);
|
|
326
|
-
return typeof row.anchor === "string" && count(row.backlinks_count) && count(row.domains_count);
|
|
327
|
-
});
|
|
328
|
-
// Some report envelopes omit pagination metadata; validate it when supplied.
|
|
329
|
-
for (const field of ["limit", "offset"]) {
|
|
330
|
-
if (meta?.[field] !== undefined) valid &&= String(meta[field]) === url.searchParams.get(field);
|
|
331
|
-
}
|
|
332
|
-
if (meta?.total !== undefined) valid &&= count(meta.total);
|
|
333
|
-
}
|
|
334
|
-
if (meta?.scope !== undefined) valid &&= meta.scope === url.searchParams.get("scope");
|
|
335
|
-
} else if (creating) {
|
|
336
|
-
const submitted = options.body;
|
|
337
|
-
valid &&= validProject(result.data) && result.data.project_name === submitted.project_name &&
|
|
338
|
-
result.data.domain.toLowerCase() === submitted.domain.toLowerCase();
|
|
339
|
-
} else if (url.pathname === "/apis/v4/projects/v1/projects") {
|
|
340
|
-
const limit = Number(url.searchParams.get("limit"));
|
|
341
|
-
valid &&= meta.scope === url.searchParams.get("scope") && meta.limit === limit &&
|
|
342
|
-
meta.offset === Number(url.searchParams.get("offset")) &&
|
|
343
|
-
Number.isSafeInteger(meta.total_count) && meta.total_count >= 0 &&
|
|
344
|
-
Array.isArray(result.data) && result.data.length <= limit && result.data.length <= meta.total_count &&
|
|
345
|
-
result.data.every(validProject);
|
|
346
|
-
} else if (options.method === "DELETE") {
|
|
347
|
-
valid &&= Number.isSafeInteger(result.data?.project_id) &&
|
|
348
|
-
result.data.project_id === Number(url.pathname.split("/").at(-1));
|
|
349
|
-
} else {
|
|
350
|
-
valid &&= validProject(result.data) && result.data.project_id === Number(url.pathname.split("/").at(-1));
|
|
351
|
-
if (options.method === "PATCH") valid &&= result.data.project_name === options.body.project_name;
|
|
352
|
-
}
|
|
353
|
-
if (!valid) {
|
|
354
|
-
throw new ConnectorError("connector_response_invalid", "Semrush returned unexpected report or project data.", { statusCode: 502 });
|
|
355
|
-
}
|
|
356
337
|
return result;
|
|
357
338
|
}
|
|
358
339
|
});
|