oauth: add netsuite provider + icon

NetSuite is a per-instance OAuth provider (account-specific authorize/token
URLs), registered via connect_config_template. Its authorize endpoint
requires scope=rest_webservices, so the template mechanism gains an
optional scopes field copied into the built connect_config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-06-11 18:24:05 +02:00
parent d3d61d4acd
commit 1387ce96b4
5 changed files with 43 additions and 1 deletions
+17
View File
@@ -210,5 +210,22 @@
"instance_url": "https://{instance}.service-now.com"
}
}
},
"netsuite": {
"connect_config_template": {
"display_name": "NetSuite",
"label": "NetSuite Account ID",
"placeholder": "<account id> as in your NetSuite domain, e.g. 1234567 or 1234567-sb1",
"help_url": "https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_157769826287.html",
"auth_url": "https://{instance}.app.netsuite.com/app/login/oauth2/authorize.nl",
"token_url": "https://{instance}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token",
"req_body_auth": false,
"strip_suffix": ".app.netsuite.com",
"scopes": ["rest_webservices"],
"extra_params_key": "account_id",
"resource_mapping": {
"account_id": "{instance}"
}
}
}
}
+4
View File
@@ -130,6 +130,10 @@ pub struct ConnectConfigTemplate {
pub token_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_body_auth: Option<bool>,
/// Scopes copied into the built `connect_config` (e.g. NetSuite's
/// `rest_webservices`). Templated providers default to no scopes.
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes: Option<Vec<String>>,
/// Key under `connect_config.extra_params` where the instance name is
/// stored (defaults to `instance`). Snowflake uses `account_identifier` for
/// backward compatibility with previously-saved configs.
@@ -308,7 +308,7 @@
instanceInputs[name] = v
if (oauths[name].connect_config?.extra_params?.[key] === v) continue
oauths[name].connect_config = {
scopes: [],
scopes: tmpl.scopes ?? [],
auth_url: tmpl.auth_url.replaceAll('{instance}', v),
token_url: tmpl.token_url.replaceAll('{instance}', v),
req_body_auth: tmpl.req_body_auth ?? false,
@@ -0,0 +1,19 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg xmlns="http://www.w3.org/2000/svg" role="img" {width} {height} viewBox="0 0.2 24 24.03">
<path
d="m 3.2198637,7.809131 h 3.7965561 v 9.018821 h 1.8902684 v 3.52423 H 3.2198637 Z M 20.792951,16.107089 H 16.996396 V 7.088266 H 15.106127 V 3.5640368 h 5.686824 z"
fill="#baccdb"
/>
<path
d="M 2.3388066,2.6989989 H 14.337205 V 12.999359 L 9.7236683,7.0562275 H 2.3388066 Z M 21.65799,21.233239 H 9.6595914 V 10.93288 l 4.6135366,5.943131 h 7.384862"
fill="#125580"
/>
</svg>
@@ -34,6 +34,7 @@ import TelegramIcon from './TelegramIcon.svelte'
import FunkwhaleIcon from './FunkwhaleIcon.svelte'
import GdocsIcon from './GdocsIcon.svelte'
import NextcloudIcon from './NextcloudIcon.svelte'
import NetsuiteIcon from './NetsuiteIcon.svelte'
import FaunadbIcon from './FaunadbIcon.svelte'
import ClickhouseIcon from './ClickhouseIcon.svelte'
import OpenaiIcon from './OpenaiIcon.svelte'
@@ -248,6 +249,7 @@ export const APP_TO_ICON_COMPONENT = {
hubspot: HubspotIcon,
datadog: DatadogIcon,
databricks: DatabricksIcon,
netsuite: NetsuiteIcon,
adobe_acrobat_sign: AdobeAcrobatSignIcon,
stripe: StripeIcon,
telegram: TelegramIcon,