@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,391 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createConnectionService } from "../../connectors-core/src/server/index.js";
|
|
7
|
+
import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
|
|
8
|
+
import { getProviderScopes, parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
|
|
9
|
+
import { snowflakeProvider } from "../src/server/snowflake.js";
|
|
10
|
+
|
|
11
|
+
const context = { applicationId: "app-one", subjectId: "owner-one" };
|
|
12
|
+
const args = { context, integrationId: "warehouse" };
|
|
13
|
+
const callback = "https://app.example.test/oauth/snowflake/callback";
|
|
14
|
+
const database = { name: "INVENTORY", owner: "VIBE64_READER", kind: "PERMANENT", comment: "Stock", created_on: "2026-09-01T12:00:00Z" };
|
|
15
|
+
async function fixture(t, { role = "VIBE64_READER", accountUrl = "https://myorg-myaccount.snowflakecomputing.com", basic = false, sqlContext = {} } = {}) {
|
|
16
|
+
const directory = await mkdtemp(path.join(tmpdir(), "snowflake-connector-"));
|
|
17
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
18
|
+
const settings = { accountUrl, ...(role ? { role } : {}), ...sqlContext };
|
|
19
|
+
const scopes = getProviderScopes(snowflakeProvider, settings).map((scope) => scope.value);
|
|
20
|
+
const configuration = { schemaVersion: 1, registrations: { snowflake: { source: "own", clientId: "fixture+client/id",
|
|
21
|
+
clientSecretRef: "env:SNOWFLAKE_SECRET", callbackUrlRef: "env:SNOWFLAKE_CALLBACK", ...(basic ? { tokenEndpointAuthMethod: "client_secret_basic" } : {}) } }, integrations: {
|
|
22
|
+
warehouse: { provider: "snowflake", accountMode: "per-user", settings, scopes,
|
|
23
|
+
authentication: { method: "oauth2", registrationRef: "snowflake" } }
|
|
24
|
+
} };
|
|
25
|
+
const state = { time: Date.now(), count: 0, value: undefined, status: 200, tokenPatch: {}, tokenStatus: 200, hang: false, secret: "private+secret/=" };
|
|
26
|
+
const requests = [];
|
|
27
|
+
const options = { configuration, providers: [{ ...snowflakeProvider, requestTimeoutMs: 50 }], authorize: async (owner) => owner,
|
|
28
|
+
now: () => state.time, resolveReference: async (reference) => {
|
|
29
|
+
if (reference === "env:SNOWFLAKE_SECRET") return state.secret;
|
|
30
|
+
assert.equal(reference, "env:SNOWFLAKE_CALLBACK"); return callback;
|
|
31
|
+
},
|
|
32
|
+
store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(6) }, activeKeyId: "current" }) }),
|
|
33
|
+
fetchImpl: async (address, init) => {
|
|
34
|
+
const url = new URL(String(address)); requests.push({ url, init });
|
|
35
|
+
assert.equal(url.origin, new URL(accountUrl).origin);
|
|
36
|
+
const headers = new Headers(init.headers);
|
|
37
|
+
if (url.pathname === "/oauth/token-request") {
|
|
38
|
+
assert.equal(init.method, "POST"); const body = new URLSearchParams(init.body);
|
|
39
|
+
if (basic) {
|
|
40
|
+
const auth = headers.get("authorization"); assert.equal(auth.split(" ")[0], "Basic");
|
|
41
|
+
assert.deepEqual(Buffer.from(auth.split(" ")[1], "base64").toString().split(":").map(decodeURIComponent), ["fixture+client/id", state.secret]);
|
|
42
|
+
assert.equal(body.has("client_secret"), false); assert.equal(body.has("client_id"), false);
|
|
43
|
+
} else {
|
|
44
|
+
assert.equal(body.get("client_id"), "fixture+client/id"); assert.equal(body.get("client_secret"), state.secret);
|
|
45
|
+
assert.equal(headers.has("authorization"), false);
|
|
46
|
+
}
|
|
47
|
+
if (body.get("grant_type") === "authorization_code") {
|
|
48
|
+
assert.equal(body.get("redirect_uri"), callback); assert.equal(body.get("code"), "private-code"); assert.ok(body.get("code_verifier"));
|
|
49
|
+
} else {
|
|
50
|
+
assert.equal(body.get("grant_type"), "refresh_token"); assert.equal(body.get("refresh_token"), `private-refresh-${state.count}`);
|
|
51
|
+
}
|
|
52
|
+
state.count++;
|
|
53
|
+
return Response.json(state.tokenStatus === 200 ? { access_token: `private-access-${state.count}`, token_type: "Bearer", expires_in: 120,
|
|
54
|
+
refresh_token: `private-refresh-${state.count}`, ...state.tokenPatch }
|
|
55
|
+
: { error: "invalid_grant", message: "private-provider-error", success: false }, { status: state.tokenStatus });
|
|
56
|
+
}
|
|
57
|
+
if (state.sqlReply && url.pathname.startsWith("/api/v2/statements")) return state.sqlReply(url, init);
|
|
58
|
+
assert.equal(init.method, "GET"); assert.equal(init.redirect, "error"); assert.equal(init.credentials, "omit");
|
|
59
|
+
assert.equal(headers.get("authorization"), `Bearer private-access-${state.count}`);
|
|
60
|
+
assert.equal(headers.get("x-snowflake-authorization-token-type"), "OAUTH");
|
|
61
|
+
assert.equal(headers.get("x-snowflake-role"), role ? `"${role}"` : null);
|
|
62
|
+
assert.equal(url.pathname, "/api/v2/databases");
|
|
63
|
+
if (state.hang) return new Promise((_, reject) => {
|
|
64
|
+
const timer = setTimeout(() => reject(new Error("Missing abort")), 1000);
|
|
65
|
+
const abort = () => { clearTimeout(timer); reject(init.signal.reason); };
|
|
66
|
+
if (init.signal.aborted) abort(); else init.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
});
|
|
68
|
+
if (state.status === 204) return new Response(null, { status: 204 });
|
|
69
|
+
if (typeof state.value === "string") return new Response(state.value, { status: state.status });
|
|
70
|
+
return Response.json(state.value === undefined ? [database] : state.value, { status: state.status });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const service = createConnectionService(options);
|
|
74
|
+
const start = async () => {
|
|
75
|
+
const { authorizationUrl } = await service.beginAuthorization({ ...args, verificationInput: { showLimit: 1 } });
|
|
76
|
+
const url = new URL(authorizationUrl); const returned = new URL(callback);
|
|
77
|
+
returned.searchParams.set("state", url.searchParams.get("state")); returned.searchParams.set("code", "private-code"); returned.searchParams.set("scope", url.searchParams.get("scope"));
|
|
78
|
+
return { url, callbackUrl: returned.href };
|
|
79
|
+
};
|
|
80
|
+
const connect = async () => service.completeAuthorization({ ...args, callbackUrl: (await start()).callbackUrl });
|
|
81
|
+
return { directory, service, state, options, requests, start, connect };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test("Snowflake binds PKCE and credentials to the account and persists encrypted restartable consent", async (t) => {
|
|
85
|
+
const f = await fixture(t); const start = await f.start();
|
|
86
|
+
assert.equal(start.url.href.split("?")[0], "https://myorg-myaccount.snowflakecomputing.com/oauth/authorize");
|
|
87
|
+
assert.equal(start.url.searchParams.get("scope"), "refresh_token session:role:VIBE64_READER");
|
|
88
|
+
assert.equal(start.url.searchParams.get("code_challenge_method"), "S256"); assert.equal(start.url.searchParams.has("client_secret"), false);
|
|
89
|
+
await f.service.completeAuthorization({ ...args, callbackUrl: start.callbackUrl });
|
|
90
|
+
for (const file of await readdir(f.directory)) {
|
|
91
|
+
const value = await readFile(path.join(f.directory, file), "utf8");
|
|
92
|
+
for (const secret of ["private-code", "private+secret/=", "private-access-1", "private-refresh-1"]) assert.equal(value.includes(secret), false);
|
|
93
|
+
}
|
|
94
|
+
const restarted = createConnectionService(f.options);
|
|
95
|
+
assert.equal((await restarted.status(args)).status, "connected");
|
|
96
|
+
assert.deepEqual((await restarted.status(args)).grantedScopes, ["refresh_token", "session:role:VIBE64_READER"]);
|
|
97
|
+
assert.deepEqual(await restarted.invoke({ ...args, operation: "databases.list" }), [database]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("Snowflake supports optional default roles, encoded case-sensitive roles, Basic auth and account URL forms", async (t) => {
|
|
101
|
+
for (const setting of [{ role: "", accountUrl: "https://myorg-my_account.snowflakecomputing.com/" },
|
|
102
|
+
{ role: "Inventory & stock", basic: true, accountUrl: "https://xy123.eu-west-1.aws.snowflakecomputing.com" },
|
|
103
|
+
{ role: "PUBLIC", accountUrl: "https://myorg-myaccount.privatelink.snowflakecomputing.com" }]) {
|
|
104
|
+
const f = await fixture(t, setting); const start = await f.start();
|
|
105
|
+
assert.equal(start.url.searchParams.get("scope"), setting.role === "" ? "refresh_token" : setting.role === "PUBLIC" ? "refresh_token session:role:PUBLIC"
|
|
106
|
+
: "refresh_token session:role-encoded:Inventory%20%26%20stock");
|
|
107
|
+
await f.service.completeAuthorization({ ...args, callbackUrl: start.callbackUrl });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Snowflake returns bounded filtered pages and empty accounts without following supplied continuation URLs", async (t) => {
|
|
112
|
+
const f = await fixture(t); await f.connect();
|
|
113
|
+
await f.service.invoke({ ...args, operation: "databases.list", input: { showLimit: 3, like: "WARE%", startsWith: "WARE", fromName: "WARE A&B", history: true } });
|
|
114
|
+
assert.equal(f.requests.at(-1).url.search, "?showLimit=3&like=WARE%25&startsWith=WARE&fromName=WARE+A%26B&history=true");
|
|
115
|
+
f.state.value = []; assert.deepEqual(await f.service.invoke({ ...args, operation: "databases.list", input: { fromName: "INVENTORY" } }), []);
|
|
116
|
+
assert.equal(f.requests.length, 4);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("Snowflake rejects oversized, unsupported and mutating operation input before transport", async (t) => {
|
|
120
|
+
const f = await fixture(t); await f.connect(); const before = f.requests.length;
|
|
121
|
+
for (const input of [{ showLimit: 0 }, { showLimit: 1001 }, { showLimit: 1.5 }, { history: "wrong" }, { fromName: "" },
|
|
122
|
+
{ fromName: "x".repeat(256) }, { like: "" }, { accountUrl: "https://other.test" }, { url: "https://other.test" }, { role: "ACCOUNTADMIN" }, { statement: "SELECT 1" }]) {
|
|
123
|
+
await assert.rejects(f.service.invoke({ ...args, operation: "databases.list", input }), { code: "connector_input_invalid" });
|
|
124
|
+
}
|
|
125
|
+
for (const operation of ["databases.create", "databases.delete", "sql.execute"]) await assert.rejects(f.service.invoke({ ...args, operation }), { code: "connector_operation_unknown" });
|
|
126
|
+
assert.equal(f.requests.length, before);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("Snowflake isolates application users and invalidates saved grants after account or role edits", async (t) => {
|
|
130
|
+
const f = await fixture(t); await f.connect();
|
|
131
|
+
for (const owner of [{ ...context, subjectId: "another" }, { ...context, applicationId: "another" }]) {
|
|
132
|
+
await assert.rejects(f.service.invoke({ ...args, context: owner, operation: "databases.list" }), { code: "connector_reconnect_required" });
|
|
133
|
+
}
|
|
134
|
+
const denied = createConnectionService({ ...f.options, authorize: async () => null });
|
|
135
|
+
await assert.rejects(denied.invoke({ ...args, operation: "databases.list" }), { code: "connector_access_denied" });
|
|
136
|
+
for (const settings of [{ accountUrl: "https://other-account.snowflakecomputing.com", role: "VIBE64_READER" }, { accountUrl: "https://myorg-myaccount.snowflakecomputing.com", role: "PUBLIC" }]) {
|
|
137
|
+
const configuration = structuredClone(f.options.configuration); configuration.integrations.warehouse.settings = settings;
|
|
138
|
+
configuration.integrations.warehouse.scopes = getProviderScopes(snowflakeProvider, settings).map((scope) => scope.value);
|
|
139
|
+
const changed = createConnectionService({ ...f.options, configuration });
|
|
140
|
+
assert.equal((await changed.status(args)).status, "reconnect-required");
|
|
141
|
+
await assert.rejects(changed.invoke({ ...args, operation: "databases.list" }), { code: "connector_reconnect_required" });
|
|
142
|
+
}
|
|
143
|
+
assert.equal(f.requests.length, 2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("Snowflake saves rotated refresh tokens before a failed read and marks revoked tokens for reconnection", async (t) => {
|
|
147
|
+
const f = await fixture(t, { basic: true }); await f.connect(); f.state.time += 121_000; f.state.status = 429; f.state.secret = "rotated+secret/=";
|
|
148
|
+
await assert.rejects(f.service.invoke({ ...args, operation: "databases.list" }), { code: "connector_rate_limited" });
|
|
149
|
+
f.state.status = 200; const restarted = createConnectionService(f.options);
|
|
150
|
+
await restarted.invoke({ ...args, operation: "databases.list" }); assert.equal(f.state.count, 2);
|
|
151
|
+
f.state.time += 121_000; f.state.tokenStatus = 400;
|
|
152
|
+
await assert.rejects(restarted.invoke({ ...args, operation: "databases.list" }), (error) => error.code === "connector_reconnect_required" && !error.message.includes("private"));
|
|
153
|
+
assert.equal((await restarted.status(args)).status, "reconnect-required");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("Snowflake accepts short-lived consent without refresh and rejects malformed grants and ambiguous callback scopes", async (t) => {
|
|
157
|
+
const short = await fixture(t); short.state.tokenPatch.refresh_token = undefined; await short.connect(); short.state.time += 121_000;
|
|
158
|
+
await assert.rejects(short.service.invoke({ ...args, operation: "databases.list" }), { code: "connector_reconnect_required" });
|
|
159
|
+
assert.equal(short.requests.length, 2);
|
|
160
|
+
for (const tokenPatch of [{ expires_in: undefined }, { expires_in: 0 }, { expires_in: 1.5 }, { scope: "" }, { scope: "refresh_token\nadmin" }, { refresh_token: "" }]) {
|
|
161
|
+
const f = await fixture(t); f.state.tokenPatch = tokenPatch;
|
|
162
|
+
await assert.rejects(f.connect(), { code: "connector_response_invalid" }); assert.equal(f.requests.length, 1);
|
|
163
|
+
}
|
|
164
|
+
for (const duplicate of [false, true]) {
|
|
165
|
+
const f = await fixture(t); const start = await f.start(); const url = new URL(start.callbackUrl);
|
|
166
|
+
if (duplicate) url.searchParams.append("scope", "refresh_token"); else url.searchParams.delete("scope");
|
|
167
|
+
await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: url.href }), { code: "connector_response_invalid" }); assert.equal(f.requests.length, 0);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("Snowflake consumes denied, cancelled, mismatched and replayed attempts while preserving earlier access", async (t) => {
|
|
172
|
+
const f = await fixture(t); await f.connect(); const start = await f.start();
|
|
173
|
+
const configuration = structuredClone(f.options.configuration); configuration.integrations.warehouse.settings.accountUrl = "https://other-account.snowflakecomputing.com";
|
|
174
|
+
const other = createConnectionService({ ...f.options, configuration });
|
|
175
|
+
await assert.rejects(other.completeAuthorization({ ...args, callbackUrl: start.callbackUrl }), { code: "connector_attempt_invalid" });
|
|
176
|
+
const denied = await f.start(); const returned = new URL(denied.callbackUrl); returned.searchParams.delete("code"); returned.searchParams.set("error", "access_denied");
|
|
177
|
+
await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: returned.href }), { code: "connector_consent_denied" });
|
|
178
|
+
await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: denied.callbackUrl }), { code: "connector_attempt_invalid" });
|
|
179
|
+
const cancelled = await f.start(); await f.service.cancelAuthorization({ ...args, state: cancelled.url.searchParams.get("state") });
|
|
180
|
+
await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: cancelled.callbackUrl }), { code: "connector_attempt_invalid" });
|
|
181
|
+
assert.equal((await f.service.status(args)).status, "connected"); assert.equal(f.requests.length, 2);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("Snowflake rejects malformed, over-limit and asynchronous pages and sanitizes provider failures", async (t) => {
|
|
185
|
+
const f = await fixture(t); await f.connect();
|
|
186
|
+
for (const value of [null, {}, { code: "390303", message: "private-error" }, [null], [{}], [{ name: "" }], [{ name: 4 }], [{ name: "ok", owner: 1 }], "<html>error</html>", [database, database]]) {
|
|
187
|
+
f.state.value = value; await assert.rejects(f.service.invoke({ ...args, operation: "databases.list", input: { showLimit: 1 } }), { code: "connector_response_invalid" });
|
|
188
|
+
}
|
|
189
|
+
for (const status of [202, 204]) { f.state.status = status; f.state.value = []; await assert.rejects(f.service.invoke({ ...args, operation: "databases.list" }), { code: "connector_response_invalid" }); }
|
|
190
|
+
for (const [status, code] of [[403, "connector_permission_denied"], [500, "connector_provider_failed"], [429, "connector_rate_limited"], [401, "connector_reconnect_required"]]) {
|
|
191
|
+
f.state.status = status; f.state.value = { message: "private-provider-error" };
|
|
192
|
+
await assert.rejects(f.service.invoke({ ...args, operation: "databases.list" }), (error) => error.code === code && !error.message.includes("private"));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("Snowflake bounds cancellation and timeout and disconnects locally", async (t) => {
|
|
197
|
+
const f = await fixture(t); await f.connect(); f.state.hang = true;
|
|
198
|
+
const controller = new AbortController(); const pending = f.service.invoke({ ...args, operation: "databases.list", signal: controller.signal });
|
|
199
|
+
setTimeout(() => controller.abort(), 10); await assert.rejects(pending, { code: "connector_cancelled" });
|
|
200
|
+
await assert.rejects(f.service.invoke({ ...args, operation: "databases.list" }), { code: "connector_provider_timeout" });
|
|
201
|
+
await f.service.disconnect(args); assert.equal((await f.service.status(args)).status, "disconnected"); assert.equal(f.requests.length, 4);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("Snowflake guide JSON, CLI validation and dynamic form permissions describe the same configuration", async () => {
|
|
205
|
+
const guide = await readFile(new URL("../docs/snowflake.md", import.meta.url), "utf8");
|
|
206
|
+
const config = JSON.parse([...guide.matchAll(/```json\n([\s\S]*?)\n```/gu)][0][1]);
|
|
207
|
+
const parse = (value) => parseIntegrationConfiguration(JSON.stringify(value), { providers: [snowflakeProvider] });
|
|
208
|
+
assert.deepEqual(parse(config), config);
|
|
209
|
+
for (const accountUrl of ["", "http://myorg-account.snowflakecomputing.com", "https://app.snowflake.com/org/account", "https://myorg-account.snowflakecomputing.com.attacker.test",
|
|
210
|
+
"https://myorg-account.snowflakecomputing.com/path", "https://myorg-account.snowflakecomputing.com/?o=1", "https://myorg-account.snowflakecomputing.com/#page",
|
|
211
|
+
"https://user@myorg-account.snowflakecomputing.com", "https://myorg-account.snowflakecomputing.com:8443", "https://myorg-account.snowflakecomputing.com/../", "https://127.0.0.1"]) {
|
|
212
|
+
const invalid = structuredClone(config); invalid.integrations.warehouse.settings.accountUrl = accountUrl;
|
|
213
|
+
assert.throws(() => parse(invalid), (error) => Boolean(error.fieldErrors["integrations.warehouse.settings.accountUrl"]));
|
|
214
|
+
}
|
|
215
|
+
for (const role of ["ACCOUNTADMIN", "SECURITYADMIN", "ORGADMIN", "GLOBALORGADMIN", "admin\nreader", '"READER"', "a\\b", "équipe", "x".repeat(256)]) {
|
|
216
|
+
const invalid = structuredClone(config); invalid.integrations.warehouse.settings.role = role;
|
|
217
|
+
assert.throws(() => parse(invalid), (error) => Boolean(error.fieldErrors["integrations.warehouse.settings.role"]));
|
|
218
|
+
}
|
|
219
|
+
const stale = structuredClone(config); stale.integrations.warehouse.settings.role = "PUBLIC";
|
|
220
|
+
assert.throws(() => parse(stale), (error) => Boolean(error.fieldErrors["integrations.warehouse.scopes"]));
|
|
221
|
+
const defaults = structuredClone(config); delete defaults.integrations.warehouse.settings.role; defaults.integrations.warehouse.scopes = ["refresh_token"];
|
|
222
|
+
assert.deepEqual(parse(defaults), defaults);
|
|
223
|
+
for (const [key, value] of [["callbackUrlRef", callback], ["clientSecretRef", "raw-secret"], ["tokenEndpointAuthMethod", "none"], ["grantType", "client_credentials"]]) {
|
|
224
|
+
const invalid = structuredClone(config); invalid.registrations.snowflake[key] = value;
|
|
225
|
+
assert.throws(() => parse(invalid), (error) => Boolean(error.fieldErrors[`registrations.snowflake.${key}`]));
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
test("Snowflake retains exact SQL context names in portable configuration", async (t) => {
|
|
231
|
+
const { options } = await fixture(t);
|
|
232
|
+
options.configuration.integrations.warehouse.settings = { ...options.configuration.integrations.warehouse.settings,
|
|
233
|
+
warehouse: "AppCompute", database: "AppData", schema: "Reporting" };
|
|
234
|
+
const configuration = parseIntegrationConfiguration(JSON.stringify(options.configuration), { providers: [snowflakeProvider] });
|
|
235
|
+
assert.equal(configuration.integrations.warehouse.settings.warehouse, "AppCompute");
|
|
236
|
+
assert.equal(configuration.integrations.warehouse.settings.database, "AppData");
|
|
237
|
+
assert.equal(configuration.integrations.warehouse.settings.schema, "Reporting");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
test("Snowflake SQL lifecycle binds context, preserves typed partitions and cancels explicitly", async (t) => {
|
|
242
|
+
const { service: runtime, state, requests, connect } = await fixture(t, { sqlContext: { warehouse: "APP_WH", database: "DATA", schema: "PUBLIC" } });
|
|
243
|
+
await connect();
|
|
244
|
+
const handle = "019c07a7-0000-df4f-0000-001000067872";
|
|
245
|
+
const requestId = "536fad38-b564-4dc5-9892-a4543504df6c";
|
|
246
|
+
let phase = 0;
|
|
247
|
+
state.sqlReply = (url, init) => {
|
|
248
|
+
assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-access-1");
|
|
249
|
+
assert.equal(new Headers(init.headers).get("x-snowflake-role"), '\"VIBE64_READER\"');
|
|
250
|
+
assert.equal(init.redirect, "error");
|
|
251
|
+
if (phase++ === 0) {
|
|
252
|
+
assert.equal(init.method, "POST");
|
|
253
|
+
assert.equal(url.searchParams.get("requestId"), requestId);
|
|
254
|
+
assert.equal(url.searchParams.get("async"), "true");
|
|
255
|
+
assert.deepEqual(JSON.parse(init.body), { statement: "select ?", bindings: { "1": { type: "FIXED", value: "9007199254740993" } }, timeout: 60,
|
|
256
|
+
warehouse: "APP_WH", database: "DATA", schema: "PUBLIC", role: "VIBE64_READER", parameters: { multi_statement_count: "1" } });
|
|
257
|
+
return Response.json({ code: "333334", statementHandle: handle, statementStatusUrl: "https://untrusted.test/never-follow" }, { status: 202 });
|
|
258
|
+
}
|
|
259
|
+
assert.equal(url.pathname, `/api/v2/statements/${handle}${phase === 4 ? "/cancel" : ""}`);
|
|
260
|
+
if (phase === 2) return Response.json({ code: "090001", statementHandle: handle,
|
|
261
|
+
resultSetMetaData: { rowType: [{ name: "COUNT", type: "fixed", scale: 0 }], partitionInfo: [{ rowCount: 1 }, { rowCount: 1 }] }, data: [["9007199254740993"]] });
|
|
262
|
+
if (phase === 3) {
|
|
263
|
+
assert.equal(url.searchParams.get("partition"), "1");
|
|
264
|
+
return Response.json({ data: [[null]] });
|
|
265
|
+
}
|
|
266
|
+
assert.equal(init.method, "POST");
|
|
267
|
+
return Response.json({ code: "000604", statementHandle: handle });
|
|
268
|
+
};
|
|
269
|
+
const pending = await runtime.invoke({ ...args, operation: "statements.submit", input: { statement: "select ?", requestId, bindings: { "1": { type: "FIXED", value: "9007199254740993" } } } });
|
|
270
|
+
assert.deepEqual(pending, { status: "pending", handle });
|
|
271
|
+
const result = await runtime.invoke({ ...args, operation: "statements.get", input: { handle } });
|
|
272
|
+
assert.equal(result.data[0][0], "9007199254740993"); assert.equal(result.metadata.rowType[0].type, "fixed");
|
|
273
|
+
assert.deepEqual((await runtime.invoke({ ...args, operation: "statements.get", input: { handle, partition: 1 } })).data, [[null]]);
|
|
274
|
+
assert.equal((await runtime.invoke({ ...args, operation: "statements.cancel", input: { handle } })).status, "cancelled");
|
|
275
|
+
const before = requests.length;
|
|
276
|
+
await assert.rejects(runtime.invoke({ ...args, operation: "statements.get", input: { handle: "../../other" } }));
|
|
277
|
+
await assert.rejects(runtime.invoke({ ...args, operation: "statements.submit", input: { statement: "select ?", requestId, bindings: { "1": { type: "FIXED", value: 9007199254740992 } } } }));
|
|
278
|
+
assert.equal(requests.length, before);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("Snowflake warehouse actions quote exact names and share the asynchronous lifecycle", async (t) => {
|
|
282
|
+
const { service, state, requests, connect } = await fixture(t);
|
|
283
|
+
await connect();
|
|
284
|
+
const requestId = "536fad38-b564-4dc5-9892-a4543504df6c";
|
|
285
|
+
const handle = "019c07a7-0000-df4f-0000-001000067872";
|
|
286
|
+
const name = 'App"; DROP DATABASE X; --';
|
|
287
|
+
const quoted = '"App""; DROP DATABASE X; --"';
|
|
288
|
+
for (const [action, suffix] of [
|
|
289
|
+
["create", `CREATE WAREHOUSE ${quoted} WAREHOUSE_SIZE = XSMALL INITIALLY_SUSPENDED = TRUE AUTO_SUSPEND = 60 AUTO_RESUME = FALSE`],
|
|
290
|
+
["resize", `ALTER WAREHOUSE ${quoted} SET WAREHOUSE_SIZE = XSMALL`],
|
|
291
|
+
["resume", `ALTER WAREHOUSE ${quoted} RESUME IF SUSPENDED`],
|
|
292
|
+
["suspend", `ALTER WAREHOUSE ${quoted} SUSPEND`],
|
|
293
|
+
["delete", `DROP WAREHOUSE ${quoted}`]
|
|
294
|
+
]) {
|
|
295
|
+
state.sqlReply = (url, init) => {
|
|
296
|
+
assert.equal(JSON.parse(init.body).statement, suffix);
|
|
297
|
+
assert.equal(JSON.parse(init.body).parameters.multi_statement_count, "1");
|
|
298
|
+
assert.equal(url.searchParams.get("requestId"), requestId);
|
|
299
|
+
return Response.json({ code: "333334", statementHandle: handle }, { status: 202 });
|
|
300
|
+
};
|
|
301
|
+
assert.deepEqual(await service.invoke({ ...args, operation: `warehouses.${action}`, input: {
|
|
302
|
+
name, requestId, ...(["create", "resize"].includes(action) ? { size: "XSMALL" } : {})
|
|
303
|
+
} }), { status: "pending", handle });
|
|
304
|
+
}
|
|
305
|
+
const before = requests.length;
|
|
306
|
+
await assert.rejects(service.invoke({ ...args, operation: "warehouses.resize", input: { name, requestId, size: "SMALL; SELECT 1" } }));
|
|
307
|
+
assert.equal(requests.length, before);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("Snowflake stops oversized SQL response streams before parsing", async (t) => {
|
|
311
|
+
const { service, state, connect } = await fixture(t);
|
|
312
|
+
await connect();
|
|
313
|
+
let cancelled = false;
|
|
314
|
+
let chunks = 0;
|
|
315
|
+
state.sqlReply = () => new Response(new ReadableStream({
|
|
316
|
+
pull(controller) { chunks++; controller.enqueue(new Uint8Array(1024 * 1024)); },
|
|
317
|
+
cancel() { cancelled = true; }
|
|
318
|
+
}));
|
|
319
|
+
await assert.rejects(service.invoke({ ...args, operation: "statements.get", input: {
|
|
320
|
+
handle: "019c07a7-0000-df4f-0000-001000067872"
|
|
321
|
+
} }), { code: "connector_response_too_large" });
|
|
322
|
+
assert.equal(cancelled, true);
|
|
323
|
+
assert.ok(chunks <= 18);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("Snowflake application policy gates SQL, handles and warehouse resources before transport", async (t) => {
|
|
327
|
+
const f = await fixture(t);
|
|
328
|
+
await f.connect();
|
|
329
|
+
const handle = "019c07a7-0000-df4f-0000-001000067872";
|
|
330
|
+
const deniedHandle = "536fad38-b564-4dc5-9892-a4543504df6c";
|
|
331
|
+
const seen = [];
|
|
332
|
+
const service = createConnectionService({ ...f.options, authorize: async (owner, request) => {
|
|
333
|
+
seen.push(request);
|
|
334
|
+
if ((request.operation === "statements.submit" && request.input.statement !== "select 1") ||
|
|
335
|
+
(request.operation === "statements.get" && request.input.handle !== handle) ||
|
|
336
|
+
(request.operation.startsWith("warehouses.") && request.input.name !== "APP_WH")) return null;
|
|
337
|
+
return owner;
|
|
338
|
+
} });
|
|
339
|
+
const before = f.requests.length;
|
|
340
|
+
for (const [operation, input] of [
|
|
341
|
+
["statements.submit", { statement: "select * from other_tenant", requestId: deniedHandle }],
|
|
342
|
+
["statements.get", { handle: deniedHandle }],
|
|
343
|
+
["warehouses.delete", { name: "OTHER_WH", requestId: deniedHandle }]
|
|
344
|
+
]) await assert.rejects(service.invoke({ ...args, operation, input }), { code: "connector_access_denied" });
|
|
345
|
+
assert.equal(f.requests.length, before);
|
|
346
|
+
assert.equal(seen[2].input.name, "OTHER_WH");
|
|
347
|
+
await assert.rejects(service.invoke({ ...args, context: { ...context, subjectId: "another-user" },
|
|
348
|
+
operation: "statements.get", input: { handle } }), { code: "connector_reconnect_required" });
|
|
349
|
+
assert.equal(f.requests.length, before);
|
|
350
|
+
f.state.sqlReply = () => Response.json({ code: "333334", statementHandle: handle }, { status: 202 });
|
|
351
|
+
assert.deepEqual(await service.invoke({ ...args, operation: "statements.submit", input: {
|
|
352
|
+
statement: "select 1", requestId: deniedHandle
|
|
353
|
+
} }), { status: "pending", handle });
|
|
354
|
+
assert.equal(f.requests.length, before + 1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("Snowflake rejects failed or mismatched SQL results without replaying execution", async (t) => {
|
|
358
|
+
const { service, state, requests, connect } = await fixture(t);
|
|
359
|
+
await connect();
|
|
360
|
+
const handle = "019c07a7-0000-df4f-0000-001000067872";
|
|
361
|
+
const invoke = () => service.invoke({ ...args, operation: "statements.get", input: { handle } });
|
|
362
|
+
for (const [status, code] of [[422, "connector_operation_rejected"], [429, "connector_rate_limited"], [503, "connector_provider_failed"]]) {
|
|
363
|
+
state.sqlReply = () => Response.json({ message: "private SQL and table names", code: "failure" }, { status });
|
|
364
|
+
const before = requests.length;
|
|
365
|
+
await assert.rejects(invoke(), (error) => error.code === code && !error.message.includes("private SQL"));
|
|
366
|
+
assert.equal(requests.length, before + 1);
|
|
367
|
+
}
|
|
368
|
+
const valid = { code: "090001", statementHandle: handle, data: [["1"]], resultSetMetaData: { rowType: [{ name: "VALUE", type: "fixed" }] } };
|
|
369
|
+
for (const value of [
|
|
370
|
+
{ ...valid, statementHandle: "536fad38-b564-4dc5-9892-a4543504df6c" },
|
|
371
|
+
{ ...valid, data: [[1]] },
|
|
372
|
+
{ ...valid, data: [["1", "2"]] },
|
|
373
|
+
{ ...valid, resultSetMetaData: undefined },
|
|
374
|
+
{ ...valid, code: "failure" }
|
|
375
|
+
]) {
|
|
376
|
+
state.sqlReply = () => Response.json(value);
|
|
377
|
+
await assert.rejects(invoke(), { code: "connector_response_invalid" });
|
|
378
|
+
}
|
|
379
|
+
state.sqlReply = () => new Response("not JSON", { status: 200 });
|
|
380
|
+
await assert.rejects(invoke(), { code: "connector_response_invalid" });
|
|
381
|
+
state.sqlReply = () => Response.json({ code: "333334", statementHandle: handle }, { status: 202 });
|
|
382
|
+
assert.deepEqual(await invoke(), { status: "pending", handle });
|
|
383
|
+
state.sqlReply = () => { throw new Error("connection lost after possible execution"); };
|
|
384
|
+
const before = requests.length;
|
|
385
|
+
await assert.rejects(service.invoke({ ...args, operation: "statements.submit", input: {
|
|
386
|
+
statement: "insert into audit_log values (?)", bindings: { "1": { type: "TEXT", value: "booking" } },
|
|
387
|
+
requestId: "536fad38-b564-4dc5-9892-a4543504df6c"
|
|
388
|
+
} }), { code: "connector_provider_failed" });
|
|
389
|
+
assert.equal(requests.length, before + 1);
|
|
390
|
+
assert.equal((await service.status(args)).status, "connected");
|
|
391
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createConnectionService } from "../../connectors-core/src/server/index.js";
|
|
7
|
+
import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
|
|
8
|
+
import { tallyProvider } from "../src/server/tally.js";
|
|
9
|
+
|
|
10
|
+
test("Tally submission retrieval preserves answers and explicit paging with account credentials", async (t) => {
|
|
11
|
+
const directory = await mkdtemp(path.join(tmpdir(), "tally-workflows-"));
|
|
12
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
13
|
+
const requests = [];
|
|
14
|
+
let response = { items: [], total: 0, hasMore: false }, status = 200;
|
|
15
|
+
const service = createConnectionService({
|
|
16
|
+
configuration: { schemaVersion: 1, registrations: {}, integrations: { forms: {
|
|
17
|
+
provider: "tally", accountMode: "shared", displayName: "Forms", scopes: [],
|
|
18
|
+
authentication: { method: "api-key", secretRef: "env:TALLY_API_KEY" }
|
|
19
|
+
} } }, providers: [tallyProvider], authorize: async (owner) => owner,
|
|
20
|
+
resolveReference: async () => "fixture-tally-key",
|
|
21
|
+
store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" }) }),
|
|
22
|
+
fetchImpl: async (url, options) => { requests.push({ url: new URL(url), options }); return Response.json(response, { status }); }
|
|
23
|
+
});
|
|
24
|
+
const identity = { context: { applicationId: "app", subjectId: "owner" }, integrationId: "forms" };
|
|
25
|
+
await service.connectApiKey(identity);
|
|
26
|
+
const formCall = (operation, input) => service.invoke({ ...identity, operation, input });
|
|
27
|
+
const form = { id: "AbC123", name: "Bookings", status: "DRAFT" };
|
|
28
|
+
response = form;
|
|
29
|
+
const blocks = [{ uuid: "f949b8e1-07a8-4ca5-899f-43a725751b80", type: "FORM_TITLE", groupUuid: "51f20dba-f8b3-49f3-80f8-d17e4a4a429f", groupType: "FORM_TITLE", payload: { title: "Bookings" } }];
|
|
30
|
+
assert.deepEqual(await formCall("forms.create", { blocks, workspaceId: "workspace-1" }), form);
|
|
31
|
+
assert.equal(requests.at(-1).options.method, "POST");
|
|
32
|
+
assert.deepEqual(JSON.parse(requests.at(-1).options.body), { workspaceId: "workspace-1", status: "DRAFT", blocks });
|
|
33
|
+
assert.deepEqual(await formCall("forms.update", { formId: form.id, status: "PUBLISHED" }), form);
|
|
34
|
+
assert.equal(requests.at(-1).options.method, "PATCH");
|
|
35
|
+
assert.deepEqual(JSON.parse(requests.at(-1).options.body), { status: "PUBLISHED" });
|
|
36
|
+
await formCall("forms.update", { formId: form.id, name: "Renamed" });
|
|
37
|
+
assert.deepEqual(JSON.parse(requests.at(-1).options.body), { name: "Renamed" });
|
|
38
|
+
response = { ...form, blocks };
|
|
39
|
+
assert.deepEqual(await formCall("forms.get", { formId: form.id }), response);
|
|
40
|
+
const beforeInvalid = requests.length;
|
|
41
|
+
for (const [operation, input] of [["forms.create", {}], ["forms.create", { blocks: [null] }],
|
|
42
|
+
["forms.create", { blocks, status: "TRASH" }], ["forms.update", { formId: form.id }],
|
|
43
|
+
["forms.get", { formId: "../anything" }]]) {
|
|
44
|
+
await assert.rejects(formCall(operation, input), { code: "connector_input_invalid" });
|
|
45
|
+
}
|
|
46
|
+
assert.equal(requests.length, beforeInvalid);
|
|
47
|
+
status = 500;
|
|
48
|
+
await assert.rejects(formCall("forms.create", { blocks }), { code: "connector_provider_failed" });
|
|
49
|
+
assert.equal(requests.length, beforeInvalid + 1);
|
|
50
|
+
status = 200;
|
|
51
|
+
const invoke = (input) => service.invoke({ ...identity, operation: "submissions.list", input });
|
|
52
|
+
response = { page: 2, limit: 20, hasMore: true, questions: [{ id: "q1", title: "Dogs?" }],
|
|
53
|
+
submissions: [{ id: "sub1", formId: "AbC123", isCompleted: true, responses: [{ questionId: "q1", answer: 0 }, { questionId: "q2", answer: false }] }] };
|
|
54
|
+
assert.deepEqual(await invoke({ formId: "AbC123", page: 2, limit: 20, filter: "all", afterId: "previous-id" }), response);
|
|
55
|
+
assert.equal(requests.at(-1).url.href, "https://api.tally.so/forms/AbC123/submissions?page=2&limit=20&filter=all&afterId=previous-id");
|
|
56
|
+
assert.equal(new Headers(requests.at(-1).options.headers).get("authorization"), "Bearer fixture-tally-key");
|
|
57
|
+
assert.equal(new Headers(requests.at(-1).options.headers).get("tally-version"), "2025-02-01");
|
|
58
|
+
const count = requests.length;
|
|
59
|
+
for (const input of [{ formId: "../forms" }, { formId: "https://other.test" }, { formId: "AbC123", page: 0 },
|
|
60
|
+
{ formId: "AbC123", limit: 501 }, { formId: "AbC123", filter: "anything" }, { formId: "AbC123", headers: {} }]) {
|
|
61
|
+
await assert.rejects(invoke(input), { code: "connector_input_invalid" });
|
|
62
|
+
}
|
|
63
|
+
assert.equal(requests.length, count);
|
|
64
|
+
response = { page: 1, limit: 50, hasMore: false, questions: [], submissions: [] };
|
|
65
|
+
assert.deepEqual(await invoke({ formId: "AbC123" }), response);
|
|
66
|
+
assert.equal(requests.at(-1).url.searchParams.get("filter"), "completed");
|
|
67
|
+
response = { ...response, submissions: {} };
|
|
68
|
+
await assert.rejects(invoke({ formId: "AbC123" }), { code: "connector_response_invalid" });
|
|
69
|
+
for (const [failure, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
|
|
70
|
+
status = failure;
|
|
71
|
+
const before = requests.length;
|
|
72
|
+
await assert.rejects(invoke({ formId: "AbC123" }), { code });
|
|
73
|
+
assert.equal(requests.length, before + 1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createConnectionService } from "../../connectors-core/src/server/index.js";
|
|
7
|
+
import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
|
|
8
|
+
import { telegramProvider } from "../src/server/telegram.js";
|
|
9
|
+
|
|
10
|
+
test("Telegram sends plain messages and polls without hidden acknowledgement or retry", async (t) => {
|
|
11
|
+
const directory = await mkdtemp(path.join(tmpdir(), "telegram-workflows-"));
|
|
12
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
13
|
+
const requests = [];
|
|
14
|
+
let response = { ok: true, result: { id: 123, is_bot: true, first_name: "Fixture" } }, status = 200;
|
|
15
|
+
const service = createConnectionService({
|
|
16
|
+
configuration: { schemaVersion: 1, registrations: {}, integrations: { forms: {
|
|
17
|
+
provider: "telegram", accountMode: "shared", displayName: "Forms", scopes: [],
|
|
18
|
+
authentication: { method: "api-key", secretRef: "env:TELEGRAM_BOT_TOKEN" }
|
|
19
|
+
} } }, providers: [telegramProvider], authorize: async (owner) => owner,
|
|
20
|
+
resolveReference: async () => "123456:fixture_token",
|
|
21
|
+
store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" }) }),
|
|
22
|
+
fetchImpl: async (url, options) => { requests.push({ url: new URL(url), options }); return Response.json(response, { status }); }
|
|
23
|
+
});
|
|
24
|
+
const identity = { context: { applicationId: "app", subjectId: "owner" }, integrationId: "forms" };
|
|
25
|
+
await service.connectApiKey(identity);
|
|
26
|
+
const invoke = (operation, input) => service.invoke({ ...identity, operation, input });
|
|
27
|
+
const message = { message_id: 42, chat: { id: -100123 }, text: "Hello <world>" };
|
|
28
|
+
response = { ok: true, result: message };
|
|
29
|
+
assert.deepEqual(await invoke("messages.send", { chat_id: "-100123", text: message.text }), response);
|
|
30
|
+
assert.equal(requests.at(-1).url.href, "https://api.telegram.org/bot123456:fixture_token/sendMessage");
|
|
31
|
+
assert.deepEqual(JSON.parse(requests.at(-1).options.body), { chat_id: "-100123", text: message.text, disable_notification: false, protect_content: false });
|
|
32
|
+
assert.equal(requests.at(-1).options.method, "POST");
|
|
33
|
+
response = { ok: true, result: true };
|
|
34
|
+
assert.deepEqual(await invoke("chats.action", { chat_id: "@example_bot", action: "typing" }), response);
|
|
35
|
+
response = { ok: true, result: [{ update_id: 90, message: { message_id: 3, chat: { id: 456 }, text: "/start" } }] };
|
|
36
|
+
assert.deepEqual(await invoke("updates.poll", {}), response);
|
|
37
|
+
assert.deepEqual(JSON.parse(requests.at(-1).options.body), { limit: 100, timeout: 20, allowed_updates: ["message"] });
|
|
38
|
+
response = { ok: true, result: [] };
|
|
39
|
+
await invoke("updates.poll", { offset: 91, timeout: 0, limit: 1 });
|
|
40
|
+
assert.equal(JSON.parse(requests.at(-1).options.body).offset, 91);
|
|
41
|
+
const before = requests.length;
|
|
42
|
+
for (const [operation, input] of [["messages.send", { chat_id: "../leak", text: "Hello" }],
|
|
43
|
+
["messages.send", { chat_id: "123", text: "" }], ["messages.send", { chat_id: "123", text: "a".repeat(4097) }],
|
|
44
|
+
["messages.send", { chat_id: "123", text: "Hello", allow_paid_broadcast: true }],
|
|
45
|
+
["chats.action", { chat_id: "123", action: "arbitrary" }],
|
|
46
|
+
["updates.poll", { offset: -1 }], ["updates.poll", { timeout: 21 }], ["updates.poll", { allowed_updates: [] }]]) {
|
|
47
|
+
await assert.rejects(invoke(operation, input), { code: "connector_input_invalid" });
|
|
48
|
+
}
|
|
49
|
+
assert.equal(requests.length, before);
|
|
50
|
+
response = { ok: true, result: [{ update_id: "bad" }] };
|
|
51
|
+
await assert.rejects(invoke("updates.poll", {}), { code: "connector_response_invalid" });
|
|
52
|
+
for (const code of [403, 429, 500]) {
|
|
53
|
+
response = { ok: false, error_code: code, description: "secret-provider-message" };
|
|
54
|
+
const count = requests.length;
|
|
55
|
+
await assert.rejects(invoke("messages.send", { chat_id: "123", text: "Hello" }), (error) => !JSON.stringify(error).includes("secret-provider-message"));
|
|
56
|
+
assert.equal(requests.length, count + 1);
|
|
57
|
+
}
|
|
58
|
+
});
|