@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,143 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
3
|
+
import { slackDefinition } from "../shared/slack.js";
|
|
4
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
5
|
+
|
|
6
|
+
function checkReply(value) {
|
|
7
|
+
if (value?.ok === true) return value;
|
|
8
|
+
if (value?.ok !== false || typeof value.error !== "string") {
|
|
9
|
+
throw new ConnectorError("connector_response_invalid", "Slack returned an invalid response.", { statusCode: 502 });
|
|
10
|
+
}
|
|
11
|
+
if (["invalid_auth", "not_authed", "token_revoked", "token_expired", "account_inactive", "invalid_refresh_token", "invalid_grant"].includes(value.error)) {
|
|
12
|
+
throw new ConnectorError("connector_reconnect_required", "Connect this Slack account again.", { statusCode: 401 });
|
|
13
|
+
}
|
|
14
|
+
if (["missing_scope", "not_allowed_token_type", "access_denied", "no_permission", "not_in_channel", "restricted_action"].includes(value.error)) {
|
|
15
|
+
throw new ConnectorError("connector_permission_denied", "Slack denied this operation.", { statusCode: 403 });
|
|
16
|
+
}
|
|
17
|
+
if (["ratelimited", "rate_limited"].includes(value.error)) {
|
|
18
|
+
throw new ConnectorError("connector_rate_limited", "Slack's request limit was reached.", { statusCode: 429 });
|
|
19
|
+
}
|
|
20
|
+
if (["invalid_client_id", "bad_client_secret", "bad_redirect_uri"].includes(value.error)) {
|
|
21
|
+
throw new ConnectorError("connector_registration_invalid", "Check the Slack client registration and callback.", { statusCode: 422 });
|
|
22
|
+
}
|
|
23
|
+
throw new ConnectorError("connector_provider_failed", "The Slack request failed. Try again.", { statusCode: 502 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function normalizeTokenResponse(response, { settings, grantType }) {
|
|
27
|
+
if ([401, 403, 429].includes(response.status)) {
|
|
28
|
+
throw new ConnectorError(
|
|
29
|
+
{ 401: "connector_reconnect_required", 403: "connector_permission_denied", 429: "connector_rate_limited" }[response.status],
|
|
30
|
+
"Slack could not authorize this request.", { statusCode: response.status }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
let value;
|
|
34
|
+
try { value = await response.clone().json(); } catch {
|
|
35
|
+
if (!response.ok) return response;
|
|
36
|
+
throw new ConnectorError("connector_response_invalid", "Slack returned an invalid token response.", { statusCode: 502 });
|
|
37
|
+
}
|
|
38
|
+
checkReply(value);
|
|
39
|
+
if (!response.ok) return response;
|
|
40
|
+
const actor = settings.actor || "user";
|
|
41
|
+
const token = grantType === "authorization_code" && actor === "user" ? value.authed_user : value;
|
|
42
|
+
if (token?.token_type !== actor || typeof token.scope !== "string" || !/^[^\s,]+(?:,[^\s,]+)*$/u.test(token.scope)) {
|
|
43
|
+
throw new ConnectorError("connector_response_invalid", "Slack returned a token for the wrong identity or an invalid permission grant.", { statusCode: 502 });
|
|
44
|
+
}
|
|
45
|
+
// Retain exactly one identity; a response may contain both user and bot tokens.
|
|
46
|
+
return Response.json({ access_token: token.access_token, token_type: "Bearer", scope: token.scope,
|
|
47
|
+
...(token.expires_in !== undefined ? { expires_in: token.expires_in } : {}),
|
|
48
|
+
...(token.refresh_token !== undefined ? { refresh_token: token.refresh_token } : {})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cursor = { type: "string", minLength: 1, maxLength: 4096 };
|
|
53
|
+
const channel = { type: "string", required: true, validator: (value) => /^[CGD][A-Z0-9]+$/u.test(value) || "Use a conversation ID.", maxLength: 100 };
|
|
54
|
+
const timestamp = { type: "string", validator: (value) => /^[0-9]+\.[0-9]+$/u.test(value) || "Use a Slack timestamp.", maxLength: 40, noTrim: true };
|
|
55
|
+
const listResult = (value) => value?.ok === true && Array.isArray(value.channels) &&
|
|
56
|
+
value.channels.every((channel) => typeof channel?.id === "string" && channel.id.length > 0) &&
|
|
57
|
+
(value.response_metadata === undefined || (value.response_metadata !== null && typeof value.response_metadata === "object" &&
|
|
58
|
+
!Array.isArray(value.response_metadata) && (value.response_metadata.next_cursor === undefined || typeof value.response_metadata.next_cursor === "string")));
|
|
59
|
+
const operations = {
|
|
60
|
+
"auth.test": jsonOperation("https://slack.com/api/auth.test", {}, (value) => value?.ok === true &&
|
|
61
|
+
typeof value.team_id === "string" && value.team_id.length > 0 && typeof value.user_id === "string" && value.user_id.length > 0)
|
|
62
|
+
};
|
|
63
|
+
for (const [name, type, permission] of [
|
|
64
|
+
["channels.list", "public_channel", "channels:read"],
|
|
65
|
+
["groups.list", "private_channel", "groups:read"],
|
|
66
|
+
["directMessages.list", "im", "im:read"],
|
|
67
|
+
["groupMessages.list", "mpim", "mpim:read"]
|
|
68
|
+
]) {
|
|
69
|
+
operations[name] = {
|
|
70
|
+
...jsonOperation(`https://slack.com/api/conversations.list?types=${type}`, {
|
|
71
|
+
limit: { type: "integer", min: 1, max: 200, defaultTo: 100 }, cursor,
|
|
72
|
+
exclude_archived: { type: "boolean", defaultTo: false }
|
|
73
|
+
}, listResult),
|
|
74
|
+
scopes: [permission]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const [name, permission] of [["channels.history", "channels:history"], ["groups.history", "groups:history"],
|
|
79
|
+
["directMessages.history", "im:history"], ["groupMessages.history", "mpim:history"]]) {
|
|
80
|
+
operations[name] = { ...jsonOperation("https://slack.com/api/conversations.history", {
|
|
81
|
+
channel, cursor, limit: { type: "integer", min: 1, max: 15, defaultTo: 15 },
|
|
82
|
+
oldest: timestamp, latest: timestamp, inclusive: { type: "boolean", defaultTo: false }
|
|
83
|
+
}, (value) => value?.ok === true && Array.isArray(value.messages) && value.messages.length <= 15 &&
|
|
84
|
+
value.messages.every((message) => typeof message?.ts === "string" && /^[0-9]+\.[0-9]+$/u.test(message.ts)) &&
|
|
85
|
+
typeof value.has_more === "boolean" && (value.response_metadata === undefined ||
|
|
86
|
+
typeof value.response_metadata?.next_cursor === "string")), scopes: [permission] };
|
|
87
|
+
}
|
|
88
|
+
operations["users.info"] = { ...jsonOperation("https://slack.com/api/users.info", {
|
|
89
|
+
user: { type: "string", required: true, validator: (value) => /^[UW][A-Z0-9]+$/u.test(value) || "Use a Slack user ID.", maxLength: 100 }
|
|
90
|
+
}, (value) => value?.ok === true && typeof value.user?.id === "string" && typeof value.user?.name === "string"), scopes: ["users:read"] };
|
|
91
|
+
operations["messages.send"] = { ...jsonOperation("https://slack.com/api/chat.postMessage", {
|
|
92
|
+
channel, text: { type: "string", required: true, minLength: 1, maxLength: 4000, noTrim: true },
|
|
93
|
+
thread_ts: timestamp, unfurl_links: { type: "boolean", defaultTo: false },
|
|
94
|
+
unfurl_media: { type: "boolean", defaultTo: false }
|
|
95
|
+
}, (value) => value?.ok === true && typeof value.channel === "string" &&
|
|
96
|
+
typeof value.ts === "string" && /^[0-9]+\.[0-9]+$/u.test(value.ts) &&
|
|
97
|
+
value.message !== null && typeof value.message === "object", "POST"), scopes: ["chat:write"] };
|
|
98
|
+
|
|
99
|
+
const slackProvider = Object.freeze({
|
|
100
|
+
...slackDefinition,
|
|
101
|
+
oauth: { issuer: "https://slack.com", authorization_endpoint: "https://slack.com/oauth/v2/authorize", token_endpoint: "https://slack.com/api/oauth.v2.access" },
|
|
102
|
+
scopeSeparator: ",",
|
|
103
|
+
authorizationScopeParameter: ({ actor = "user" } = {}) => actor === "user" ? "user_scope" : "scope",
|
|
104
|
+
normalizeTokenResponse,
|
|
105
|
+
apiOrigins: ["https://slack.com"], checkOperation: "channels.list", operations,
|
|
106
|
+
async exchange(address, options, { request }) {
|
|
107
|
+
return checkReply(await request(address, options));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// The app supplies raw bytes before body parsing and binds the expected installation.
|
|
111
|
+
function verifySlackRequest({ rawBody, signature, timestamp, secret, contentType, appId, teamId, now = Date.now() }) {
|
|
112
|
+
const reject = () => { throw new ConnectorError("connector_webhook_invalid", "Invalid Slack callback.", { statusCode: 401 }); };
|
|
113
|
+
if (!(rawBody instanceof Uint8Array) || rawBody.byteLength > 1048576 ||
|
|
114
|
+
typeof secret !== "string" || !secret || typeof signature !== "string" || !/^v0=[a-f0-9]{64}$/u.test(signature) ||
|
|
115
|
+
typeof timestamp !== "string" || !/^[0-9]{1,12}$/u.test(timestamp) || !Number.isFinite(now) ||
|
|
116
|
+
Math.abs(now / 1000 - Number(timestamp)) > 300 || typeof appId !== "string" || !appId ||
|
|
117
|
+
typeof teamId !== "string" || !teamId) reject();
|
|
118
|
+
const expected = createHmac("sha256", secret).update(`v0:${timestamp}:`).update(rawBody).digest();
|
|
119
|
+
if (!timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"))) reject();
|
|
120
|
+
let payload;
|
|
121
|
+
try {
|
|
122
|
+
const body = Buffer.from(rawBody).toString("utf8");
|
|
123
|
+
const mediaType = contentType?.split(";", 1)[0].trim().toLowerCase();
|
|
124
|
+
if (mediaType === "application/json") payload = JSON.parse(body);
|
|
125
|
+
else if (mediaType === "application/x-www-form-urlencoded") {
|
|
126
|
+
const form = new URLSearchParams(body);
|
|
127
|
+
if (form.getAll("payload").length !== 1) reject();
|
|
128
|
+
payload = JSON.parse(form.get("payload"));
|
|
129
|
+
} else reject();
|
|
130
|
+
} catch { reject(); }
|
|
131
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) reject();
|
|
132
|
+
// URL verification has no installation identity; the signature binds its app secret.
|
|
133
|
+
if (payload.type === "url_verification" && typeof payload.challenge === "string" && payload.challenge.length > 0) return payload;
|
|
134
|
+
if (payload.api_app_id !== appId || (payload.team_id ?? payload.team?.id) !== teamId) reject();
|
|
135
|
+
if (payload.type === "event_callback") {
|
|
136
|
+
if (typeof payload.event_id !== "string" || !payload.event_id || !payload.event ||
|
|
137
|
+
typeof payload.event.type !== "string") reject();
|
|
138
|
+
} else if (["block_actions", "view_submission", "view_closed", "shortcut", "message_action"].includes(payload.type)) {
|
|
139
|
+
if (typeof payload.user?.id !== "string" || !payload.user.id) reject();
|
|
140
|
+
} else reject();
|
|
141
|
+
return payload;
|
|
142
|
+
}
|
|
143
|
+
export { slackProvider, verifySlackRequest };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
2
|
+
import { snowflakeDefinition } from "../shared/snowflake.js";
|
|
3
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
4
|
+
|
|
5
|
+
const origin = (settings) => new URL(settings.accountUrl).origin;
|
|
6
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
const database = (value) => object(value) && typeof value.name === "string" && value.name.length > 0 && value.name.length <= 255 &&
|
|
8
|
+
["owner", "comment", "kind", "created_on"].every((key) => value[key] == null || typeof value[key] === "string");
|
|
9
|
+
const nameFilter = { type: "string", minLength: 1, maxLength: 255, noTrim: true };
|
|
10
|
+
const databasesList = jsonOperation((settings) => `${origin(settings)}/api/v2/databases`, {
|
|
11
|
+
showLimit: { type: "integer", min: 1, max: 1000, defaultTo: 20 },
|
|
12
|
+
like: nameFilter, startsWith: nameFilter, fromName: nameFilter, history: { type: "boolean", defaultTo: false }
|
|
13
|
+
}, (value) => Array.isArray(value) && value.every(database));
|
|
14
|
+
|
|
15
|
+
const uuid = (value) => typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu.test(value);
|
|
16
|
+
const uuidField = { type: "string", required: true, validator: (value) => uuid(value) || "Use a Snowflake UUID." };
|
|
17
|
+
const bindings = { type: "object", additionalProperties: true, validator: (value) =>
|
|
18
|
+
Object.keys(value).length <= 100 && Object.entries(value).every(([key, binding]) =>
|
|
19
|
+
/^[1-9][0-9]{0,2}$/u.test(key) && object(binding) && Object.keys(binding).every((name) => ["type", "value"].includes(name)) &&
|
|
20
|
+
["FIXED", "REAL", "TEXT", "BINARY", "BOOLEAN", "DATE", "TIME", "TIMESTAMP_LTZ", "TIMESTAMP_NTZ", "TIMESTAMP_TZ"].includes(binding.type) &&
|
|
21
|
+
(binding.value === null || (typeof binding.value === "string" && binding.value.length <= 65536))) || "Use at most 100 numbered typed bindings." };
|
|
22
|
+
const sqlResult = (value) => object(value) && ["pending", "completed", "cancelled"].includes(value.status);
|
|
23
|
+
const submit = jsonOperation((settings) => `${origin(settings)}/api/v2/statements`, {
|
|
24
|
+
statement: { type: "string", required: true, minLength: 1, maxLength: 65536, noTrim: true },
|
|
25
|
+
requestId: uuidField, bindings, timeout: { type: "integer", min: 1, max: 3600, defaultTo: 60 }
|
|
26
|
+
}, sqlResult, "POST");
|
|
27
|
+
const operations = { "databases.list": databasesList,
|
|
28
|
+
"statements.submit": { ...submit, request(input, settings) {
|
|
29
|
+
const request = submit.request(input, settings);
|
|
30
|
+
const { requestId, ...body } = request.body;
|
|
31
|
+
for (const name of ["warehouse", "database", "schema", "role"]) if (settings[name]) body[name] = settings[name];
|
|
32
|
+
body.parameters = { multi_statement_count: "1" };
|
|
33
|
+
return { ...request, url: `${request.url}?async=true&requestId=${requestId}`, body };
|
|
34
|
+
} }
|
|
35
|
+
};
|
|
36
|
+
for (const [operation, cancel] of [["statements.get", false], ["statements.cancel", true]]) {
|
|
37
|
+
const base = jsonOperation((settings) => `${origin(settings)}/api/v2/statements`, {
|
|
38
|
+
handle: uuidField, ...(!cancel ? { partition: { type: "integer", min: 0, max: 100000 } } : {})
|
|
39
|
+
}, sqlResult, "POST");
|
|
40
|
+
operations[operation] = { ...base, request(input, settings) {
|
|
41
|
+
const { url, body } = base.request(input, settings);
|
|
42
|
+
return { method: cancel ? "POST" : "GET", url: `${url}/${body.handle}${cancel ? "/cancel" : body.partition === undefined ? "" : `?partition=${body.partition}`}`,
|
|
43
|
+
...(cancel ? { body: {} } : {}) };
|
|
44
|
+
} };
|
|
45
|
+
}
|
|
46
|
+
for (const action of ["create", "resize", "resume", "suspend", "delete"]) {
|
|
47
|
+
const fields = { name: { ...nameFilter, required: true }, requestId: uuidField };
|
|
48
|
+
if (["create", "resize"].includes(action)) fields.size = { type: "string", required: true,
|
|
49
|
+
enum: ["XSMALL", "SMALL", "MEDIUM", "LARGE", "XLARGE", "XXLARGE", "XXXLARGE", "X4LARGE", "X5LARGE", "X6LARGE"] };
|
|
50
|
+
const base = jsonOperation((settings) => `${origin(settings)}/api/v2/statements`, fields, sqlResult, "POST");
|
|
51
|
+
operations[`warehouses.${action}`] = { ...base, request(input, settings) {
|
|
52
|
+
const { body } = base.request(input, settings);
|
|
53
|
+
const name = `"${body.name.replaceAll('"', '""')}"`;
|
|
54
|
+
const statement = action === "create"
|
|
55
|
+
? `CREATE WAREHOUSE ${name} WAREHOUSE_SIZE = ${body.size} INITIALLY_SUSPENDED = TRUE AUTO_SUSPEND = 60 AUTO_RESUME = FALSE`
|
|
56
|
+
: action === "resize" ? `ALTER WAREHOUSE ${name} SET WAREHOUSE_SIZE = ${body.size}`
|
|
57
|
+
: action === "delete" ? `DROP WAREHOUSE ${name}`
|
|
58
|
+
: `ALTER WAREHOUSE ${name} ${action === "resume" ? "RESUME IF SUSPENDED" : "SUSPEND"}`;
|
|
59
|
+
return operations["statements.submit"].request({ statement, requestId: body.requestId }, settings);
|
|
60
|
+
} };
|
|
61
|
+
}
|
|
62
|
+
for (const [name, operation] of Object.entries(operations)) {
|
|
63
|
+
operations[name] = { ...operation, request(input, settings) {
|
|
64
|
+
return { ...operation.request(input, settings), headers: { "X-Snowflake-Authorization-Token-Type": "OAUTH",
|
|
65
|
+
"User-Agent": "jskit-connectors/0.1", ...(settings.role ? { "X-Snowflake-Role": `"${settings.role}"` } : {}) } };
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const snowflakeProvider = Object.freeze({
|
|
70
|
+
...snowflakeDefinition,
|
|
71
|
+
oauth: (settings) => ({ issuer: origin(settings), authorization_endpoint: `${origin(settings)}/oauth/authorize`,
|
|
72
|
+
token_endpoint: `${origin(settings)}/oauth/token-request` }),
|
|
73
|
+
apiOrigins: (settings) => [origin(settings)], checkOperation: "databases.list", scopesInAuthorizationResponse: true,
|
|
74
|
+
async normalizeTokenResponse(response) {
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
let value;
|
|
77
|
+
try { value = await response.clone().json(); } catch { /* rejected below */ }
|
|
78
|
+
if (!Number.isSafeInteger(value?.expires_in) || value.expires_in <= 0 ||
|
|
79
|
+
(value.scope !== undefined && (typeof value.scope !== "string" || !/^[^\s]+(?: [^\s]+)*$/u.test(value.scope))) ||
|
|
80
|
+
(value.refresh_token !== undefined && (typeof value.refresh_token !== "string" || !value.refresh_token.trim()))) {
|
|
81
|
+
throw new ConnectorError("connector_response_invalid", "Snowflake returned an invalid token grant.", { statusCode: 502 });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
},
|
|
86
|
+
operations,
|
|
87
|
+
async exchange(address, options, { fetchImpl }) {
|
|
88
|
+
const response = await fetchImpl(address, { ...options, credentials: "omit",
|
|
89
|
+
...(options.body !== undefined ? { body: JSON.stringify(options.body), headers: { ...options.headers, "Content-Type": "application/json" } } : {}) });
|
|
90
|
+
const url = new URL(address);
|
|
91
|
+
const sql = url.pathname.startsWith("/api/v2/statements");
|
|
92
|
+
if (sql && response.status === 422) throw new ConnectorError("connector_operation_rejected", "Snowflake rejected the statement. Inspect query history with the authorized role.", { statusCode: 422 });
|
|
93
|
+
if (!response.ok) throw Object.assign(new Error("Snowflake request failed."), { status: response.status });
|
|
94
|
+
const reader = response.body?.getReader();
|
|
95
|
+
const chunks = [];
|
|
96
|
+
let size = 0;
|
|
97
|
+
if (reader) try {
|
|
98
|
+
while (true) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done) break;
|
|
101
|
+
size += value.byteLength;
|
|
102
|
+
if (size > 16 * 1024 * 1024) {
|
|
103
|
+
await reader.cancel();
|
|
104
|
+
throw new ConnectorError("connector_response_too_large", "Snowflake response exceeds 16 MiB. Reduce the query result or use a native streaming client.", { statusCode: 413 });
|
|
105
|
+
}
|
|
106
|
+
chunks.push(value);
|
|
107
|
+
}
|
|
108
|
+
} finally { reader.releaseLock(); }
|
|
109
|
+
let result;
|
|
110
|
+
try { result = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { /* rejected below */ }
|
|
111
|
+
if (sql) {
|
|
112
|
+
const reject = () => { throw new ConnectorError("connector_response_invalid", "Snowflake returned an invalid statement response.", { statusCode: 502 }); };
|
|
113
|
+
if (!object(result)) reject();
|
|
114
|
+
const requestedHandle = url.pathname.split("/")[4];
|
|
115
|
+
const handle = result.statementHandle ?? requestedHandle;
|
|
116
|
+
if (!uuid(handle) || (requestedHandle && requestedHandle !== handle)) reject();
|
|
117
|
+
if (response.status === 202 && result.code === "333334") return { status: "pending", handle };
|
|
118
|
+
if (response.status !== 200) reject();
|
|
119
|
+
if (url.pathname.endsWith("/cancel")) {
|
|
120
|
+
if (result.code !== "000604") reject();
|
|
121
|
+
return { status: "cancelled", handle };
|
|
122
|
+
}
|
|
123
|
+
if (result.code !== undefined && !["090001", "391908"].includes(result.code)) reject();
|
|
124
|
+
if (!Array.isArray(result.data) || !result.data.every((row) => Array.isArray(row) && row.every((cell) => cell === null || typeof cell === "string"))) reject();
|
|
125
|
+
const metadata = result.resultSetMetaData;
|
|
126
|
+
if (metadata !== undefined && (!object(metadata) || !Array.isArray(metadata.rowType) ||
|
|
127
|
+
!metadata.rowType.every((column) => object(column) && typeof column.name === "string" && typeof column.type === "string") ||
|
|
128
|
+
!result.data.every((row) => row.length === metadata.rowType.length))) reject();
|
|
129
|
+
if (metadata === undefined && !url.searchParams.has("partition")) reject();
|
|
130
|
+
return { status: "completed", handle, data: result.data, ...(metadata ? { metadata } : {}),
|
|
131
|
+
...(url.searchParams.has("partition") ? { partition: Number(url.searchParams.get("partition")) } : {}) };
|
|
132
|
+
}
|
|
133
|
+
if (response.status !== 200 || !Array.isArray(result) || result.length > Number(url.searchParams.get("showLimit"))) {
|
|
134
|
+
throw new ConnectorError("connector_response_invalid", "Snowflake returned an unexpected database page.", { statusCode: 502 });
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
export { snowflakeProvider };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { storyblokDefinition } from "../shared/tokens.js";
|
|
2
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
3
|
+
|
|
4
|
+
const origins = {
|
|
5
|
+
eu: "https://api.storyblok.com", us: "https://api-us.storyblok.com",
|
|
6
|
+
ca: "https://api-ca.storyblok.com", ap: "https://api-ap.storyblok.com",
|
|
7
|
+
cn: "https://app.storyblokchina.cn"
|
|
8
|
+
};
|
|
9
|
+
const contentFields = {
|
|
10
|
+
version: { type: "string", enum: ["published", "draft"], defaultTo: "published" },
|
|
11
|
+
language: { type: "string", maxLength: 64 },
|
|
12
|
+
cv: { type: "integer", min: 0 },
|
|
13
|
+
resolve_relations: { type: "string", minLength: 1, maxLength: 2048 },
|
|
14
|
+
resolve_links: { type: "string", enum: ["url", "link", "story"] },
|
|
15
|
+
resolve_assets: { type: "integer", enum: [0, 1] }
|
|
16
|
+
};
|
|
17
|
+
const story = (value) => Number.isSafeInteger(value?.id) && value.id > 0 &&
|
|
18
|
+
value.content !== null && typeof value.content === "object" && !Array.isArray(value.content);
|
|
19
|
+
const getStory = jsonOperation((settings) => `${origins[settings.region]}/v2/cdn/stories`, {
|
|
20
|
+
...contentFields,
|
|
21
|
+
id: { type: "string", required: true, minLength: 1, maxLength: 1024,
|
|
22
|
+
validator: (value) => value.split("/").every((part) => part && part !== "." && part !== "..") || "Use a story ID or full slug without empty or relative path segments." },
|
|
23
|
+
find_by: { type: "string", enum: ["uuid"] }
|
|
24
|
+
}, (result) => story(result?.story), "POST");
|
|
25
|
+
const storyblokProvider = Object.freeze({
|
|
26
|
+
...storyblokDefinition, apiOrigins: Object.values(origins),
|
|
27
|
+
apiKey: { queryParameter: "token" },
|
|
28
|
+
checkOperation: "space.read",
|
|
29
|
+
operations: {
|
|
30
|
+
"space.read": jsonOperation((settings) => `${origins[settings.region]}/v2/cdn/spaces/me`, {},
|
|
31
|
+
(result) => Number.isInteger(result?.space?.id) && typeof result.space.name === "string" &&
|
|
32
|
+
Array.isArray(result.space.language_codes)),
|
|
33
|
+
"stories.list": jsonOperation((settings) => `${origins[settings.region]}/v2/cdn/stories`, {
|
|
34
|
+
...contentFields,
|
|
35
|
+
page: { type: "integer", min: 1, max: 2147483647, defaultTo: 1 },
|
|
36
|
+
per_page: { type: "integer", min: 1, max: 100, defaultTo: 25 },
|
|
37
|
+
starts_with: { type: "string", maxLength: 1024 },
|
|
38
|
+
search_term: { type: "string", maxLength: 1024 },
|
|
39
|
+
content_type: { type: "string", maxLength: 255 },
|
|
40
|
+
}, (result) => Array.isArray(result?.stories) && result.stories.every(story)),
|
|
41
|
+
"stories.get": { ...getStory, request(input, settings) {
|
|
42
|
+
const { url, body: { id, ...query } } = getStory.request(input, settings);
|
|
43
|
+
const destination = new URL(`${url}/${id.split("/").map(encodeURIComponent).join("/")}`);
|
|
44
|
+
for (const [key, value] of Object.entries(query)) destination.searchParams.set(key, String(value));
|
|
45
|
+
return { method: "GET", url: destination.href };
|
|
46
|
+
} }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
export { storyblokProvider };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { stripeDefinition } from "../shared/tokens.js";
|
|
2
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
3
|
+
|
|
4
|
+
const stripeProvider = Object.freeze({
|
|
5
|
+
...stripeDefinition, apiOrigins: ["https://api.stripe.com"],
|
|
6
|
+
apiKey: { headers: (key) => ({ Authorization: `Bearer ${key}` }) },
|
|
7
|
+
checkOperation: "balance.read",
|
|
8
|
+
operations: {
|
|
9
|
+
"balance.read": jsonOperation("https://api.stripe.com/v1/balance", {},
|
|
10
|
+
(result) => result?.object === "balance" && Array.isArray(result.available) && Array.isArray(result.pending) && typeof result.livemode === "boolean")
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
export { stripeProvider };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { supabaseDefinition } from "../shared/tokens.js";
|
|
2
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
3
|
+
|
|
4
|
+
const supabaseProvider = Object.freeze({
|
|
5
|
+
...supabaseDefinition, apiOrigins: ["https://api.supabase.com"],
|
|
6
|
+
apiKey: { headers: (key) => ({ Authorization: `Bearer ${key}` }) },
|
|
7
|
+
checkOperation: "projects.list",
|
|
8
|
+
operations: {
|
|
9
|
+
"projects.list": jsonOperation("https://api.supabase.com/v1/projects", {},
|
|
10
|
+
(result) => Array.isArray(result) && result.every((item) => typeof item?.id === "string" && typeof item.name === "string"))
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
export { supabaseProvider };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
4
|
+
import { tallyDefinition } from "../shared/tokens.js";
|
|
5
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
6
|
+
|
|
7
|
+
const formIdField = { type: "string", required: true, noTrim: true,
|
|
8
|
+
validator: (value) => /^[A-Za-z0-9_-]{1,128}$/u.test(value) || "Enter a Tally form ID, not a URL." };
|
|
9
|
+
const blocksField = { type: "array", validator: (value) => value.length <= 500 &&
|
|
10
|
+
value.every((block) => block && typeof block === "object" && !Array.isArray(block) && typeof block.type === "string") || "Supply up to 500 native Tally block objects." };
|
|
11
|
+
const statusField = { type: "string", enum: ["BLANK", "DRAFT", "PUBLISHED"] };
|
|
12
|
+
const formResult = (result) => typeof result?.id === "string" && typeof result.name === "string" &&
|
|
13
|
+
["BLANK", "DRAFT", "PUBLISHED"].includes(result.status);
|
|
14
|
+
const formGetSchema = createSchema({ formId: formIdField });
|
|
15
|
+
const formUpdateSchema = createSchema({ formId: formIdField,
|
|
16
|
+
name: { type: "string", maxLength: 1000 }, status: statusField, blocks: blocksField });
|
|
17
|
+
|
|
18
|
+
const submissionsSchema = createSchema({
|
|
19
|
+
formId: { type: "string", required: true, noTrim: true,
|
|
20
|
+
validator: (value) => /^[A-Za-z0-9_-]{1,128}$/u.test(value) || "Enter a Tally form ID, not a URL." },
|
|
21
|
+
page: { type: "integer", min: 1, defaultTo: 1 },
|
|
22
|
+
limit: { type: "integer", min: 1, max: 500, defaultTo: 50 },
|
|
23
|
+
filter: { type: "string", enum: ["all", "completed", "partial"], defaultTo: "completed" },
|
|
24
|
+
afterId: { type: "string", noTrim: true,
|
|
25
|
+
validator: (value) => /^[A-Za-z0-9_-]{1,128}$/u.test(value) || "Use the returned submission ID." }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const tallyProvider = Object.freeze({
|
|
29
|
+
...tallyDefinition, apiOrigins: ["https://api.tally.so"],
|
|
30
|
+
apiKey: { headers: (key) => ({ Authorization: `Bearer ${key}`, "tally-version": "2025-02-01" }) },
|
|
31
|
+
checkOperation: "forms.list",
|
|
32
|
+
operations: {
|
|
33
|
+
"forms.create": jsonOperation("https://api.tally.so/forms", {
|
|
34
|
+
workspaceId: { ...formIdField, required: false },
|
|
35
|
+
templateId: { ...formIdField, required: false },
|
|
36
|
+
folderId: { ...formIdField, required: false },
|
|
37
|
+
status: { ...statusField, defaultTo: "DRAFT" },
|
|
38
|
+
blocks: { ...blocksField, required: true }
|
|
39
|
+
}, formResult, "POST"),
|
|
40
|
+
"forms.get": {
|
|
41
|
+
scopes: [],
|
|
42
|
+
request(input) {
|
|
43
|
+
const { formId } = validateSchemaPayload({ schema: formGetSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
44
|
+
return { method: "GET", url: `https://api.tally.so/forms/${formId}` };
|
|
45
|
+
},
|
|
46
|
+
validateResult: (result) => formResult(result) && Array.isArray(result.blocks)
|
|
47
|
+
},
|
|
48
|
+
"forms.update": {
|
|
49
|
+
scopes: [],
|
|
50
|
+
request(input) {
|
|
51
|
+
const { formId, ...body } = validateSchemaPayload({ schema: formUpdateSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
52
|
+
if (!Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Choose a name, status or block change.", { statusCode: 422 });
|
|
53
|
+
return { method: "PATCH", url: `https://api.tally.so/forms/${formId}`, body };
|
|
54
|
+
},
|
|
55
|
+
validateResult: formResult
|
|
56
|
+
},
|
|
57
|
+
"submissions.list": {
|
|
58
|
+
scopes: [],
|
|
59
|
+
request(input) {
|
|
60
|
+
const { formId, ...values } = validateSchemaPayload({ schema: submissionsSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
61
|
+
const url = new URL(`https://api.tally.so/forms/${formId}/submissions`);
|
|
62
|
+
for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
|
|
63
|
+
return { method: "GET", url: url.href };
|
|
64
|
+
},
|
|
65
|
+
validateResult: (result) => Number.isSafeInteger(result?.page) && result.page >= 1 &&
|
|
66
|
+
Number.isSafeInteger(result.limit) && result.limit >= 1 && result.limit <= 500 &&
|
|
67
|
+
typeof result.hasMore === "boolean" && Array.isArray(result.questions) &&
|
|
68
|
+
Array.isArray(result.submissions) && result.submissions.every((submission) =>
|
|
69
|
+
typeof submission?.id === "string" && typeof submission.formId === "string" &&
|
|
70
|
+
typeof submission.isCompleted === "boolean" && Array.isArray(submission.responses))
|
|
71
|
+
},
|
|
72
|
+
"forms.list": jsonOperation("https://api.tally.so/forms", {
|
|
73
|
+
limit: { type: "integer", min: 1, max: 500, defaultTo: 50 },
|
|
74
|
+
page: { type: "integer", min: 1, defaultTo: 1 }
|
|
75
|
+
}, (result) => Array.isArray(result?.items) && Number.isInteger(result.total) && typeof result.hasMore === "boolean")
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
export { tallyProvider };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
2
|
+
import { telegramDefinition } from "../shared/tokens.js";
|
|
3
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
4
|
+
|
|
5
|
+
function telegramResult(response, validate) {
|
|
6
|
+
if (response?.ok === false) {
|
|
7
|
+
const error = new Error("Telegram rejected the operation.");
|
|
8
|
+
error.status = response.error_code;
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
return response?.ok === true && validate(response.result);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const chatIdField = { type: "string", required: true, noTrim: true,
|
|
15
|
+
validator: (value) => /^-?[1-9][0-9]{0,15}$/u.test(value) || /^@[A-Za-z][A-Za-z0-9_]{4,31}$/u.test(value) || "Enter a chat ID as a decimal string or @username." };
|
|
16
|
+
|
|
17
|
+
const telegramProvider = Object.freeze({
|
|
18
|
+
...telegramDefinition, requestTimeoutMs: 30000,
|
|
19
|
+
apiOrigins: ["https://api.telegram.org"],
|
|
20
|
+
apiKey: { pathPrefix(key) {
|
|
21
|
+
if (!/^\d+:[A-Za-z0-9_-]+$/.test(key)) {
|
|
22
|
+
throw new ConnectorError("connector_binding_invalid", "Configure a Telegram bot token in the referenced environment variable.");
|
|
23
|
+
}
|
|
24
|
+
return `/bot${key}`;
|
|
25
|
+
} },
|
|
26
|
+
checkOperation: "profile.read",
|
|
27
|
+
operations: {
|
|
28
|
+
"messages.send": jsonOperation("https://api.telegram.org/sendMessage", {
|
|
29
|
+
chat_id: chatIdField,
|
|
30
|
+
text: { type: "string", required: true, noTrim: true, minLength: 1, maxLength: 4096 },
|
|
31
|
+
message_thread_id: { type: "integer", min: 1 },
|
|
32
|
+
disable_notification: { type: "boolean", defaultTo: false },
|
|
33
|
+
protect_content: { type: "boolean", defaultTo: false }
|
|
34
|
+
}, (response) => telegramResult(response, (message) => Number.isSafeInteger(message?.message_id) &&
|
|
35
|
+
Number.isSafeInteger(message.chat?.id) && typeof message.text === "string"), "POST"),
|
|
36
|
+
"chats.action": jsonOperation("https://api.telegram.org/sendChatAction", {
|
|
37
|
+
chat_id: chatIdField, message_thread_id: { type: "integer", min: 1 },
|
|
38
|
+
action: { type: "string", required: true, enum: ["typing", "upload_photo", "record_video", "upload_video", "record_voice", "upload_voice", "upload_document", "choose_sticker", "find_location", "record_video_note", "upload_video_note"] }
|
|
39
|
+
}, (response) => telegramResult(response, (value) => value === true), "POST"),
|
|
40
|
+
"updates.poll": jsonOperation("https://api.telegram.org/getUpdates", {
|
|
41
|
+
offset: { type: "integer", min: 0 },
|
|
42
|
+
limit: { type: "integer", min: 1, max: 100, defaultTo: 100 },
|
|
43
|
+
timeout: { type: "integer", min: 0, max: 20, defaultTo: 20 },
|
|
44
|
+
allowed_updates: { type: "array", defaultTo: ["message"],
|
|
45
|
+
validator: (value) => value.length > 0 && value.length <= 4 && value.every((kind) => ["message", "edited_message", "channel_post", "edited_channel_post"].includes(kind)) || "Choose supported message update types explicitly." }
|
|
46
|
+
}, (response) => telegramResult(response, (updates) => Array.isArray(updates) &&
|
|
47
|
+
updates.every((update) => Number.isSafeInteger(update?.update_id) && update.update_id >= 0)), "POST"),
|
|
48
|
+
"profile.read": jsonOperation("https://api.telegram.org/getMe", {}, (response) => telegramResult(response,
|
|
49
|
+
(bot) => Number.isSafeInteger(bot?.id) && bot.id > 0 && bot.is_bot === true && typeof bot.first_name === "string")),
|
|
50
|
+
"webhook.read": jsonOperation("https://api.telegram.org/getWebhookInfo", {}, (response) => telegramResult(response,
|
|
51
|
+
(webhook) => typeof webhook?.url === "string" && typeof webhook.has_custom_certificate === "boolean" &&
|
|
52
|
+
Number.isInteger(webhook.pending_update_count) && webhook.pending_update_count >= 0))
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
export { telegramProvider };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
2
|
+
import { tiktokDefinition } from "../shared/tiktok.js";
|
|
3
|
+
import { jsonOperation } from "./jsonOperation.js";
|
|
4
|
+
|
|
5
|
+
const textId = (value) => typeof value === "string" && value.length > 0 && value.length <= 1024 && !/[\p{Cc}]/u.test(value);
|
|
6
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
7
|
+
|
|
8
|
+
function checkReply(value) {
|
|
9
|
+
const code = value?.error?.code ?? value?.code;
|
|
10
|
+
if (code === "ok") return value;
|
|
11
|
+
if (typeof code !== "string" || !code) throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid response.", { statusCode: 502 });
|
|
12
|
+
if (code === "access_token_invalid") throw new ConnectorError("connector_reconnect_required", "Connect this TikTok account again.", { statusCode: 401 });
|
|
13
|
+
if (["scope_not_authorized", "scope_permission_missed"].includes(code)) {
|
|
14
|
+
throw new ConnectorError("connector_scope_missing", "Connect TikTok with the permission required for this operation.", { statusCode: 403 });
|
|
15
|
+
}
|
|
16
|
+
if (code === "rate_limit_exceeded") throw new ConnectorError("connector_rate_limited", "TikTok's request limit was reached.", { statusCode: 429 });
|
|
17
|
+
throw new ConnectorError("connector_provider_failed", "TikTok could not complete the request.", { statusCode: 502 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function profileOperation(fields, scope, validateUser) {
|
|
21
|
+
return { ...jsonOperation(`https://open.tiktokapis.com/v2/user/info/?fields=${fields.join(",")}`, {},
|
|
22
|
+
(value) => value?.error?.code === "ok" && validateUser(value.data?.user)), scopes: [scope] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tiktokProvider = Object.freeze({
|
|
26
|
+
...tiktokDefinition,
|
|
27
|
+
oauth: { issuer: "https://www.tiktok.com", authorization_endpoint: "https://www.tiktok.com/v2/auth/authorize/", token_endpoint: "https://open.tiktokapis.com/v2/oauth/token/" },
|
|
28
|
+
oauthClientIdParameter: "client_key", scopeSeparator: ",",
|
|
29
|
+
validateCallbackUrl: (value) => value.startsWith("https://") && value.length < 512,
|
|
30
|
+
async normalizeTokenResponse(response) {
|
|
31
|
+
let value;
|
|
32
|
+
try { value = await response.clone().json(); } catch {
|
|
33
|
+
if (!response.ok) return response;
|
|
34
|
+
throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid token response.", { statusCode: 502 });
|
|
35
|
+
}
|
|
36
|
+
if (!response.ok) return response;
|
|
37
|
+
if (value?.error) {
|
|
38
|
+
if (value.error === "invalid_grant") throw new ConnectorError("connector_reconnect_required", "Connect this TikTok account again.", { statusCode: 401 });
|
|
39
|
+
throw new ConnectorError("connector_provider_failed", "TikTok could not authorize this connection.", { statusCode: 502 });
|
|
40
|
+
}
|
|
41
|
+
if (!textId(value?.open_id) || !Number.isSafeInteger(value.expires_in) || value.expires_in <= 0 ||
|
|
42
|
+
!Number.isSafeInteger(value.refresh_expires_in) || value.refresh_expires_in <= 0 || !textId(value.refresh_token) ||
|
|
43
|
+
typeof value.scope !== "string" || !/^[^\s,]+(?:,[^\s,]+)*$/u.test(value.scope)) {
|
|
44
|
+
throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid permission grant.", { statusCode: 502 });
|
|
45
|
+
}
|
|
46
|
+
const scopes = value.scope.split(",");
|
|
47
|
+
if (new Set(scopes).size !== scopes.length) throw new ConnectorError("connector_response_invalid", "TikTok returned duplicate permissions.", { statusCode: 502 });
|
|
48
|
+
if (!scopes.includes("user.info.basic")) throw new ConnectorError("connector_scope_missing", "Connect TikTok with basic profile permission.", { statusCode: 403 });
|
|
49
|
+
return response;
|
|
50
|
+
},
|
|
51
|
+
apiOrigins: ["https://open.tiktokapis.com"], checkOperation: "profile.read",
|
|
52
|
+
operations: {
|
|
53
|
+
"profile.read": profileOperation(["open_id", "display_name", "avatar_url"], "user.info.basic", (user) =>
|
|
54
|
+
textId(user?.open_id) && typeof user.display_name === "string" && typeof user.avatar_url === "string"),
|
|
55
|
+
"profile.extended": profileOperation(["username", "bio_description", "profile_deep_link", "is_verified"], "user.info.profile", (user) =>
|
|
56
|
+
["username", "bio_description", "profile_deep_link"].every((field) => typeof user?.[field] === "string") && typeof user?.is_verified === "boolean"),
|
|
57
|
+
"profile.stats": profileOperation(["follower_count", "following_count", "likes_count", "video_count"], "user.info.stats", (user) =>
|
|
58
|
+
["follower_count", "following_count", "likes_count", "video_count"].every((field) => count(user?.[field]))),
|
|
59
|
+
"videos.list": {
|
|
60
|
+
...jsonOperation("https://open.tiktokapis.com/v2/video/list/?fields=id,title,cover_image_url", {
|
|
61
|
+
max_count: { type: "integer", min: 1, max: 20, defaultTo: 10 },
|
|
62
|
+
cursor: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER }
|
|
63
|
+
}, (value) => value?.error?.code === "ok" && Array.isArray(value.data?.videos) && value.data.videos.length <= 20 &&
|
|
64
|
+
value.data.videos.every((video) => textId(video?.id) && typeof video.title === "string" && typeof video.cover_image_url === "string") &&
|
|
65
|
+
typeof value.data.has_more === "boolean" && count(value.data.cursor), "POST"),
|
|
66
|
+
scopes: ["video.list"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async exchange(address, options, { fetchImpl }) {
|
|
70
|
+
const response = await fetchImpl(address, { ...options, credentials: "omit",
|
|
71
|
+
headers: { ...options.headers, ...(options.body ? { "Content-Type": "application/json" } : {}) },
|
|
72
|
+
...(options.body ? { body: JSON.stringify(options.body) } : {}) });
|
|
73
|
+
let value;
|
|
74
|
+
try { value = await response.json(); } catch {
|
|
75
|
+
throw new ConnectorError("connector_response_invalid", "TikTok returned an invalid response.", { statusCode: 502 });
|
|
76
|
+
}
|
|
77
|
+
checkReply(value);
|
|
78
|
+
if (!response.ok) throw new ConnectorError("connector_provider_failed", "TikTok could not complete the request.", { statusCode: 502 });
|
|
79
|
+
if (options.body?.max_count !== undefined && value.data?.videos?.length > options.body.max_count) {
|
|
80
|
+
throw new ConnectorError("connector_response_invalid", "TikTok returned too many videos.", { statusCode: 502 });
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export { tiktokProvider };
|