@jskit-ai/connectors-catalog 0.1.1
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 +192 -0
- package/docs/ai-upstream-notices.md +51 -0
- package/docs/ai.md +191 -0
- package/docs/airtable.md +130 -0
- package/docs/algolia.md +190 -0
- package/docs/amazon-redshift.md +229 -0
- package/docs/amplitude.md +210 -0
- package/docs/apify.md +162 -0
- package/docs/apollo-io.md +200 -0
- package/docs/asana.md +126 -0
- package/docs/ashby.md +188 -0
- package/docs/atlassian.md +224 -0
- package/docs/attention.md +162 -0
- package/docs/aws-athena.md +150 -0
- package/docs/aws-credentials.md +75 -0
- package/docs/aws-s3.md +193 -0
- package/docs/bigquery.md +146 -0
- package/docs/brevo.md +122 -0
- package/docs/calendly.md +158 -0
- package/docs/canva.md +191 -0
- package/docs/chargebee.md +192 -0
- package/docs/clay.md +194 -0
- package/docs/clickhouse.md +242 -0
- package/docs/confidence-exp.md +74 -0
- package/docs/confidence-flags.md +75 -0
- package/docs/confidence-oauth.md +137 -0
- package/docs/contentful.md +123 -0
- package/docs/databricks.md +180 -0
- package/docs/dbt-semantic-layer.md +175 -0
- package/docs/elevenlabs.md +102 -0
- package/docs/figma.md +132 -0
- package/docs/firebase-cloud-messaging.md +262 -0
- package/docs/firecrawl.md +137 -0
- package/docs/fireflies.md +95 -0
- package/docs/fireworks-ai.md +53 -0
- package/docs/gatewayapi.md +113 -0
- package/docs/gemini-enterprise.md +224 -0
- package/docs/github-api.md +133 -0
- package/docs/gitlab-api.md +138 -0
- package/docs/gmail.md +133 -0
- package/docs/gong.md +205 -0
- package/docs/google-ads.md +156 -0
- package/docs/google-analytics.md +202 -0
- package/docs/google-docs.md +105 -0
- package/docs/google-drive.md +108 -0
- package/docs/google-maps-platform.md +208 -0
- package/docs/google-oauth.md +123 -0
- package/docs/google-search-console.md +124 -0
- package/docs/google-sheets.md +94 -0
- package/docs/google-slides.md +95 -0
- package/docs/granola.md +178 -0
- package/docs/hex.md +161 -0
- package/docs/heygen.md +124 -0
- package/docs/hubspot.md +142 -0
- package/docs/incident-io.md +118 -0
- package/docs/inngest.md +178 -0
- package/docs/klipy.md +149 -0
- package/docs/lexware.md +144 -0
- package/docs/lightspeed.md +193 -0
- package/docs/linear.md +137 -0
- package/docs/linkedin.md +196 -0
- package/docs/logo-dev.md +152 -0
- package/docs/mailgun.md +153 -0
- package/docs/mapbox.md +174 -0
- package/docs/microsoft-excel.md +97 -0
- package/docs/microsoft-fabric.md +249 -0
- package/docs/microsoft-oauth.md +143 -0
- package/docs/microsoft-onedrive.md +76 -0
- package/docs/microsoft-onenote.md +75 -0
- package/docs/microsoft-outlook.md +90 -0
- package/docs/microsoft-powerpoint.md +119 -0
- package/docs/microsoft-sharepoint.md +75 -0
- package/docs/microsoft-teams.md +72 -0
- package/docs/microsoft-word.md +119 -0
- package/docs/miro.md +117 -0
- package/docs/n8n.md +169 -0
- package/docs/notion.md +137 -0
- package/docs/oura.md +161 -0
- package/docs/paddle.md +168 -0
- package/docs/perplexity.md +85 -0
- package/docs/pipedrive.md +120 -0
- package/docs/polar.md +63 -0
- package/docs/posthog.md +227 -0
- package/docs/prestashop.md +151 -0
- package/docs/replicate.md +60 -0
- package/docs/resend.md +153 -0
- package/docs/salesforce.md +234 -0
- package/docs/sanity.md +122 -0
- package/docs/semrush.md +276 -0
- package/docs/sentry.md +112 -0
- package/docs/sevdesk.md +107 -0
- package/docs/shopify.md +206 -0
- package/docs/slack.md +257 -0
- package/docs/snowflake.md +271 -0
- package/docs/storyblok.md +130 -0
- package/docs/stripe.md +77 -0
- package/docs/supabase.md +51 -0
- package/docs/tally.md +105 -0
- package/docs/telegram.md +165 -0
- package/docs/tiktok.md +201 -0
- package/docs/twilio.md +284 -0
- package/docs/twitch.md +428 -0
- package/docs/wave.md +440 -0
- package/docs/wix.md +1249 -0
- package/docs/wiz.md +177 -0
- package/docs/woocommerce.md +400 -0
- package/docs/wordpress-com.md +358 -0
- package/docs/wordpress-self-hosted.md +252 -0
- package/docs/workday.md +292 -0
- package/docs/x-twitter.md +188 -0
- package/docs/xero.md +429 -0
- package/docs/zoho-books.md +214 -0
- package/docs/zoho-crm.md +192 -0
- package/package.json +153 -0
- package/patterns/ai-connections/PATTERN.md +84 -0
- package/patterns/ai-connections/example/ai-model.js +11 -0
- package/patterns/ai-connections/example/perplexity-answer.js +29 -0
- package/patterns/api-key-connection/PATTERN.md +368 -0
- package/patterns/api-key-connection/example/connections.js +18 -0
- package/patterns/api-key-connection/example/integrations.json +19 -0
- package/patterns/assistant-mcp/PATTERN.md +110 -0
- package/patterns/assistant-mcp/example/integrations.json +13 -0
- package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
- package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
- package/patterns/aws-storage-queries/PATTERN.md +169 -0
- package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
- package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
- package/patterns/aws-storage-queries/example/formats/package.json +12 -0
- package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
- package/patterns/aws-storage-queries/example/integrations.json +38 -0
- package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
- package/patterns/event-delivery/PATTERN.md +151 -0
- package/patterns/event-delivery/example/integrations.json +17 -0
- package/patterns/firebase-messaging/PATTERN.md +156 -0
- package/patterns/firebase-messaging/example/server/notifications.js +23 -0
- package/patterns/google-ads-search/PATTERN.md +92 -0
- package/patterns/google-ads-search/example/ads-setup.js +25 -0
- package/patterns/oauth-connection/PATTERN.md +724 -0
- package/patterns/oauth-connection/example/integrations.json +20 -0
- package/patterns/paddle-catalogue/PATTERN.md +78 -0
- package/patterns/paddle-catalogue/example/create-products.js +38 -0
- package/patterns/public-image/PATTERN.md +84 -0
- package/patterns/public-image/example/integrations.json +13 -0
- package/patterns/public-image/example/logo-url.js +16 -0
- package/patterns/redshift-queries/PATTERN.md +150 -0
- package/patterns/redshift-queries/example/integrations.json +33 -0
- package/patterns/source-scanning/PATTERN.md +83 -0
- package/patterns/source-scanning/example/source-scanner.js +13 -0
- package/src/client/firebase-cloud-messaging.js +10 -0
- package/src/client/google-analytics.js +72 -0
- package/src/client/google-maps-platform.js +39 -0
- package/src/client/logo-dev.js +36 -0
- package/src/server/ai.js +46 -0
- package/src/server/airtable.js +67 -0
- package/src/server/algolia.js +63 -0
- package/src/server/amazon-redshift.js +96 -0
- package/src/server/amplitude.js +31 -0
- package/src/server/apify.js +68 -0
- package/src/server/apollo-io.js +85 -0
- package/src/server/asana.js +64 -0
- package/src/server/ashby.js +61 -0
- package/src/server/atlassian.js +32 -0
- package/src/server/attention.js +93 -0
- package/src/server/aws-athena.js +82 -0
- package/src/server/aws-s3.js +49 -0
- package/src/server/awsTransport.js +75 -0
- package/src/server/bigquery.js +97 -0
- package/src/server/brevo.js +97 -0
- package/src/server/calendly.js +77 -0
- package/src/server/canva.js +13 -0
- package/src/server/chargebee.js +88 -0
- package/src/server/clay.js +78 -0
- package/src/server/clickhouse.js +66 -0
- package/src/server/confidence.js +29 -0
- package/src/server/contentful.js +40 -0
- package/src/server/databricks.js +55 -0
- package/src/server/dbt-semantic-layer.js +95 -0
- package/src/server/elevenlabs.js +81 -0
- package/src/server/figma.js +24 -0
- package/src/server/firebase-cloud-messaging.js +111 -0
- package/src/server/firecrawl.js +70 -0
- package/src/server/fireflies.js +31 -0
- package/src/server/fireworks-ai.js +16 -0
- package/src/server/gatewayapi.js +48 -0
- package/src/server/gemini-enterprise.js +47 -0
- package/src/server/github-api.js +90 -0
- package/src/server/gitlab-api.js +67 -0
- package/src/server/gmail.js +60 -0
- package/src/server/gong.js +52 -0
- package/src/server/google-ads-search.js +134 -0
- package/src/server/google-ads.js +80 -0
- package/src/server/google-docs.js +36 -0
- package/src/server/google-drive.js +71 -0
- package/src/server/google-maps-platform.js +90 -0
- package/src/server/google-search-console.js +31 -0
- package/src/server/google-sheets.js +60 -0
- package/src/server/google-slides.js +36 -0
- package/src/server/google.js +61 -0
- package/src/server/granola.js +115 -0
- package/src/server/graphqlOperation.js +15 -0
- package/src/server/hex.js +29 -0
- package/src/server/heygen.js +54 -0
- package/src/server/hubspot.js +85 -0
- package/src/server/incident-io.js +68 -0
- package/src/server/inngest.js +86 -0
- package/src/server/jsonOperation.js +34 -0
- package/src/server/klipy.js +34 -0
- package/src/server/lexware.js +59 -0
- package/src/server/lightspeed.js +82 -0
- package/src/server/linear.js +52 -0
- package/src/server/linkedin.js +74 -0
- package/src/server/mailgun.js +76 -0
- package/src/server/mapbox.js +46 -0
- package/src/server/mcpRegistration.js +41 -0
- package/src/server/mcpTransport.js +60 -0
- package/src/server/microsoft-excel.js +67 -0
- package/src/server/microsoft-fabric.js +81 -0
- package/src/server/microsoft-onedrive.js +58 -0
- package/src/server/microsoft-onenote.js +52 -0
- package/src/server/microsoft-outlook.js +58 -0
- package/src/server/microsoft-powerpoint.js +5 -0
- package/src/server/microsoft-sharepoint.js +46 -0
- package/src/server/microsoft-teams.js +29 -0
- package/src/server/microsoft-word.js +5 -0
- package/src/server/microsoft.js +88 -0
- package/src/server/miro.js +29 -0
- package/src/server/n8n.js +78 -0
- package/src/server/notion.js +76 -0
- package/src/server/oura.js +48 -0
- package/src/server/paddle.js +83 -0
- package/src/server/perplexity.js +15 -0
- package/src/server/pipedrive.js +95 -0
- package/src/server/polar.js +20 -0
- package/src/server/posthog.js +44 -0
- package/src/server/prestashop.js +42 -0
- package/src/server/replicate.js +42 -0
- package/src/server/resend.js +93 -0
- package/src/server/salesforce.js +84 -0
- package/src/server/sanity.js +31 -0
- package/src/server/semrush.js +359 -0
- package/src/server/sentry.js +30 -0
- package/src/server/sevdesk.js +20 -0
- package/src/server/shopify.js +99 -0
- package/src/server/slack.js +143 -0
- package/src/server/snowflake.js +139 -0
- package/src/server/storyblok.js +49 -0
- package/src/server/stripe.js +13 -0
- package/src/server/supabase.js +13 -0
- package/src/server/tally.js +78 -0
- package/src/server/telegram.js +55 -0
- package/src/server/tiktok.js +86 -0
- package/src/server/twilio.js +90 -0
- package/src/server/twitch.js +409 -0
- package/src/server/wave.js +520 -0
- package/src/server/wix.js +1048 -0
- package/src/server/wiz.js +117 -0
- package/src/server/woocommerce.js +412 -0
- package/src/server/wordpress-com.js +236 -0
- package/src/server/wordpress-self-hosted.js +134 -0
- package/src/server/workday.js +136 -0
- package/src/server/x-twitter.js +110 -0
- package/src/server/xero.js +397 -0
- package/src/server/zoho-books.js +90 -0
- package/src/server/zoho-crm.js +89 -0
- package/src/server/zohoOAuth.js +21 -0
- package/src/shared/ai-models.json +8049 -0
- package/src/shared/ai.js +63 -0
- package/src/shared/algolia.js +29 -0
- package/src/shared/amazon-redshift.js +51 -0
- package/src/shared/amplitude.js +28 -0
- package/src/shared/atlassian.js +43 -0
- package/src/shared/aws.js +72 -0
- package/src/shared/canva.js +66 -0
- package/src/shared/chargebee.js +26 -0
- package/src/shared/clickhouse.js +38 -0
- package/src/shared/confidence.js +28 -0
- package/src/shared/databricks.js +33 -0
- package/src/shared/dbt-semantic-layer.js +32 -0
- package/src/shared/definitions.js +140 -0
- package/src/shared/figma.js +21 -0
- package/src/shared/firebase-cloud-messaging.js +45 -0
- package/src/shared/gemini-enterprise.js +33 -0
- package/src/shared/gong.js +33 -0
- package/src/shared/google-ads-search.js +31 -0
- package/src/shared/google-ads.js +28 -0
- package/src/shared/google-maps-platform.js +28 -0
- package/src/shared/google.js +133 -0
- package/src/shared/granola.js +25 -0
- package/src/shared/hex.js +28 -0
- package/src/shared/inngest.js +35 -0
- package/src/shared/lightspeed.js +75 -0
- package/src/shared/linkedin.js +27 -0
- package/src/shared/logo-dev.js +21 -0
- package/src/shared/mapbox.js +38 -0
- package/src/shared/mcp.js +92 -0
- package/src/shared/microsoft-fabric.js +47 -0
- package/src/shared/microsoft.js +231 -0
- package/src/shared/miro.js +27 -0
- package/src/shared/oura.js +23 -0
- package/src/shared/posthog.js +33 -0
- package/src/shared/prestashop.js +23 -0
- package/src/shared/salesforce.js +36 -0
- package/src/shared/semrush.js +24 -0
- package/src/shared/shopify.js +51 -0
- package/src/shared/siteUrl.js +12 -0
- package/src/shared/slack.js +88 -0
- package/src/shared/snowflake.js +55 -0
- package/src/shared/tiktok.js +22 -0
- package/src/shared/tokens.js +747 -0
- package/src/shared/twilio.js +41 -0
- package/src/shared/twitch.js +49 -0
- package/src/shared/wave.js +52 -0
- package/src/shared/wix.js +40 -0
- package/src/shared/wiz.js +41 -0
- package/src/shared/wordpress-com.js +30 -0
- package/src/shared/wordpress.js +69 -0
- package/src/shared/workday.js +50 -0
- package/src/shared/x-twitter.js +19 -0
- package/src/shared/xero.js +46 -0
- package/src/shared/zoho-books.js +39 -0
- package/src/shared/zoho-crm.js +43 -0
- package/src/shared/zoho-regions.js +12 -0
- package/test/ai.test.js +136 -0
- package/test/airtable.test.js +125 -0
- package/test/algolia.test.js +207 -0
- package/test/amazon-redshift.test.js +272 -0
- package/test/amplitude.test.js +239 -0
- package/test/apify.test.js +93 -0
- package/test/apollo-io.test.js +125 -0
- package/test/asana.test.js +99 -0
- package/test/ashby.test.js +97 -0
- package/test/atlassian.test.js +249 -0
- package/test/attention.test.js +79 -0
- package/test/aws.test.js +405 -0
- package/test/brevo.test.js +71 -0
- package/test/calendly.test.js +136 -0
- package/test/canva.test.js +258 -0
- package/test/chargebee.test.js +195 -0
- package/test/clickhouse.test.js +278 -0
- package/test/confidence.test.js +205 -0
- package/test/databricks.test.js +253 -0
- package/test/dbt-semantic-layer.test.js +221 -0
- package/test/designMcp.test.js +336 -0
- package/test/elevenlabs.test.js +52 -0
- package/test/firebase-cloud-messaging.test.js +242 -0
- package/test/firecrawl.test.js +66 -0
- package/test/gemini-enterprise.test.js +218 -0
- package/test/github-api.test.js +167 -0
- package/test/gitlab-api.test.js +160 -0
- package/test/gong.test.js +192 -0
- package/test/google-ads.test.js +350 -0
- package/test/google-maps-platform.test.js +191 -0
- package/test/google.test.js +710 -0
- package/test/granola.test.js +240 -0
- package/test/hex.test.js +253 -0
- package/test/heygen.test.js +121 -0
- package/test/hubspot.test.js +103 -0
- package/test/inngest.test.js +219 -0
- package/test/lightspeed.test.js +250 -0
- package/test/linear.test.js +146 -0
- package/test/linkedin.test.js +243 -0
- package/test/logo-dev.test.js +79 -0
- package/test/mailgun.test.js +107 -0
- package/test/mapbox.test.js +181 -0
- package/test/mcp.test.js +227 -0
- package/test/microsoft-fabric.test.js +258 -0
- package/test/microsoft.test.js +554 -0
- package/test/n8nOAuth.test.js +188 -0
- package/test/notionOAuth.test.js +183 -0
- package/test/oura.test.js +211 -0
- package/test/paddleCatalogue.test.js +51 -0
- package/test/paddlePermissions.test.js +101 -0
- package/test/perplexityComposition.test.js +39 -0
- package/test/pipedriveOAuth.test.js +140 -0
- package/test/posthog.test.js +200 -0
- package/test/prestashop.test.js +207 -0
- package/test/resend.test.js +163 -0
- package/test/salesforce.test.js +276 -0
- package/test/semrush.test.js +479 -0
- package/test/sentry.test.js +35 -0
- package/test/shopify.test.js +247 -0
- package/test/slack.test.js +320 -0
- package/test/snowflake.test.js +391 -0
- package/test/tally.test.js +75 -0
- package/test/telegram.test.js +58 -0
- package/test/tiktok.test.js +243 -0
- package/test/tokens.test.js +1079 -0
- package/test/twilio.test.js +221 -0
- package/test/twitch.test.js +573 -0
- package/test/wave.test.js +678 -0
- package/test/wix.test.js +1099 -0
- package/test/wiz.test.js +185 -0
- package/test/wordpress-com.test.js +469 -0
- package/test/wordpress.test.js +660 -0
- package/test/workday.test.js +314 -0
- package/test/x-twitter.test.js +256 -0
- package/test/xero.test.js +700 -0
- package/test/zoho-books.test.js +253 -0
- package/test/zoho-crm.test.js +223 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, open, realpath, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
7
|
+
import { validateIntegrationConfiguration } from "@jskit-ai/connectors-core/shared/configuration";
|
|
8
|
+
import { wizDefinition } from "../shared/wiz.js";
|
|
9
|
+
|
|
10
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
const failure = (code, message, statusCode = 502) => new ConnectorError(code, message, { statusCode });
|
|
12
|
+
|
|
13
|
+
// This process belongs to the scanner, not the application's chat/agent runtime.
|
|
14
|
+
async function runCli(executable, args, options, signal, timeoutMs) {
|
|
15
|
+
if (signal?.aborted) throw failure("connector_cancelled", "The Wiz scan was cancelled.", 499);
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let child;
|
|
18
|
+
try { child = spawn(executable, args, { ...options, shell: false, detached: true, stdio: "ignore" }); }
|
|
19
|
+
catch { reject(failure("connector_runner_unavailable", "The Wiz CLI could not be started.")); return; }
|
|
20
|
+
let cancelled = false; let timedOut = false; let startFailed = false;
|
|
21
|
+
const kill = () => {
|
|
22
|
+
if (!child.pid) return;
|
|
23
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch (error) {
|
|
24
|
+
if (error.code !== "ESRCH") child.kill("SIGKILL");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const abort = () => { cancelled = true; kill(); };
|
|
28
|
+
const timer = setTimeout(() => { timedOut = true; kill(); }, timeoutMs);
|
|
29
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
30
|
+
if (signal?.aborted) abort();
|
|
31
|
+
child.once("error", () => { startFailed = true; });
|
|
32
|
+
// Wait for close before deleting reports/cache, including after cancellation.
|
|
33
|
+
child.once("close", (exitCode) => {
|
|
34
|
+
clearTimeout(timer); signal?.removeEventListener("abort", abort); kill();
|
|
35
|
+
if (cancelled) reject(failure("connector_cancelled", "The Wiz scan was cancelled.", 499));
|
|
36
|
+
else if (timedOut) reject(failure("connector_timeout", "The Wiz scan exceeded its time limit.", 504));
|
|
37
|
+
else if (startFailed) reject(failure("connector_runner_unavailable", "The Wiz CLI could not be started."));
|
|
38
|
+
else resolve(exitCode);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createWizScanner({ configuration, authorize, resolveReference, resolveScanTarget, executable,
|
|
44
|
+
environment = { PATH: process.env.PATH || "/usr/bin:/bin" }, timeoutMs = 600_000, maxReportBytes = 16 * 1024 * 1024 }) {
|
|
45
|
+
if (!["linux", "darwin"].includes(process.platform)) throw new TypeError("Run Wiz in a Linux or macOS scan worker.");
|
|
46
|
+
if (typeof executable !== "string" || !path.isAbsolute(executable) || executable.includes("\0")) throw new TypeError("Supply the installed Wiz CLI's absolute path.");
|
|
47
|
+
if (![authorize, resolveReference, resolveScanTarget].every((value) => typeof value === "function")) throw new TypeError("Wiz requires host authorization, secret resolution and source resolution.");
|
|
48
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000) throw new TypeError("Use a scan timeout between 1 and 3600000 milliseconds.");
|
|
49
|
+
if (!Number.isSafeInteger(maxReportBytes) || maxReportBytes < 1 || maxReportBytes > 64 * 1024 * 1024) throw new TypeError("Use a positive report limit of at most 64 MiB.");
|
|
50
|
+
const env = {};
|
|
51
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
52
|
+
if (!["PATH", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR"].includes(key) || typeof value !== "string" || value.includes("\0")) {
|
|
53
|
+
throw new TypeError("Supply only the scan worker's path, proxy and CA environment.");
|
|
54
|
+
}
|
|
55
|
+
env[key] = value;
|
|
56
|
+
}
|
|
57
|
+
const config = validateIntegrationConfiguration(configuration, { providers: [wizDefinition], allowUnknownProviders: true });
|
|
58
|
+
|
|
59
|
+
async function scan({ context, integrationId, signal }) {
|
|
60
|
+
const integration = Object.hasOwn(config.integrations, integrationId) ? config.integrations[integrationId] : null;
|
|
61
|
+
if (integration?.provider !== "wiz") throw failure("connector_not_found", "This Wiz integration is not configured.", 404);
|
|
62
|
+
const owner = await authorize(context, { integrationId, operation: "source.scan", accountMode: "shared" });
|
|
63
|
+
if (!owner?.applicationId || !owner?.subjectId) throw failure("connector_access_denied", "Access to this source scan was denied.", 403);
|
|
64
|
+
const registration = config.registrations[integration.authentication.registrationRef];
|
|
65
|
+
if (integration.settings.tokenUrl !== "https://auth.app.wiz.io/oauth/token") {
|
|
66
|
+
throw failure("connector_authentication_unsupported", "Auth0 scanning requires a verified compatible Wiz runner.", 501);
|
|
67
|
+
}
|
|
68
|
+
if (signal?.aborted) throw failure("connector_cancelled", "The Wiz scan was cancelled.", 499);
|
|
69
|
+
// The browser supplies no filesystem path. The trusted host selects an immutable
|
|
70
|
+
// source snapshot and revision after checking the requesting principal's access.
|
|
71
|
+
const target = await resolveScanTarget({ ...owner }, { integrationId, signal });
|
|
72
|
+
if (!object(target) || typeof target.directory !== "string" || !path.isAbsolute(target.directory) ||
|
|
73
|
+
target.directory.includes("\0") || typeof target.revision !== "string" || !target.revision.trim() || target.revision.length > 512) {
|
|
74
|
+
throw failure("connector_source_invalid", "The host must supply an absolute source directory and revision.", 422);
|
|
75
|
+
}
|
|
76
|
+
let directory;
|
|
77
|
+
try { directory = await realpath(target.directory); if (!(await stat(directory)).isDirectory()) throw new Error(); }
|
|
78
|
+
catch { throw failure("connector_source_unavailable", "The authorised source directory is unavailable.", 422); }
|
|
79
|
+
let secret;
|
|
80
|
+
try { secret = await resolveReference(registration.clientSecretRef, { ...owner }); }
|
|
81
|
+
catch { throw failure("connector_binding_missing", "The Wiz client-secret reference is unavailable.", 422); }
|
|
82
|
+
if (typeof secret !== "string" || !secret.trim() || secret.includes("\0") || secret.length > 16384) {
|
|
83
|
+
throw failure("connector_binding_missing", "The Wiz client-secret reference is invalid.", 422);
|
|
84
|
+
}
|
|
85
|
+
const temporary = await mkdtemp(path.join(tmpdir(), "jskit-wiz-"));
|
|
86
|
+
try {
|
|
87
|
+
const reportPath = path.join(temporary, "report.json");
|
|
88
|
+
const args = ["scan", "dir", directory, "--assist-migration", "--no-style", "--no-color", "--no-telemetry", "--stdout=human",
|
|
89
|
+
`--json-output-file=${reportPath}`, `--timeout=${Math.ceil(timeoutMs / 1000)}s`, `--by-policy-hits=${integration.settings.byPolicyHits}`];
|
|
90
|
+
if (integration.settings.policies) args.push(`--policies=${integration.settings.policies.split(",").map((item) => item.trim()).join(",")}`);
|
|
91
|
+
const exitCode = await runCli(executable, args, { cwd: temporary,
|
|
92
|
+
env: { ...env, HOME: temporary, TMPDIR: temporary, WIZ_DIR: temporary, WIZ_CLIENT_ID: registration.clientId, WIZ_CLIENT_SECRET: secret }
|
|
93
|
+
}, signal, timeoutMs);
|
|
94
|
+
if (exitCode === 3) throw failure("connector_authentication_failed", "Wiz rejected the service-account credentials.", 401);
|
|
95
|
+
if (exitCode !== 0 && exitCode !== 4) throw failure("connector_scan_failed", "Wiz could not complete the scan. Check the runner, policies and tenant access.");
|
|
96
|
+
let report;
|
|
97
|
+
try {
|
|
98
|
+
const file = await open(reportPath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
99
|
+
try {
|
|
100
|
+
const info = await file.stat();
|
|
101
|
+
if (!info.isFile() || info.size > maxReportBytes) throw new Error();
|
|
102
|
+
report = JSON.parse(await file.readFile("utf8"));
|
|
103
|
+
} finally { await file.close(); }
|
|
104
|
+
} catch { throw failure("connector_response_invalid", "Wiz did not produce a readable report within the configured size limit."); }
|
|
105
|
+
const verdict = report?.status?.verdict;
|
|
106
|
+
if (!object(report) || report.status?.state !== "SUCCESS" || !object(report.result) || !["PASSED_BY_POLICY", "WARN_BY_POLICY", "FAILED_BY_POLICY"].includes(verdict) ||
|
|
107
|
+
(exitCode === 4 && verdict !== "FAILED_BY_POLICY")) {
|
|
108
|
+
throw failure("connector_response_invalid", "Wiz returned an unsupported or inconsistent scan report.");
|
|
109
|
+
}
|
|
110
|
+
return { provider: "wiz", integrationId, revision: target.revision,
|
|
111
|
+
policyStatus: verdict === "FAILED_BY_POLICY" ? "blocked" : verdict === "WARN_BY_POLICY" ? "warning" : "passed", report };
|
|
112
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze({ scan });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { createWizScanner };
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
4
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
5
|
+
import { woocommerceDefinition } from "../shared/wordpress.js";
|
|
6
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
7
|
+
|
|
8
|
+
const baseUrl = (settings) => `${settings.siteUrl.replace(/\/+$/u, "")}/wp-json/wc/v3`;
|
|
9
|
+
const pagination = {
|
|
10
|
+
page: { type: "integer", min: 1, max: 100000, defaultTo: 1 },
|
|
11
|
+
per_page: { type: "integer", min: 1, max: 100, defaultTo: 10 },
|
|
12
|
+
search: { type: "string", minLength: 1, maxLength: 500 }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const validRecord = {
|
|
16
|
+
products: value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.name === "string",
|
|
17
|
+
orders: value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.status === "string" && typeof value.total === "string",
|
|
18
|
+
customers: value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.email === "string",
|
|
19
|
+
coupons: value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.code === "string" && typeof value.amount === "string"
|
|
20
|
+
};
|
|
21
|
+
function readRecord(resource) {
|
|
22
|
+
const schema = createSchema({ id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } });
|
|
23
|
+
return {
|
|
24
|
+
scopes: [],
|
|
25
|
+
request(input, settings) {
|
|
26
|
+
const { id } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
27
|
+
return { method: "GET", url: `${baseUrl(settings)}/${resource}/${id}` };
|
|
28
|
+
},
|
|
29
|
+
validateResult: validRecord[resource]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deleteRecord(resource) {
|
|
34
|
+
const permanentOnly = resource === "customers" || resource === "variations";
|
|
35
|
+
const schema = createSchema({
|
|
36
|
+
id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
37
|
+
force: { type: "boolean", required: true },
|
|
38
|
+
...(resource === "variations" ? { product_id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } } : {}),
|
|
39
|
+
...(resource === "customers" ? { reassign: { type: "integer", min: 1, max: Number.MAX_SAFE_INTEGER } } : {})
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
scopes: [],
|
|
43
|
+
request(input, settings) {
|
|
44
|
+
if (typeof input?.force !== "boolean") throw new ConnectorError("connector_input_invalid", "Choose explicitly whether to permanently delete.", { statusCode: 422 });
|
|
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 });
|
|
48
|
+
const path = resource === "variations" ? `products/${product_id}/variations` : resource;
|
|
49
|
+
const url = new URL(`${baseUrl(settings)}/${path}/${id}`);
|
|
50
|
+
url.searchParams.set("force", String(force));
|
|
51
|
+
if (reassign !== undefined) url.searchParams.set("reassign", String(reassign));
|
|
52
|
+
return { method: "DELETE", url: url.href };
|
|
53
|
+
},
|
|
54
|
+
validateResult: resource === "variations" ? value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.price === "string" : validRecord[resource]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function categoryOperation(action) {
|
|
59
|
+
const writing = action === "create" || action === "update";
|
|
60
|
+
const schema = createSchema({
|
|
61
|
+
...(action === "list" ? { ...pagination, hide_empty: { type: "boolean" }, parent: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER } } :
|
|
62
|
+
action === "create" ? {} : { id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } }),
|
|
63
|
+
...(writing ? {
|
|
64
|
+
name: { type: "string", required: action === "create", minLength: 1, maxLength: 200 },
|
|
65
|
+
slug: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 10000 },
|
|
66
|
+
parent: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER },
|
|
67
|
+
display: { type: "string", enum: ["default", "products", "subcategories", "both"] },
|
|
68
|
+
menu_order: { type: "integer", min: 0, max: 2147483647 },
|
|
69
|
+
image: { type: "object", schema: createSchema({ id: { type: "integer", required: true, min: 0, max: Number.MAX_SAFE_INTEGER }, alt: { type: "string", maxLength: 1000 } }) }
|
|
70
|
+
} : {}),
|
|
71
|
+
...(action === "delete" ? { force: { type: "boolean", required: true } } : {})
|
|
72
|
+
});
|
|
73
|
+
const valid = value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.name === "string" && Number.isSafeInteger(value.parent) && value.parent >= 0;
|
|
74
|
+
return {
|
|
75
|
+
scopes: [],
|
|
76
|
+
request(input, settings) {
|
|
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 });
|
|
80
|
+
const url = new URL(`${baseUrl(settings)}/products/categories${id ? `/${id}` : ""}`);
|
|
81
|
+
if (writing) {
|
|
82
|
+
if (!Object.keys(fields).length) throw new ConnectorError("connector_input_invalid", "Supply category changes.", { statusCode: 422 });
|
|
83
|
+
return { method: action === "create" ? "POST" : "PUT", url: url.href, body: fields };
|
|
84
|
+
}
|
|
85
|
+
for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, String(value));
|
|
86
|
+
return { method: action === "delete" ? "DELETE" : "GET", url: url.href };
|
|
87
|
+
},
|
|
88
|
+
validateResult: action === "list" ? value => Array.isArray(value) && value.length <= 100 && value.every(valid) : valid
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function couponWrite(update) {
|
|
93
|
+
const money = { type: "string", maxLength: 40, validator: value => /^\d+(?:\.\d+)?$/u.test(value) || "Use a non-negative decimal amount string." };
|
|
94
|
+
const ids = { type: "array", validator: values => values.length <= 1000 || "Use at most 1000 IDs.", items: { type: "integer", min: 1, max: Number.MAX_SAFE_INTEGER } };
|
|
95
|
+
const schema = createSchema({
|
|
96
|
+
...(update ? { id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } } : {}),
|
|
97
|
+
code: { type: "string", required: !update, minLength: 1, maxLength: 200 },
|
|
98
|
+
discount_type: { type: "string", enum: ["percent", "fixed_cart", "fixed_product"] },
|
|
99
|
+
amount: money, minimum_amount: money, maximum_amount: money,
|
|
100
|
+
description: { type: "string", maxLength: 10000 }, individual_use: { type: "boolean" },
|
|
101
|
+
product_ids: ids, excluded_product_ids: ids, product_categories: ids, excluded_product_categories: ids,
|
|
102
|
+
usage_limit: { type: "integer", min: 0, max: 2147483647 },
|
|
103
|
+
usage_limit_per_user: { type: "integer", min: 0, max: 2147483647 },
|
|
104
|
+
limit_usage_to_x_items: { type: "integer", min: 0, max: 2147483647 },
|
|
105
|
+
free_shipping: { type: "boolean" }, exclude_sale_items: { type: "boolean" },
|
|
106
|
+
email_restrictions: { type: "array", validator: values => values.length <= 1000 || "Use at most 1000 email restrictions.", items: { type: "string", minLength: 1, maxLength: 320 } }
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
scopes: [],
|
|
110
|
+
request(input, settings) {
|
|
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 });
|
|
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 });
|
|
115
|
+
}
|
|
116
|
+
return { method: update ? "PUT" : "POST", url: `${baseUrl(settings)}/coupons${update ? `/${id}` : ""}`, body };
|
|
117
|
+
}, validateResult: validRecord.coupons
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function productWrite(update) {
|
|
122
|
+
const price = { type: "string", maxLength: 40, validator: value => /^(?:\d+(?:\.\d+)?)?$/u.test(value) || "Use a non-negative decimal string, or empty to clear." };
|
|
123
|
+
const schema = createSchema({
|
|
124
|
+
...(update ? { id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } } : {}),
|
|
125
|
+
name: { type: "string", required: !update, minLength: 1, maxLength: 1000 },
|
|
126
|
+
status: { type: "string", required: !update, enum: ["draft", "pending", "private", "publish"] },
|
|
127
|
+
type: { type: "string", enum: ["simple", "variable"] },
|
|
128
|
+
description: { type: "string", maxLength: 100000 }, short_description: { type: "string", maxLength: 10000 },
|
|
129
|
+
sku: { type: "string", maxLength: 200 }, regular_price: price, sale_price: price,
|
|
130
|
+
manage_stock: { type: "boolean" }, stock_quantity: { type: "integer", min: -2147483648, max: 2147483647 },
|
|
131
|
+
stock_status: { type: "string", enum: ["instock", "outofstock", "onbackorder"] },
|
|
132
|
+
backorders: { type: "string", enum: ["no", "notify", "yes"] },
|
|
133
|
+
virtual: { type: "boolean" }, featured: { type: "boolean" }, sold_individually: { type: "boolean" },
|
|
134
|
+
attributes: { type: "array", validator: values => values.length <= 100 || "Use at most 100 attributes.",
|
|
135
|
+
items: { type: "object", schema: createSchema({
|
|
136
|
+
name: { type: "string", required: true, minLength: 1, maxLength: 200 },
|
|
137
|
+
visible: { type: "boolean" }, variation: { type: "boolean" },
|
|
138
|
+
options: { type: "array", required: true, validator: values => values.length <= 100 || "Use at most 100 options.", items: { type: "string", minLength: 1, maxLength: 200 } }
|
|
139
|
+
}) } },
|
|
140
|
+
categories: { type: "array", validator: values => values.length <= 100 || "Use at most 100 categories.",
|
|
141
|
+
items: { type: "object", schema: createSchema({ id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } }) } },
|
|
142
|
+
images: { type: "array", validator: values => values.length <= 100 || "Use at most 100 images.",
|
|
143
|
+
items: { type: "object", schema: createSchema({ id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER }, alt: { type: "string", maxLength: 1000 } }) } }
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
scopes: [],
|
|
147
|
+
request(input, settings) {
|
|
148
|
+
for (const field of ["regular_price", "sale_price"]) {
|
|
149
|
+
if (input?.[field] !== undefined && typeof input[field] !== "string") {
|
|
150
|
+
throw new ConnectorError("connector_input_invalid", "Product prices must be decimal strings.", { statusCode: 422 });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
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 });
|
|
155
|
+
return { method: update ? "PUT" : "POST", url: `${baseUrl(settings)}/products${update ? `/${id}` : ""}`, body };
|
|
156
|
+
}, validateResult: validRecord.products
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function customerWrite(update) {
|
|
161
|
+
const text = { type: "string", maxLength: 1000 };
|
|
162
|
+
const address = {
|
|
163
|
+
first_name: text, last_name: text, company: text, address_1: text, address_2: text,
|
|
164
|
+
city: text, state: text, postcode: text, country: { type: "string", maxLength: 2 }
|
|
165
|
+
};
|
|
166
|
+
const schema = createSchema({
|
|
167
|
+
...(update ? { id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } } : {
|
|
168
|
+
username: { type: "string", minLength: 1, maxLength: 60 }
|
|
169
|
+
}),
|
|
170
|
+
email: { type: "string", required: !update, minLength: 1, maxLength: 320 },
|
|
171
|
+
first_name: text, last_name: text,
|
|
172
|
+
billing: { type: "object", schema: createSchema({ ...address, email: text, phone: text }) },
|
|
173
|
+
shipping: { type: "object", schema: createSchema(address) }
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
scopes: [],
|
|
177
|
+
request(input, settings) {
|
|
178
|
+
const { id, ...body } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
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 });
|
|
181
|
+
}
|
|
182
|
+
return { method: update ? "PUT" : "POST", url: `${baseUrl(settings)}/customers${update ? `/${id}` : ""}`, body };
|
|
183
|
+
}, validateResult: validRecord.customers
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function variationOperation(action) {
|
|
188
|
+
const writing = action === "create" || action === "update";
|
|
189
|
+
const schema = createSchema({
|
|
190
|
+
product_id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
191
|
+
...(action === "create" ? {} : action === "list" ? pagination : { id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } }),
|
|
192
|
+
...(writing ? {
|
|
193
|
+
regular_price: { type: "string", maxLength: 40, validator: value => /^(?:\d+(?:\.\d+)?)?$/u.test(value) || "Use a decimal string or empty to clear." },
|
|
194
|
+
sale_price: { type: "string", maxLength: 40, validator: value => /^(?:\d+(?:\.\d+)?)?$/u.test(value) || "Use a decimal string or empty to clear." },
|
|
195
|
+
sku: { type: "string", maxLength: 200 },
|
|
196
|
+
status: { type: "string", required: action === "create", enum: ["draft", "pending", "private", "publish"] },
|
|
197
|
+
attributes: { type: "array", required: action === "create", validator: values => values.length > 0 && values.length <= 100 || "Choose 1–100 attributes.",
|
|
198
|
+
items: { type: "object", schema: createSchema({ name: { type: "string", required: true, minLength: 1, maxLength: 200 }, option: { type: "string", required: true, minLength: 1, maxLength: 200 } }) } },
|
|
199
|
+
manage_stock: { type: "boolean" },
|
|
200
|
+
stock_quantity: { type: "integer", min: -2147483648, max: 2147483647 },
|
|
201
|
+
stock_status: { type: "string", enum: ["instock", "outofstock", "onbackorder"] },
|
|
202
|
+
backorders: { type: "string", enum: ["no", "notify", "yes"] }
|
|
203
|
+
} : {})
|
|
204
|
+
});
|
|
205
|
+
const valid = value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.price === "string" && Array.isArray(value.attributes);
|
|
206
|
+
return {
|
|
207
|
+
scopes: [],
|
|
208
|
+
request(input, settings) {
|
|
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 });
|
|
211
|
+
}
|
|
212
|
+
const { product_id, id, ...fields } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
213
|
+
const url = new URL(`${baseUrl(settings)}/products/${product_id}/variations${id ? `/${id}` : ""}`);
|
|
214
|
+
if (writing) {
|
|
215
|
+
if (!Object.keys(fields).length) throw new ConnectorError("connector_input_invalid", "Supply variation changes.", { statusCode: 422 });
|
|
216
|
+
return { method: action === "create" ? "POST" : "PUT", url: url.href, body: fields };
|
|
217
|
+
}
|
|
218
|
+
for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, String(value));
|
|
219
|
+
return { method: "GET", url: url.href };
|
|
220
|
+
},
|
|
221
|
+
validateResult: action === "list" ? value => Array.isArray(value) && value.length <= 100 && value.every(valid) : valid
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function refundOperation(action) {
|
|
226
|
+
const amount = { type: "string", required: true, maxLength: 40, validator: value => /^\d+(?:\.\d+)?$/u.test(value) || "Use a non-negative decimal string." };
|
|
227
|
+
const schema = createSchema({
|
|
228
|
+
order_id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
229
|
+
...(action === "get" ? { id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER } } : {}),
|
|
230
|
+
...(action === "list" ? { page: pagination.page, per_page: pagination.per_page } : {}),
|
|
231
|
+
...(action === "create" ? {
|
|
232
|
+
amount, reason: { type: "string", required: true, minLength: 1, maxLength: 1000 },
|
|
233
|
+
api_refund: { type: "boolean", required: true }, api_restock: { type: "boolean", required: true },
|
|
234
|
+
line_items: { type: "array", validator: values => values.length <= 100 || "Use at most 100 refund lines.", items: { type: "object", schema: createSchema({
|
|
235
|
+
id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
236
|
+
quantity: { type: "integer", required: true, min: 0, max: 100000 }, refund_total: amount
|
|
237
|
+
}) } }
|
|
238
|
+
} : {})
|
|
239
|
+
});
|
|
240
|
+
const valid = value => Number.isSafeInteger(value?.id) && value.id > 0 && typeof value.amount === "string" && typeof value.refunded_payment === "boolean";
|
|
241
|
+
return {
|
|
242
|
+
scopes: [],
|
|
243
|
+
request(input, settings) {
|
|
244
|
+
if (action === "create" && (typeof input?.amount !== "string" || typeof input.api_refund !== "boolean" || typeof input.api_restock !== "boolean" ||
|
|
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 });
|
|
247
|
+
}
|
|
248
|
+
const { order_id, id, ...fields } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
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 });
|
|
251
|
+
}
|
|
252
|
+
const url = new URL(`${baseUrl(settings)}/orders/${order_id}/refunds${id ? `/${id}` : ""}`);
|
|
253
|
+
if (action === "create") return { method: "POST", url: url.href, body: fields };
|
|
254
|
+
for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, String(value));
|
|
255
|
+
return { method: "GET", url: url.href };
|
|
256
|
+
},
|
|
257
|
+
validateResult: action === "list" ? value => Array.isArray(value) && value.length <= 100 && value.every(valid) : valid
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function reportOperation(report) {
|
|
262
|
+
const dated = report === "sales" || report === "top_sellers";
|
|
263
|
+
const date = { type: "string", validator: value => /^\d{4}-\d{2}-\d{2}$/u.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value || "Use a valid YYYY-MM-DD date." };
|
|
264
|
+
const schema = createSchema(dated ? { period: { type: "string", enum: ["week", "month", "last_month", "year"] }, date_min: date, date_max: date } : {});
|
|
265
|
+
return {
|
|
266
|
+
scopes: [],
|
|
267
|
+
request(input, settings) {
|
|
268
|
+
const fields = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
269
|
+
if ((fields.date_min === undefined) !== (fields.date_max === undefined) ||
|
|
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 });
|
|
272
|
+
}
|
|
273
|
+
const url = new URL(`${baseUrl(settings)}/reports/${report}`);
|
|
274
|
+
for (const [key, value] of Object.entries(fields)) url.searchParams.set(key, value);
|
|
275
|
+
return { method: "GET", url: url.href };
|
|
276
|
+
},
|
|
277
|
+
validateResult(value) {
|
|
278
|
+
if (!Array.isArray(value)) return false;
|
|
279
|
+
if (report === "sales") return value.length === 1 && typeof value[0]?.total_sales === "string" && typeof value[0]?.net_sales === "string" && Number.isSafeInteger(value[0]?.total_orders) && value[0]?.totals !== null && typeof value[0]?.totals === "object";
|
|
280
|
+
if (report === "top_sellers") return value.every(row => Number.isSafeInteger(row?.product_id) && row.product_id > 0 && typeof row.title === "string" && Number.isSafeInteger(row.quantity));
|
|
281
|
+
return value.every(row => typeof row?.slug === "string" && typeof row.name === "string" && Number.isSafeInteger(row.total) && row.total >= 0);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const orderAddressFields = Object.fromEntries(
|
|
287
|
+
["first_name", "last_name", "company", "address_1", "address_2", "city", "state", "postcode", "country"].map(name => [name, { type: "string", maxLength: 1000 }])
|
|
288
|
+
);
|
|
289
|
+
const orderCreateSchema = createSchema({
|
|
290
|
+
customer_id: { type: "integer", required: true, min: 0, max: Number.MAX_SAFE_INTEGER },
|
|
291
|
+
status: { type: "string", required: true, enum: ["pending", "on-hold"] },
|
|
292
|
+
customer_note: { type: "string", maxLength: 10000 },
|
|
293
|
+
billing: { type: "object", schema: createSchema({ ...orderAddressFields, email: { type: "string", maxLength: 320 }, phone: { type: "string", maxLength: 100 } }) },
|
|
294
|
+
shipping: { type: "object", schema: createSchema(orderAddressFields) },
|
|
295
|
+
line_items: { type: "array", required: true, validator: values => values.length > 0 && values.length <= 100 || "Choose 1–100 order items.",
|
|
296
|
+
items: { type: "object", schema: createSchema({
|
|
297
|
+
product_id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
298
|
+
variation_id: { type: "integer", min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
299
|
+
quantity: { type: "integer", required: true, min: 1, max: 100000 }
|
|
300
|
+
}) } },
|
|
301
|
+
coupon_lines: { type: "array", validator: values => values.length <= 100 || "Use at most 100 coupons.",
|
|
302
|
+
items: { type: "object", schema: createSchema({ code: { type: "string", required: true, minLength: 1, maxLength: 200 } }) } }
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const orderUpdateSchema = createSchema({
|
|
306
|
+
id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
307
|
+
status: { type: "string", enum: ["pending", "processing", "on-hold", "completed", "cancelled", "refunded", "failed"] },
|
|
308
|
+
customer_note: { type: "string", maxLength: 10000 }
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const woocommerceProvider = Object.freeze({
|
|
312
|
+
...woocommerceDefinition,
|
|
313
|
+
apiOrigins: (settings) => [new URL(settings.siteUrl).origin],
|
|
314
|
+
apiKey: { headers: (secret, settings) => ({ Authorization: `Basic ${Buffer.from(`${settings.consumerKey}:${secret}`).toString("base64")}` }) },
|
|
315
|
+
checkOperation: "products.list",
|
|
316
|
+
async exchange(address, options, { request }) {
|
|
317
|
+
const result = await request(address, options);
|
|
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 });
|
|
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 });
|
|
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 });
|
|
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 });
|
|
326
|
+
return result;
|
|
327
|
+
},
|
|
328
|
+
operations: {
|
|
329
|
+
"refunds.list": refundOperation("list"),
|
|
330
|
+
"refunds.get": refundOperation("get"),
|
|
331
|
+
"refunds.create": refundOperation("create"),
|
|
332
|
+
"reports.sales": reportOperation("sales"),
|
|
333
|
+
"reports.topSellers": reportOperation("top_sellers"),
|
|
334
|
+
"reports.orders": reportOperation("orders/totals"),
|
|
335
|
+
"reports.products": reportOperation("products/totals"),
|
|
336
|
+
"reports.customers": reportOperation("customers/totals"),
|
|
337
|
+
"reports.coupons": reportOperation("coupons/totals"),
|
|
338
|
+
"categories.list": categoryOperation("list"),
|
|
339
|
+
"categories.get": categoryOperation("get"),
|
|
340
|
+
"categories.create": categoryOperation("create"),
|
|
341
|
+
"categories.update": categoryOperation("update"),
|
|
342
|
+
"categories.delete": categoryOperation("delete"),
|
|
343
|
+
"variations.create": variationOperation("create"),
|
|
344
|
+
"variations.list": variationOperation("list"),
|
|
345
|
+
"variations.delete": deleteRecord("variations"),
|
|
346
|
+
"variations.get": variationOperation("get"),
|
|
347
|
+
"variations.update": variationOperation("update"),
|
|
348
|
+
"products.delete": deleteRecord("products"),
|
|
349
|
+
"products.get": readRecord("products"),
|
|
350
|
+
"products.create": productWrite(false),
|
|
351
|
+
"products.update": productWrite(true),
|
|
352
|
+
"orders.delete": deleteRecord("orders"),
|
|
353
|
+
"orders.get": readRecord("orders"),
|
|
354
|
+
"orders.create": {
|
|
355
|
+
scopes: [],
|
|
356
|
+
request(input, settings) {
|
|
357
|
+
const body = validateSchemaPayload({ schema: orderCreateSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
358
|
+
return { method: "POST", url: `${baseUrl(settings)}/orders`, body };
|
|
359
|
+
}, validateResult: validRecord.orders
|
|
360
|
+
},
|
|
361
|
+
"orders.update": {
|
|
362
|
+
scopes: [],
|
|
363
|
+
request(input, settings) {
|
|
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 });
|
|
366
|
+
return { method: "PUT", url: `${baseUrl(settings)}/orders/${id}`, body };
|
|
367
|
+
}, validateResult: validRecord.orders
|
|
368
|
+
},
|
|
369
|
+
"customers.delete": deleteRecord("customers"),
|
|
370
|
+
"customers.get": readRecord("customers"),
|
|
371
|
+
"customers.create": customerWrite(false),
|
|
372
|
+
"customers.update": customerWrite(true),
|
|
373
|
+
"coupons.delete": deleteRecord("coupons"),
|
|
374
|
+
"coupons.get": readRecord("coupons"),
|
|
375
|
+
"coupons.create": couponWrite(false),
|
|
376
|
+
"coupons.update": couponWrite(true),
|
|
377
|
+
"customers.list": jsonOperation(settings => `${baseUrl(settings)}/customers`, {
|
|
378
|
+
...pagination, email: { type: "string", minLength: 1, maxLength: 320 }
|
|
379
|
+
}, result => Array.isArray(result) && result.length <= 100 && result.every(validRecord.customers)),
|
|
380
|
+
"coupons.list": jsonOperation(settings => `${baseUrl(settings)}/coupons`, {
|
|
381
|
+
...pagination, code: { type: "string", minLength: 1, maxLength: 500 }
|
|
382
|
+
}, result => Array.isArray(result) && result.length <= 100 && result.every(validRecord.coupons)),
|
|
383
|
+
"products.list": jsonOperation((settings) => `${baseUrl(settings)}/products`, {
|
|
384
|
+
...pagination,
|
|
385
|
+
status: { type: "string", enum: ["any", "draft", "pending", "private", "publish"], defaultTo: "any" },
|
|
386
|
+
sku: { type: "string", minLength: 1, maxLength: 500 },
|
|
387
|
+
stock_status: { type: "string", enum: ["instock", "outofstock", "onbackorder"] }
|
|
388
|
+
}, (result) => Array.isArray(result) && result.every((product) => Number.isInteger(product?.id) && product.id > 0 && typeof product.name === "string")),
|
|
389
|
+
"orders.list": jsonOperation((settings) => `${baseUrl(settings)}/orders`, {
|
|
390
|
+
...pagination,
|
|
391
|
+
status: { type: "string", enum: ["any", "pending", "processing", "on-hold", "completed", "cancelled", "refunded", "failed", "trash"], defaultTo: "any" },
|
|
392
|
+
customer: { type: "integer", min: 0 }
|
|
393
|
+
}, (result) => Array.isArray(result) && result.every((order) => Number.isInteger(order?.id) && order.id > 0 && typeof order.status === "string" && typeof order.total === "string"))
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Verify before parsing or processing. The application owns routing and replay protection.
|
|
398
|
+
function verifyWooCommerceWebhook({ rawBody, signature, secret } = {}) {
|
|
399
|
+
if (!(rawBody instanceof Uint8Array) || rawBody.byteLength > 2 * 1024 * 1024 ||
|
|
400
|
+
typeof secret !== "string" || !secret || typeof signature !== "string" ||
|
|
401
|
+
!/^[A-Za-z0-9+/]{43}=$/u.test(signature)) {
|
|
402
|
+
throw new ConnectorError("connector_webhook_invalid", "Invalid WooCommerce webhook signature inputs.", { statusCode: 401 });
|
|
403
|
+
}
|
|
404
|
+
const supplied = Buffer.from(signature, "base64");
|
|
405
|
+
const expected = createHmac("sha256", secret).update(rawBody).digest();
|
|
406
|
+
if (supplied.toString("base64") !== signature || !timingSafeEqual(expected, supplied)) {
|
|
407
|
+
throw new ConnectorError("connector_webhook_invalid", "Invalid WooCommerce webhook signature.", { statusCode: 401 });
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export { woocommerceProvider, verifyWooCommerceWebhook };
|