From 83c9a6ea0b9029165197f9b072b2679a9e1d9c69 Mon Sep 17 00:00:00 2001 From: Nathan Hu Date: Tue, 5 May 2026 15:35:11 -0400 Subject: [PATCH 1/3] fix(providers/kno-commerce): pass grant_type+scope on URL, expose scope as connection config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous config passed grant_type (and any scope we'd add) via token_params, which Nango places in the request body. Kno's OAuth token endpoint silently ignores the body and only reads query-string params, returning HTTP 400 invalid_request unless both grant_type and scope are present in the URL itself. Tested live against the Kno production token endpoint with the exact shape Nango produces (Basic auth header + form-urlencoded body): | URL has gt+scope, body empty → 200 OK | URL empty, body has gt+scope → 400 invalid_request | URL has gt only, body has scope → 400 invalid_request | URL has scope only (no gt) → 400 invalid_request Both grant_type and scope are mandatory and must live in the URL. Fix: * Embed grant_type=client_credentials directly in token_url (always this value — Kno only supports the client-credentials grant). * Interpolate ${connectionConfig.scope} into the URL so customers can set the scope at connect time, matching the templating pattern used by commercetools, databricks-workspace, and others. Defaulting/example set to "SURVEYS RESPONSES QUESTIONS WEBHOOKS" — the four read scopes Kno publishes — with a pattern guard against typos and injection. * Drop token_params entirely; Kno ignores the body. --- packages/providers/providers.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/providers/providers.yaml b/packages/providers/providers.yaml index 4fb197ff05..23e8a287d4 100644 --- a/packages/providers/providers.yaml +++ b/packages/providers/providers.yaml @@ -10069,14 +10069,20 @@ kno-commerce: categories: - e-commerce auth_mode: OAUTH2_CC - token_url: https://app-api.knocommerce.com/api/oauth2/token + token_url: https://app-api.knocommerce.com/api/oauth2/token?grant_type=client_credentials&scope=${connectionConfig.scope} token_request_auth_method: basic - token_params: - grant_type: client_credentials proxy: base_url: https://app-api.knocommerce.com docs: https://nango.dev/docs/api-integrations/kno-commerce docs_connect: https://nango.dev/docs/api-integrations/kno-commerce/connect + connection_config: + scope: + type: string + title: API Scopes + description: Space-separated list of Kno API scopes to grant the access token. Valid values are SURVEYS, RESPONSES, QUESTIONS, and WEBHOOKS. + example: SURVEYS RESPONSES QUESTIONS WEBHOOKS + pattern: '^[A-Z]+( [A-Z]+)*$' + doc_section: '#step-1-creating-an-api-client' credentials: client_id: type: string From 3195b0783eeedff2e04304477249914e98059f38 Mon Sep 17 00:00:00 2001 From: Nathan Hu Date: Wed, 6 May 2026 10:32:04 -0400 Subject: [PATCH 2/3] feat(oauth2-cc): use scope_separator when templating oauth_scopes into URL Some OAuth2 client-credentials providers (e.g. Kno Commerce) require the `scope` parameter to be on the URL query string rather than in the form body. Templating ${connectionConfig.oauth_scopes} into token_url previously returned the comma-separated storage form, which those APIs reject (400 invalid_scope). This change pre-computes oauth_scopes joined with the provider's scope_separator (defaulting to ' ' per RFC 6749) and exposes that joined form to the URL templating context. Body emission reuses the same pre-computed value rather than recomputing it inline. No behavior change for providers that don't template oauth_scopes into token_url (none do today). For kno-commerce, this lets developers pick scopes from the dashboard catalog (added in providers.scopes.yaml) and have those scopes flow into the URL with the correct delimiter. --- packages/providers/providers.scopes.yaml | 7 +++++++ packages/providers/providers.yaml | 11 ++--------- packages/shared/lib/services/connection.service.ts | 9 ++++++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/providers/providers.scopes.yaml b/packages/providers/providers.scopes.yaml index 4b733c9d53..80d5b61fd4 100644 --- a/packages/providers/providers.scopes.yaml +++ b/packages/providers/providers.scopes.yaml @@ -2722,6 +2722,13 @@ klaviyo-oauth: - web-feeds:read - web-feeds:write +# source: https://developers.knocommerce.com/ (read scopes for survey responses, questions, and webhook subscriptions) +kno-commerce: + - SURVEYS + - RESPONSES + - QUESTIONS + - WEBHOOKS + # source: https://hire.lever.co/developer/documentation lever: - applications:read:admin diff --git a/packages/providers/providers.yaml b/packages/providers/providers.yaml index 23e8a287d4..08d7a9d11d 100644 --- a/packages/providers/providers.yaml +++ b/packages/providers/providers.yaml @@ -10069,20 +10069,13 @@ kno-commerce: categories: - e-commerce auth_mode: OAUTH2_CC - token_url: https://app-api.knocommerce.com/api/oauth2/token?grant_type=client_credentials&scope=${connectionConfig.scope} + token_url: https://app-api.knocommerce.com/api/oauth2/token?grant_type=client_credentials&scope=${connectionConfig.oauth_scopes} token_request_auth_method: basic + scope_separator: ' ' proxy: base_url: https://app-api.knocommerce.com docs: https://nango.dev/docs/api-integrations/kno-commerce docs_connect: https://nango.dev/docs/api-integrations/kno-commerce/connect - connection_config: - scope: - type: string - title: API Scopes - description: Space-separated list of Kno API scopes to grant the access token. Valid values are SURVEYS, RESPONSES, QUESTIONS, and WEBHOOKS. - example: SURVEYS RESPONSES QUESTIONS WEBHOOKS - pattern: '^[A-Z]+( [A-Z]+)*$' - doc_section: '#step-1-creating-an-api-client' credentials: client_id: type: string diff --git a/packages/shared/lib/services/connection.service.ts b/packages/shared/lib/services/connection.service.ts index 12df759292..de17794345 100644 --- a/packages/shared/lib/services/connection.service.ts +++ b/packages/shared/lib/services/connection.service.ts @@ -1202,8 +1202,12 @@ class ConnectionService { client_certificate?: string | undefined; client_private_key?: string | undefined; }): Promise> { + const scope = + connectionConfig['oauth_scopes'] && typeof connectionConfig['oauth_scopes'] === 'string' + ? connectionConfig['oauth_scopes'].split(',').join(provider.scope_separator || ' ') + : ''; const strippedTokenUrl = typeof provider.token_url === 'string' ? provider.token_url.replace(/connectionConfig\./g, '') : ''; - const url = new URL(interpolateString(strippedTokenUrl, connectionConfig)); + const url = new URL(interpolateString(strippedTokenUrl, { ...connectionConfig, oauth_scopes: scope })); let interpolatedParams: Record = {}; if (provider.token_params) { @@ -1211,8 +1215,7 @@ class ConnectionService { } let tokenParams = interpolatedParams && Object.keys(interpolatedParams).length > 0 ? new URLSearchParams(interpolatedParams).toString() : ''; - if (connectionConfig['oauth_scopes'] && typeof connectionConfig['oauth_scopes'] === 'string') { - const scope = connectionConfig['oauth_scopes'].split(',').join(provider.scope_separator || ' '); + if (scope) { tokenParams += (tokenParams ? '&' : '') + `scope=${encodeURIComponent(scope)}`; } From f7be39d5a11eae19b354130c7b82fb7e021b5199 Mon Sep 17 00:00:00 2001 From: Nathan Hu Date: Wed, 6 May 2026 10:58:14 -0400 Subject: [PATCH 3/3] chore(validation): exempt runtime-populated keys from connection_config check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The provider validator walks every \${connectionConfig.X} interpolation and requires X to be declared in connection_config or token_response_metadata. That's the right rule for fields a customer or developer types in. But some keys are populated at runtime by the auth subsystem itself — notably oauth_scopes, which is built from the integration's stored scope chips and joined with the provider's scope_separator at request time. There's no place in the YAML to declare such keys, and forcing them to appear in connection_config would mislead readers (the field is not user-supplied). Add a small RUNTIME_DYNAMIC_KEYS set listing names the validator should permit without a declaration. Today: just oauth_scopes. Future runtime- populated identifiers can be added to the same set without scattering one-off if-branches through the validator. --- scripts/validation/providers/validate.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/validation/providers/validate.ts b/scripts/validation/providers/validate.ts index acd9dbb513..1cec84c349 100644 --- a/scripts/validation/providers/validate.ts +++ b/scripts/validation/providers/validate.ts @@ -150,6 +150,8 @@ function validateProvider(providerKey: string, provider: ExtendedProvider) { } } + const RUNTIME_DYNAMIC_KEYS = new Set(['oauth_scopes']); + // Find all connectionConfig references const connectionConfigReferences = findConnectionConfigReferences(provider); @@ -158,8 +160,9 @@ function validateProvider(providerKey: string, provider: ExtendedProvider) { for (const reference of connectionConfigReferences) { const defined = provider.connection_config && reference.key in provider.connection_config; const inTokenResponseMetadata = provider.token_response_metadata?.includes(reference.key); + const isRuntimeDynamic = RUNTIME_DYNAMIC_KEYS.has(reference.key); - if (!defined && !inTokenResponseMetadata) { + if (!defined && !inTokenResponseMetadata && !isRuntimeDynamic) { console.error( chalk.red('error'), chalk.blue(providerKey),