mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(security): harden SSO domain registration, webhook path isolation, and CSV export (#4813)
* fix(security): harden KB file access, SSO domain registration, webhook path isolation, env secrets, and CSV export * fix(sso): scope domain conflict query with indexed lower(domain) filter Address PR review: avoid a full-table scan on every SSO provider registration by filtering candidate rows in SQL with lower(domain) = <normalized>, keeping the in-memory ownership check. Also tighten the normalizeSSODomain TSDoc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: condense env route security comments Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * icons update * chore(security): tighten inline comments in CSV export and KB file authorization Condense verbose comment blocks to concise TSDoc/single-line form; no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): validate internal serve origin in KB file authorization Replace the bypassable isInternalFileUrl substring check in resolveInternalKbKey with an origin allow-list (base URL, internal API base URL, TRUSTED_ORIGINS). A crafted external host whose path is /api/files/serve/<victim-key> no longer resolves to the victim key. Relative same-origin URLs are unaffected. * style(sso): use idiomatic sql lower() comparison for domain conflict query Match the repo's prevailing `sql`lower(col) = value`` idiom for the case-insensitive SSO domain conflict lookup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): align workspace env admin gate with hasWorkspaceAdminAccess Use the same admin check the secrets UI uses (owner, admin permission, or org-admin) so owners and org-admins are not wrongly denied their own decrypted workspace secrets, while read-only members remain restricted to names only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(sso): rely on lower(domain) match for conflict detection, drop dead in-memory recheck Address PR review: the SQL `lower(domain) = <normalized>` predicate already excludes rows that the in-memory `normalizeSSODomain(...) === domain` recheck claimed to catch, making that recheck dead/misleading code. Match on the canonical lower-cased domain and filter purely by ownership. Malformed legacy values (wildcards, schemes, ports) never match an email domain at sign-in, so excluding them is not a gap. Test DB mock now applies the lower() predicate so the casing-variant case is genuinely exercised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): scope webhook deploy path conflict to active webhooks findConflictingWebhookPathOwner omitted the isActive filter that the runtime dispatcher (findAllWebhooksForPath) applies, so an inactive but non-archived webhook from another workflow (e.g. after undeploy or failure auto-disable) would permanently block any new deployment on that path even though it never receives deliveries. Align the guard with the runtime isActive + archivedAt filter; the earliest-owner runtime check remains the authoritative cross-tenant protection. Also trims verbose TSDoc on the webhook path-isolation helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): exclude archived workflows from webhook deploy path conflict findConflictingWebhookPathOwner now joins workflow and filters isNull(workflow.archivedAt), matching the runtime dispatcher (findAllWebhooksForPath). A webhook on an archived workflow can never receive deliveries at runtime, so it must not block legitimate path reuse with a 409. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): anchor KB file ownership to earliest document in any state A KB file's owner is now the earliest document referencing its key regardless of state (active/archived/deleted/excluded); access is granted only when that owning document is still active. Closes the residual where an attacker could plant an active document to claim a file whose original document was archived or deleted. * updated greptile icon * revert(security): drop KB file authorization changes Reverts the knowledge-base file-access work (origin-pinning / owner-pinning / origin allow-list in verifyKBFileAccess) and its test. The other hardening fixes (SSO domain registration, webhook path isolation, workspace env secrets, CSV export) are unchanged. apps/sim/app/api/files/authorization.ts is restored to its origin/staging baseline. * fix(sso): treat caller's own user-scoped provider as owned during conflict check Self-hosters often register SSO user-scoped via the CLI script (no SSO_ORGANIZATION_ID). If they later enable organizations and reconfigure the same domain org-scoped through the UI, the conflict check previously treated their own user-scoped row as another tenant's and returned a misleading 409. Recognize the caller's own user-scoped provider as owned so that migration is allowed, while still blocking another user's or another org's domain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * revert(security): remove workspace-env admin gate Defer to a credential-based access model (separate change). Restores GET /api/workspaces/[id]/environment to main behavior and removes the test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(security): consolidate webhook path-collision check into one helper Extract findConflictingWebhookPathOwner to lib/webhooks/utils.server.ts as the single source of truth for cross-tenant path-collision detection, used by both webhook creation paths (deploy sync and the manual /api/webhooks route). This also repairs two latent issues in the manual route's previous inline check, which queried with limit(1) and only webhook.archivedAt: - limit(1) inspected one arbitrary row, so a same-workflow row could mask a foreign collision (false negative). The shared helper scans all matching rows. - It omitted isActive/workflow.archivedAt, so inactive or archived-workflow webhooks (which never receive deliveries) permanently blocked path reuse. The helper mirrors the runtime dispatcher's filter. Same-workflow webhook reuse for upsert is now a separate, explicit lookup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1d4a2772ab
commit
a8f86c0c83
+215
-50
@@ -780,7 +780,7 @@ export function GitLabIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function SerperIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox='0 0 654 600' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<svg viewBox='35.411 -19.109 628.098 628.098' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<path
|
||||
d='M324 38C356 37 389 36 417 47C452 56 484 72 509 94C539 118 561 145 577 176C593 205 601 238 606 271C610 343 590 403 552 452C528 482 499 507 467 523C438 539 404 547 372 552C297 556 235 534 184 492C133 449 103 392 93 330C93 292 89 255 102 224C112 189 128 158 149 132C194 78 255 46 322 38'
|
||||
fill='rgb(71,97,118)'
|
||||
@@ -1298,7 +1298,7 @@ export function GoogleSheetsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export const S3Icon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='-3.097 -3 70 70'
|
||||
viewBox='0 0 80 80'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
@@ -2175,27 +2175,8 @@ export function MistralIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function BrainIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M12 5a3 3 0 1 0-6.125 4 4 0 0 0-2.53 5.77 4 4 0 0 0 .556 6.59A4 4 0 1 0 12 18Z' />
|
||||
<path d='M12 5a3 3 0 1 1 6.125 4 4 0 0 1 2.53 5.77 4 4 0 0 1-.556 6.59A4 4 0 1 1 12 18Z' />
|
||||
<path d='M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4' />
|
||||
<path d='M17.6 6.5a3 3 0 0 0 .399-1.37' />
|
||||
<path d='M6 5.13A3 3 0 0 0 6.4 6.5' />
|
||||
<path d='M3.48 10.9a4 4 0 0 1 .585-.396' />
|
||||
<path d='M19.94 10.5a4 4 0 0 1 .585.4' />
|
||||
<path d='M6 18a4 4 0 0 1-1.97-.516' />
|
||||
<path d='M19.97 17.48A4 4 0 0 1 18 18' />
|
||||
<svg {...props} viewBox='0 0 256 256' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M212,76V72a44,44,0,0,0-74.86-31.31,3.93,3.93,0,0,0-1.14,2.8v88.72a4,4,0,0,0,6.2,3.33A47.67,47.67,0,0,1,167.68,128a8.18,8.18,0,0,1,8.31,7.58,8,8,0,0,1-8,8.42,32,32,0,0,0-32,32v33.88a4,4,0,0,0,1.49,3.12,47.92,47.92,0,0,0,74.21-17.16,4,4,0,0,0-4.49-5.56A68.06,68.06,0,0,1,192,192h-7.73a8.18,8.18,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h8a51.6,51.6,0,0,0,24-5.88v0A52,52,0,0,0,212,76Zm-12,36h-4a36,36,0,0,1-36-36V72a8,8,0,0,1,16,0v4a20,20,0,0,0,20,20h4a8,8,0,0,1,0,16ZM88,28A44.05,44.05,0,0,0,44,72v4a52,52,0,0,0-4,94.12h0A51.6,51.6,0,0,0,64,176h7.73A8.18,8.18,0,0,1,80,183.47,8,8,0,0,1,72,192H64a67.48,67.48,0,0,1-15.21-1.73,4,4,0,0,0-4.5,5.55A47.93,47.93,0,0,0,118.51,213a4,4,0,0,0,1.49-3.12V176a32,32,0,0,0-32-32,8,8,0,0,1-8-8.42A8.18,8.18,0,0,1,88.32,128a47.67,47.67,0,0,1,25.48,7.54,4,4,0,0,0,6.2-3.33V43.49a4,4,0,0,0-1.14-2.81A43.85,43.85,0,0,0,88,28Zm8,48a36,36,0,0,1-36,36H56a8,8,0,0,1,0-16h4A20,20,0,0,0,80,76V72a8,8,0,0,1,16,0Z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -2342,8 +2323,8 @@ export function ExtendIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
|
||||
<path d='M29.34 16.82c.1 1.7-.08 3.37-.305 5.05-.225 1.71-.508 3.42-.964 5.08-.3 1.07-.673 2.1-1.2 3.07-.65 1.19-1.63 1.87-2.99 1.92l-3.83.036c-.636-.017-1.28-.146-1.9-.297-1.19-.3-1.86-1.1-2.06-2.3-.186-1.08-.173-2.19.04-3.26.252-1.23 1-1.96 2.23-2.1.817-.1 1.65-.077 2.48-.1.21-.7.28.98.2.287-.196.53-.236 1.07-.098 1.62.53.21-.23.31-.26.31a7.77 7.77 0 0 0-1.12.053c-.636.09-.96.47-.96 1.11 0 .205.03.416.07.622.1.507.45.78.94.837 1.12.127 2.25.138 3.37-.5.68-.114 1.08-.54 1.16-1.21.152-1.3.16-2.59-.228-3.84-.33-1.09-1.01-1.56-2.13-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.76-.013-.106-.074-.208-.1-.3-.04.1-.106.21-.117.33-.066.68-.053 1.37-.185 2.04-.16.8-.404 1.57-.67 2.33-.185.54-.616.84-1.2.8a37.76 37.76 0 0 1-7.12-1.35l-.64-.207c-.927-.26-1.49-.903-1.74-1.79l-1-3.85-.74-4.3c-.115-.755-.2-1.52-.083-2.29.154-1.11.914-1.9 2.04-1.96l3.56-.062c.127 0 .254.373-.026a1.23 1.23 0 0 0 1.01-1.25l-.05-3.04c-.048-1.58.8-2.38 2.16-2.62a10.58 10.58 0 0 1 4.91.26c.933.28 1.47.923 1.72 1.83.06.22.15.3.37.29l2.58.01 3.33.37c.686.1 1.36.25 2.03.42 1.17.298 1.79 1.11 1.96 2.26l.357 3.36.3 5.58.01 2.28zm-4.53-1.15c-.02-.666-.07-1.27-.444-1.78a1.66 1.66 0 0 0-2.47-.15c-.364.4-.494.88-.564 1.4-.8.03.106.13.16.13l.8-.053c.768.01 1.52.113 2.25.39.66.03.136.4.26.077zM8.79 1.15a3.82 3.82 0 0 0-.278 1.59l.05 2.93c.5.36-.075.45-.433.45L5.1 6.16c-.583 0-1.14.1-1.55.278l5.2-5.33c.2.01.4.03.6.05z' />
|
||||
<svg {...props} viewBox='0 0 32 32' fill='#7fce2c' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -2538,7 +2519,7 @@ export function TelegramIcon(props: SVGProps<SVGSVGElement>) {
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
viewBox='1 1 22 22'
|
||||
width='24'
|
||||
height='24'
|
||||
fill='none'
|
||||
@@ -3635,26 +3616,200 @@ export function HunterIOIcon(props: SVGProps<SVGSVGElement>) {
|
||||
}
|
||||
|
||||
export function MicrosoftOneDriveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
return (
|
||||
<svg {...props} fill='currentColor' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g>
|
||||
<path
|
||||
d='M12.2,11.19l.00031-.0011,6.72,4.02,4-1.68.18A6.48,6.48,0,0,1,25.5,13c.14764,0,.29358.1.44.02a10,10,0,0,0-18.04-3.01C7.93,10,7.97,10,8,10A7.96,7.96,0,0,1,12.2,11.19Z'
|
||||
fill='#0364b8'
|
||||
/>
|
||||
<path
|
||||
d='M12.2,11.19l-.31A7.96,7.96,0,0,0,8,10c-.0343,0-.6805-.10223A8,8,0,0,0,1.44,22.57l5.92-2.49,2.63-1.11,5.86-2.47,3.06-1.29Z'
|
||||
fill='#0078d4'
|
||||
/>
|
||||
<path
|
||||
d='M25.94,13.02C25.79,13.01,25.65,13,25.5,13a6.48,6.48,0,0,0-2.58.53l-.00018-.00068-4,1.68,1.16.7L23.89,18.19l1.66.99,5.68,3.4a6.5,6.5,0,0,0-5.28-9.57Z'
|
||||
fill='#1490df'
|
||||
/>
|
||||
<path
|
||||
d='M25.55,19.18,23.89,18.19l-3.8-2.28-1.16-.69528L15.86,16.5,9.99,18.97,7.36,20.08l-5.92,2.49A7.99,7.99,0,0,0,8,26H25.5a6.5,6.5,0,0,0,5.72-3.42Z'
|
||||
fill='#28a8ea'
|
||||
/>
|
||||
</g>
|
||||
<svg {...props} viewBox='35.98 139.2 648.03 430.85' xmlns='http://www.w3.org/2000/svg'>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id={`onedrive_radial0_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(130.864814,156.804864,-260.089994,217.063603,48.669602,228.766494)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(28.235294%,58.039216%,99.607843%)' stopOpacity={1} />
|
||||
<stop
|
||||
offset='0.695072'
|
||||
stopColor='rgb(3.529412%,20.392157%,70.196078%)'
|
||||
stopOpacity={1}
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial1_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-575.289668,663.594003,-491.728488,-426.294267,596.956501,-6.380235)'
|
||||
>
|
||||
<stop
|
||||
offset='0.165327'
|
||||
stopColor='rgb(13.72549%,75.294118%,99.607843%)'
|
||||
stopOpacity={1}
|
||||
/>
|
||||
<stop offset='0.534' stopColor='rgb(10.980392%,56.862745%,100%)' stopOpacity={1} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial2_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-136.753383,-114.806698,262.816935,-313.057562,181.196995,240.395994)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(100%,100%,100%)' stopOpacity={0.4} />
|
||||
<stop offset='0.660528' stopColor='rgb(67.843137%,75.294118%,100%)' stopOpacity={0} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial3_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-153.638428,-130.000063,197.433014,-233.332948,375.353994,451.43549)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(1.176471%,22.745098%,80%)' stopOpacity={1} />
|
||||
<stop offset='1' stopColor='rgb(21.176471%,55.686275%,100%)' stopOpacity={0} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial4_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(175.585899,405.198026,-437.434522,189.555055,169.378495,125.589294)'
|
||||
>
|
||||
<stop
|
||||
offset='0.592618'
|
||||
stopColor='rgb(20.392157%,39.215686%,89.019608%)'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
<stop offset='1' stopColor='rgb(1.176471%,22.745098%,80%)' stopOpacity={0.6} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial5_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-459.329491,459.329491,-719.614455,-719.614455,589.876499,39.484649)'
|
||||
>
|
||||
<stop
|
||||
offset='0'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0.898039}
|
||||
/>
|
||||
<stop
|
||||
offset='0.543937'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id={`onedrive_linear0_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='29.999701'
|
||||
y1='37.9823'
|
||||
x2='29.999701'
|
||||
y2='18.398199'
|
||||
gradientTransform='matrix(15,0,0,15,0,0)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(0%,52.54902%,100%)' stopOpacity={1} />
|
||||
<stop offset='0.49' stopColor='rgb(0%,73.333333%,100%)' stopOpacity={1} />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial6_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(273.622108,108.513684,-205.488428,518.148261,296.488495,307.441492)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(100%,100%,100%)' stopOpacity={0.4} />
|
||||
<stop offset='0.785262' stopColor='rgb(100%,100%,100%)' stopOpacity={0} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial7_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-305.683909,263.459223,-264.352324,-306.720147,674.845505,249.378004)'
|
||||
>
|
||||
<stop
|
||||
offset='0'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0.898039}
|
||||
/>
|
||||
<stop
|
||||
offset='0.584724'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill={`url(#onedrive_radial0_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.078125 205.089844 C 116.011719 205.09375 41.957031 286.1875 36.382812 376.527344 C 39.835938 395.992188 51.175781 434.429688 68.941406 432.457031 C 91.144531 429.988281 147.066406 432.457031 194.765625 346.105469 C 229.609375 283.027344 301.285156 205.085938 215.078125 205.089844 Z M 215.078125 205.089844 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial1_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial2_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial3_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial4_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial5_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_linear0_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial6_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial7_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 '
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -5882,12 +6037,22 @@ export function GreenhouseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function GreptileIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg {...props} viewBox='0 0 367 420' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
clipRule='evenodd'
|
||||
fillRule='evenodd'
|
||||
fill='#44A775'
|
||||
d='M3.353.004a6.074 6.074 0 01-.265.045C2.63.12 2.092.348 1.71.633 1.426.846.717 1.575.557 1.819a3.359 3.359 0 00-.23 3.296c.154.322.35.59.71.972.187.198.434.486.55.64a6.629 6.629 0 011.305 3.546c.01.138.035 1.607.057 3.264.043 3.273.038 3.18.203 3.485.266.494.94.79 1.474.648.29-.077.463-.204 1.353-.986.957-.84 1.092-.932 1.446-.98.124-.017.631 0 1.66.053 1.513.08 1.622.079 1.85-.016.393-.164.539-.4.661-1.074.247-1.36 1.296-2.56 2.64-3.022.116-.04.373-.104.572-.144.198-.04.426-.102.506-.138.296-.136.515-.424.566-.744.017-.11-.007-.549-.089-1.602-.091-1.179-.107-1.483-.083-1.621.057-.342.139-.46 1.01-1.448.447-.506.85-.976.895-1.043.262-.39.288-.91.068-1.345a1.44 1.44 0 00-.822-.67c-.1-.029-.834-.037-3.544-.038H9.897l-.335-.063c-.958-.179-1.765-.49-2.484-.958-.362-.236-.583-.41-1.018-.804-.408-.37-.59-.502-.921-.67A3.018 3.018 0 003.744.005a3.942 3.942 0 00-.391 0zm15.728 5.858c-.132.049-.217.127-.48.44-.592.707-.74 1.336-.531 2.256.106.466.163.572.361.673.105.054.169.055 2.637.046l2.53-.009.118-.063a.551.551 0 00.095-.895 184.88 184.88 0 00-2.223-1.254c-2.293-1.282-2.281-1.276-2.507-1.194zm-3.216 6.71a9.258 9.258 0 00-1.364.696c-.844.557-1.454 1.36-1.923 2.53-.211.525-.202.75.04.935.111.087 6.478 3.14 6.667 3.198.153.047.27.027.43-.074a.538.538 0 00.24-.434c0-.06-.03-.18-.065-.264-.156-.368-3.098-6.467-3.158-6.545-.168-.222-.394-.232-.867-.042zm-8.48 5.457c-.453.102-.83.32-1.285.745-.296.277-.336.468-.167.798.053.103.61 1.104 1.236 2.224 1.297 2.317 1.22 2.206 1.558 2.202.152-.002.198-.015.296-.084a.662.662 0 00.173-.193c.058-.11.06-.152.08-2.595.018-1.93.015-2.51-.011-2.606a.569.569 0 00-.138-.227c-.09-.091-.14-.112-.406-.176-.582-.138-.992-.165-1.336-.088z'
|
||||
d='M240.269 49.8154L166.804 115.963L115.966 159.44L181.335 220.585L249.784 162.048L196.78 112.47L253.068 61.7881L362.605 164.246L178.739 321.489L3.14502 157.242L187.011 0L240.269 49.8154Z'
|
||||
fill='#3D3B4F'
|
||||
/>
|
||||
<rect
|
||||
width='236.453'
|
||||
height='83.4566'
|
||||
transform='matrix(0.75471 -0.656059 0 1 188.017 336.544)'
|
||||
fill='#3D3B4F'
|
||||
/>
|
||||
<rect
|
||||
width='236.453'
|
||||
height='83.4566'
|
||||
transform='matrix(0.731354 0.681998 0 1 0 174.962)'
|
||||
fill='#3D3B4F'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createEnvMock, createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockGetSession,
|
||||
mockRegisterSSOProvider,
|
||||
mockHasSSOAccess,
|
||||
mockValidateUrlWithDNS,
|
||||
dbState,
|
||||
memberTable,
|
||||
ssoProviderTable,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
mockRegisterSSOProvider: vi.fn(),
|
||||
mockHasSSOAccess: vi.fn(),
|
||||
mockValidateUrlWithDNS: vi.fn(),
|
||||
dbState: { members: [] as any[], providers: [] as any[] },
|
||||
memberTable: {
|
||||
userId: 'member.userId',
|
||||
organizationId: 'member.organizationId',
|
||||
role: 'member.role',
|
||||
},
|
||||
ssoProviderTable: {
|
||||
id: 'sso.id',
|
||||
providerId: 'sso.providerId',
|
||||
domain: 'sso.domain',
|
||||
issuer: 'sso.issuer',
|
||||
userId: 'sso.userId',
|
||||
organizationId: 'sso.organizationId',
|
||||
oidcConfig: 'sso.oidcConfig',
|
||||
samlConfig: 'sso.samlConfig',
|
||||
},
|
||||
}))
|
||||
|
||||
function makeBuilder(rows: any[]): any {
|
||||
const thenable: any = Promise.resolve(rows)
|
||||
thenable.where = (condition: any) => {
|
||||
const values = condition?.values
|
||||
if (Array.isArray(values) && values.length > 0) {
|
||||
const target = String(values[values.length - 1]).toLowerCase()
|
||||
return makeBuilder(rows.filter((r) => String(r.domain ?? '').toLowerCase() === target))
|
||||
}
|
||||
return makeBuilder(rows)
|
||||
}
|
||||
thenable.limit = () => Promise.resolve(rows)
|
||||
thenable.orderBy = () => Promise.resolve(rows)
|
||||
return thenable
|
||||
}
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: {
|
||||
select: () => ({
|
||||
from: (table: unknown) =>
|
||||
makeBuilder(table === memberTable ? dbState.members : dbState.providers),
|
||||
}),
|
||||
},
|
||||
member: memberTable,
|
||||
ssoProvider: ssoProviderTable,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
auth: { api: { registerSSOProvider: mockRegisterSSOProvider } },
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/billing', () => ({
|
||||
hasSSOAccess: mockHasSSOAccess,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/sso/domain', () => ({
|
||||
normalizeSSODomain: (input: unknown): string | null => {
|
||||
if (typeof input !== 'string') return null
|
||||
const value = input.trim().toLowerCase()
|
||||
return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(value) ? value : null
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/security/input-validation.server', () => ({
|
||||
validateUrlWithDNS: mockValidateUrlWithDNS,
|
||||
secureFetchWithPinnedIP: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => createEnvMock({ SSO_ENABLED: 'true' }))
|
||||
|
||||
import { POST } from '@/app/api/auth/sso/register/route'
|
||||
|
||||
const OIDC_BODY = {
|
||||
providerType: 'oidc' as const,
|
||||
providerId: 'acme-oidc',
|
||||
issuer: 'https://idp.acme.com',
|
||||
domain: 'acme.com',
|
||||
clientId: 'client-id',
|
||||
clientSecret: 'client-secret',
|
||||
authorizationEndpoint: 'https://idp.acme.com/authorize',
|
||||
tokenEndpoint: 'https://idp.acme.com/token',
|
||||
userInfoEndpoint: 'https://idp.acme.com/userinfo',
|
||||
jwksEndpoint: 'https://idp.acme.com/jwks',
|
||||
}
|
||||
|
||||
function request(body: Record<string, unknown>) {
|
||||
return createMockRequest('POST', body)
|
||||
}
|
||||
|
||||
describe('POST /api/auth/sso/register', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
dbState.members = []
|
||||
dbState.providers = []
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'u1' } })
|
||||
mockHasSSOAccess.mockResolvedValue(true)
|
||||
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '1.2.3.4' })
|
||||
mockRegisterSSOProvider.mockResolvedValue({ providerId: 'acme-oidc' })
|
||||
})
|
||||
|
||||
it('rejects callers without an Enterprise plan', async () => {
|
||||
mockHasSSOAccess.mockResolvedValue(false)
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
|
||||
expect(res.status).toBe(403)
|
||||
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects callers who are not an admin/owner of the target org', async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'member' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
|
||||
expect(res.status).toBe(403)
|
||||
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an invalid domain', async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, domain: 'not-a-domain', orgId: 'org1' }))
|
||||
expect(res.status).toBe(400)
|
||||
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a domain already registered by another organization', async () => {
|
||||
dbState.members = [{ organizationId: 'org-attacker', role: 'owner' }]
|
||||
dbState.providers = [{ domain: 'acme.com', userId: 'u-victim', organizationId: 'org-victim' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org-attacker' }))
|
||||
const json = await res.json()
|
||||
expect(res.status).toBe(409)
|
||||
expect(json.code).toBe('SSO_DOMAIN_ALREADY_REGISTERED')
|
||||
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('matches conflicts across casing variants', async () => {
|
||||
dbState.members = [{ organizationId: 'org-attacker', role: 'owner' }]
|
||||
dbState.providers = [{ domain: 'ACME.com', userId: 'u-victim', organizationId: 'org-victim' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org-attacker' }))
|
||||
expect(res.status).toBe(409)
|
||||
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('registers when the domain is unclaimed', async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
|
||||
expect(res.status).toBe(200)
|
||||
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('allows the owning tenant to update its own provider for the same domain', async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
|
||||
dbState.providers = [{ domain: 'acme.com', userId: 'u1', organizationId: 'org1' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
|
||||
expect(res.status).toBe(200)
|
||||
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('lets an org admin adopt their own user-scoped provider for the same domain', async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
|
||||
dbState.providers = [{ domain: 'acme.com', userId: 'u1', organizationId: null }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
|
||||
expect(res.status).toBe(200)
|
||||
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("still blocks an org admin from claiming another user's user-scoped domain", async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
|
||||
dbState.providers = [{ domain: 'acme.com', userId: 'someone-else', organizationId: null }]
|
||||
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
|
||||
expect(res.status).toBe(409)
|
||||
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('normalizes the domain before persisting it', async () => {
|
||||
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
|
||||
const res = await POST(request({ ...OIDC_BODY, domain: 'ACME.com', orgId: 'org1' }))
|
||||
expect(res.status).toBe(200)
|
||||
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
|
||||
const config = mockRegisterSSOProvider.mock.calls[0][0].body
|
||||
expect(config.domain).toBe('acme.com')
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,12 @@
|
||||
import { db, member, ssoProvider } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { ssoRegistrationContract } from '@/lib/api/contracts/auth'
|
||||
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
|
||||
import { auth, getSession } from '@/lib/auth'
|
||||
import { normalizeSSODomain } from '@/lib/auth/sso/domain'
|
||||
import { hasSSOAccess } from '@/lib/billing'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import {
|
||||
@@ -51,7 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const body = parsed.data.body
|
||||
const { providerId, issuer, domain, providerType, mapping, orgId } = body
|
||||
const { providerId, issuer, providerType, mapping, orgId } = body
|
||||
|
||||
if (orgId) {
|
||||
const [membership] = await db
|
||||
@@ -67,6 +68,43 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
}
|
||||
|
||||
const domain = normalizeSSODomain(body.domain)
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Enter a valid domain like company.com' }, { status: 400 })
|
||||
}
|
||||
|
||||
const isOwnedByCaller = (provider: {
|
||||
userId: string | null
|
||||
organizationId: string | null
|
||||
}): boolean => {
|
||||
if (provider.userId === session.user.id && !provider.organizationId) return true
|
||||
return orgId ? provider.organizationId === orgId : false
|
||||
}
|
||||
|
||||
const existingProviders = await db
|
||||
.select({
|
||||
userId: ssoProvider.userId,
|
||||
organizationId: ssoProvider.organizationId,
|
||||
})
|
||||
.from(ssoProvider)
|
||||
.where(sql`lower(${ssoProvider.domain}) = ${domain}`)
|
||||
const conflictingProvider = existingProviders.find((provider) => !isOwnedByCaller(provider))
|
||||
|
||||
if (conflictingProvider) {
|
||||
logger.warn('Rejected SSO registration for domain owned by another tenant', {
|
||||
domain,
|
||||
orgId,
|
||||
userId: session.user.id,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'This domain is already registered for SSO by another organization.',
|
||||
code: 'SSO_DOMAIN_ALREADY_REGISTERED',
|
||||
},
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
request.headers.forEach((value, key) => {
|
||||
headers[key] = value
|
||||
|
||||
@@ -53,7 +53,9 @@ export const GET = withRouteHandler(async (request: NextRequest, { params }: Rou
|
||||
const encoder = new TextEncoder()
|
||||
try {
|
||||
if (format === 'csv') {
|
||||
controller.enqueue(encoder.encode(`${toCsvRow(columns.map((c) => c.name))}\n`))
|
||||
controller.enqueue(
|
||||
encoder.encode(`${toCsvRow(columns.map((c) => neutralizeCsvFormula(c.name)))}\n`)
|
||||
)
|
||||
} else {
|
||||
controller.enqueue(encoder.encode('['))
|
||||
}
|
||||
@@ -111,10 +113,23 @@ function sanitizeFilename(name: string): string {
|
||||
return cleaned || 'table'
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes a single quote to values starting with a spreadsheet formula trigger
|
||||
* (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets.
|
||||
*/
|
||||
function neutralizeCsvFormula(value: string): string {
|
||||
return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a cell for CSV. Only string cells are formula-neutralized; numbers,
|
||||
* booleans, dates, and JSON objects can never form a trigger and pass through verbatim.
|
||||
*/
|
||||
function formatCsvValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return ''
|
||||
if (value instanceof Date) return value.toISOString()
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
if (typeof value === 'string') return neutralizeCsvFormula(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
} from '@/lib/webhooks/provider-subscriptions'
|
||||
import { getProviderHandler } from '@/lib/webhooks/providers'
|
||||
import { mergeNonUserFields } from '@/lib/webhooks/utils'
|
||||
import { syncWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
import {
|
||||
findConflictingWebhookPathOwner,
|
||||
syncWebhooksForCredentialSet,
|
||||
} from '@/lib/webhooks/utils.server'
|
||||
import { extractCredentialSetId, isCredentialSetValue } from '@/executor/constants'
|
||||
|
||||
const logger = createLogger('WebhooksAPI')
|
||||
@@ -330,21 +333,31 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
}
|
||||
if (!targetWebhookId) {
|
||||
const existingByPath = await db
|
||||
.select({ id: webhook.id, workflowId: webhook.workflowId })
|
||||
const conflictingOwner = await findConflictingWebhookPathOwner({
|
||||
path: finalPath,
|
||||
workflowId,
|
||||
})
|
||||
if (conflictingOwner) {
|
||||
logger.warn(`[${requestId}] Webhook path conflict: ${finalPath}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook path already exists.', code: 'PATH_EXISTS' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
const ownExisting = await db
|
||||
.select({ id: webhook.id })
|
||||
.from(webhook)
|
||||
.where(and(eq(webhook.path, finalPath), isNull(webhook.archivedAt)))
|
||||
.limit(1)
|
||||
if (existingByPath.length > 0) {
|
||||
// If a webhook with the same path exists but belongs to a different workflow, return an error
|
||||
if (existingByPath[0].workflowId !== workflowId) {
|
||||
logger.warn(`[${requestId}] Webhook path conflict: ${finalPath}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook path already exists.', code: 'PATH_EXISTS' },
|
||||
{ status: 409 }
|
||||
.where(
|
||||
and(
|
||||
eq(webhook.path, finalPath),
|
||||
eq(webhook.workflowId, workflowId),
|
||||
isNull(webhook.archivedAt)
|
||||
)
|
||||
}
|
||||
targetWebhookId = existingByPath[0].id
|
||||
)
|
||||
.limit(1)
|
||||
if (ownExisting.length > 0) {
|
||||
targetWebhookId = ownExisting[0].id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+215
-50
@@ -780,7 +780,7 @@ export function GitLabIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function SerperIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox='0 0 654 600' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<svg viewBox='35.411 -19.109 628.098 628.098' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<path
|
||||
d='M324 38C356 37 389 36 417 47C452 56 484 72 509 94C539 118 561 145 577 176C593 205 601 238 606 271C610 343 590 403 552 452C528 482 499 507 467 523C438 539 404 547 372 552C297 556 235 534 184 492C133 449 103 392 93 330C93 292 89 255 102 224C112 189 128 158 149 132C194 78 255 46 322 38'
|
||||
fill='rgb(71,97,118)'
|
||||
@@ -1298,7 +1298,7 @@ export function GoogleSheetsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export const S3Icon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='-3.097 -3 70 70'
|
||||
viewBox='0 0 80 80'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
@@ -2175,27 +2175,8 @@ export function MistralIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function BrainIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M12 5a3 3 0 1 0-6.125 4 4 0 0 0-2.53 5.77 4 4 0 0 0 .556 6.59A4 4 0 1 0 12 18Z' />
|
||||
<path d='M12 5a3 3 0 1 1 6.125 4 4 0 0 1 2.53 5.77 4 4 0 0 1-.556 6.59A4 4 0 1 1 12 18Z' />
|
||||
<path d='M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4' />
|
||||
<path d='M17.6 6.5a3 3 0 0 0 .399-1.37' />
|
||||
<path d='M6 5.13A3 3 0 0 0 6.4 6.5' />
|
||||
<path d='M3.48 10.9a4 4 0 0 1 .585-.396' />
|
||||
<path d='M19.94 10.5a4 4 0 0 1 .585.4' />
|
||||
<path d='M6 18a4 4 0 0 1-1.97-.516' />
|
||||
<path d='M19.97 17.48A4 4 0 0 1 18 18' />
|
||||
<svg {...props} viewBox='0 0 256 256' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M212,76V72a44,44,0,0,0-74.86-31.31,3.93,3.93,0,0,0-1.14,2.8v88.72a4,4,0,0,0,6.2,3.33A47.67,47.67,0,0,1,167.68,128a8.18,8.18,0,0,1,8.31,7.58,8,8,0,0,1-8,8.42,32,32,0,0,0-32,32v33.88a4,4,0,0,0,1.49,3.12,47.92,47.92,0,0,0,74.21-17.16,4,4,0,0,0-4.49-5.56A68.06,68.06,0,0,1,192,192h-7.73a8.18,8.18,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h8a51.6,51.6,0,0,0,24-5.88v0A52,52,0,0,0,212,76Zm-12,36h-4a36,36,0,0,1-36-36V72a8,8,0,0,1,16,0v4a20,20,0,0,0,20,20h4a8,8,0,0,1,0,16ZM88,28A44.05,44.05,0,0,0,44,72v4a52,52,0,0,0-4,94.12h0A51.6,51.6,0,0,0,64,176h7.73A8.18,8.18,0,0,1,80,183.47,8,8,0,0,1,72,192H64a67.48,67.48,0,0,1-15.21-1.73,4,4,0,0,0-4.5,5.55A47.93,47.93,0,0,0,118.51,213a4,4,0,0,0,1.49-3.12V176a32,32,0,0,0-32-32,8,8,0,0,1-8-8.42A8.18,8.18,0,0,1,88.32,128a47.67,47.67,0,0,1,25.48,7.54,4,4,0,0,0,6.2-3.33V43.49a4,4,0,0,0-1.14-2.81A43.85,43.85,0,0,0,88,28Zm8,48a36,36,0,0,1-36,36H56a8,8,0,0,1,0-16h4A20,20,0,0,0,80,76V72a8,8,0,0,1,16,0Z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -2342,8 +2323,8 @@ export function ExtendIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
|
||||
<path d='M29.34 16.82c.1 1.7-.08 3.37-.305 5.05-.225 1.71-.508 3.42-.964 5.08-.3 1.07-.673 2.1-1.2 3.07-.65 1.19-1.63 1.87-2.99 1.92l-3.83.036c-.636-.017-1.28-.146-1.9-.297-1.19-.3-1.86-1.1-2.06-2.3-.186-1.08-.173-2.19.04-3.26.252-1.23 1-1.96 2.23-2.1.817-.1 1.65-.077 2.48-.1.21-.7.28.98.2.287-.196.53-.236 1.07-.098 1.62.53.21-.23.31-.26.31a7.77 7.77 0 0 0-1.12.053c-.636.09-.96.47-.96 1.11 0 .205.03.416.07.622.1.507.45.78.94.837 1.12.127 2.25.138 3.37-.5.68-.114 1.08-.54 1.16-1.21.152-1.3.16-2.59-.228-3.84-.33-1.09-1.01-1.56-2.13-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.76-.013-.106-.074-.208-.1-.3-.04.1-.106.21-.117.33-.066.68-.053 1.37-.185 2.04-.16.8-.404 1.57-.67 2.33-.185.54-.616.84-1.2.8a37.76 37.76 0 0 1-7.12-1.35l-.64-.207c-.927-.26-1.49-.903-1.74-1.79l-1-3.85-.74-4.3c-.115-.755-.2-1.52-.083-2.29.154-1.11.914-1.9 2.04-1.96l3.56-.062c.127 0 .254.373-.026a1.23 1.23 0 0 0 1.01-1.25l-.05-3.04c-.048-1.58.8-2.38 2.16-2.62a10.58 10.58 0 0 1 4.91.26c.933.28 1.47.923 1.72 1.83.06.22.15.3.37.29l2.58.01 3.33.37c.686.1 1.36.25 2.03.42 1.17.298 1.79 1.11 1.96 2.26l.357 3.36.3 5.58.01 2.28zm-4.53-1.15c-.02-.666-.07-1.27-.444-1.78a1.66 1.66 0 0 0-2.47-.15c-.364.4-.494.88-.564 1.4-.8.03.106.13.16.13l.8-.053c.768.01 1.52.113 2.25.39.66.03.136.4.26.077zM8.79 1.15a3.82 3.82 0 0 0-.278 1.59l.05 2.93c.5.36-.075.45-.433.45L5.1 6.16c-.583 0-1.14.1-1.55.278l5.2-5.33c.2.01.4.03.6.05z' />
|
||||
<svg {...props} viewBox='0 0 32 32' fill='#7fce2c' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -2538,7 +2519,7 @@ export function TelegramIcon(props: SVGProps<SVGSVGElement>) {
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
viewBox='1 1 22 22'
|
||||
width='24'
|
||||
height='24'
|
||||
fill='none'
|
||||
@@ -3635,26 +3616,200 @@ export function HunterIOIcon(props: SVGProps<SVGSVGElement>) {
|
||||
}
|
||||
|
||||
export function MicrosoftOneDriveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
return (
|
||||
<svg {...props} fill='currentColor' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g>
|
||||
<path
|
||||
d='M12.2,11.19l.00031-.0011,6.72,4.02,4-1.68.18A6.48,6.48,0,0,1,25.5,13c.14764,0,.29358.1.44.02a10,10,0,0,0-18.04-3.01C7.93,10,7.97,10,8,10A7.96,7.96,0,0,1,12.2,11.19Z'
|
||||
fill='#0364b8'
|
||||
/>
|
||||
<path
|
||||
d='M12.2,11.19l-.31A7.96,7.96,0,0,0,8,10c-.0343,0-.6805-.10223A8,8,0,0,0,1.44,22.57l5.92-2.49,2.63-1.11,5.86-2.47,3.06-1.29Z'
|
||||
fill='#0078d4'
|
||||
/>
|
||||
<path
|
||||
d='M25.94,13.02C25.79,13.01,25.65,13,25.5,13a6.48,6.48,0,0,0-2.58.53l-.00018-.00068-4,1.68,1.16.7L23.89,18.19l1.66.99,5.68,3.4a6.5,6.5,0,0,0-5.28-9.57Z'
|
||||
fill='#1490df'
|
||||
/>
|
||||
<path
|
||||
d='M25.55,19.18,23.89,18.19l-3.8-2.28-1.16-.69528L15.86,16.5,9.99,18.97,7.36,20.08l-5.92,2.49A7.99,7.99,0,0,0,8,26H25.5a6.5,6.5,0,0,0,5.72-3.42Z'
|
||||
fill='#28a8ea'
|
||||
/>
|
||||
</g>
|
||||
<svg {...props} viewBox='35.98 139.2 648.03 430.85' xmlns='http://www.w3.org/2000/svg'>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id={`onedrive_radial0_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(130.864814,156.804864,-260.089994,217.063603,48.669602,228.766494)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(28.235294%,58.039216%,99.607843%)' stopOpacity={1} />
|
||||
<stop
|
||||
offset='0.695072'
|
||||
stopColor='rgb(3.529412%,20.392157%,70.196078%)'
|
||||
stopOpacity={1}
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial1_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-575.289668,663.594003,-491.728488,-426.294267,596.956501,-6.380235)'
|
||||
>
|
||||
<stop
|
||||
offset='0.165327'
|
||||
stopColor='rgb(13.72549%,75.294118%,99.607843%)'
|
||||
stopOpacity={1}
|
||||
/>
|
||||
<stop offset='0.534' stopColor='rgb(10.980392%,56.862745%,100%)' stopOpacity={1} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial2_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-136.753383,-114.806698,262.816935,-313.057562,181.196995,240.395994)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(100%,100%,100%)' stopOpacity={0.4} />
|
||||
<stop offset='0.660528' stopColor='rgb(67.843137%,75.294118%,100%)' stopOpacity={0} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial3_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-153.638428,-130.000063,197.433014,-233.332948,375.353994,451.43549)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(1.176471%,22.745098%,80%)' stopOpacity={1} />
|
||||
<stop offset='1' stopColor='rgb(21.176471%,55.686275%,100%)' stopOpacity={0} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial4_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(175.585899,405.198026,-437.434522,189.555055,169.378495,125.589294)'
|
||||
>
|
||||
<stop
|
||||
offset='0.592618'
|
||||
stopColor='rgb(20.392157%,39.215686%,89.019608%)'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
<stop offset='1' stopColor='rgb(1.176471%,22.745098%,80%)' stopOpacity={0.6} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial5_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-459.329491,459.329491,-719.614455,-719.614455,589.876499,39.484649)'
|
||||
>
|
||||
<stop
|
||||
offset='0'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0.898039}
|
||||
/>
|
||||
<stop
|
||||
offset='0.543937'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id={`onedrive_linear0_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='29.999701'
|
||||
y1='37.9823'
|
||||
x2='29.999701'
|
||||
y2='18.398199'
|
||||
gradientTransform='matrix(15,0,0,15,0,0)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(0%,52.54902%,100%)' stopOpacity={1} />
|
||||
<stop offset='0.49' stopColor='rgb(0%,73.333333%,100%)' stopOpacity={1} />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial6_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(273.622108,108.513684,-205.488428,518.148261,296.488495,307.441492)'
|
||||
>
|
||||
<stop offset='0' stopColor='rgb(100%,100%,100%)' stopOpacity={0.4} />
|
||||
<stop offset='0.785262' stopColor='rgb(100%,100%,100%)' stopOpacity={0} />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`onedrive_radial7_${id}`}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
cx='0'
|
||||
cy='0'
|
||||
fx='0'
|
||||
fy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(-305.683909,263.459223,-264.352324,-306.720147,674.845505,249.378004)'
|
||||
>
|
||||
<stop
|
||||
offset='0'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0.898039}
|
||||
/>
|
||||
<stop
|
||||
offset='0.584724'
|
||||
stopColor='rgb(29.411765%,99.215686%,90.980392%)'
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill={`url(#onedrive_radial0_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.078125 205.089844 C 116.011719 205.09375 41.957031 286.1875 36.382812 376.527344 C 39.835938 395.992188 51.175781 434.429688 68.941406 432.457031 C 91.144531 429.988281 147.066406 432.457031 194.765625 346.105469 C 229.609375 283.027344 301.285156 205.085938 215.078125 205.089844 Z M 215.078125 205.089844 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial1_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial2_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial3_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial4_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial5_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_linear0_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial6_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 '
|
||||
/>
|
||||
<path
|
||||
fill={`url(#onedrive_radial7_${id})`}
|
||||
fillRule='nonzero'
|
||||
d='M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 '
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -5882,12 +6037,22 @@ export function GreenhouseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function GreptileIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg {...props} viewBox='0 0 367 420' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
clipRule='evenodd'
|
||||
fillRule='evenodd'
|
||||
fill='#44A775'
|
||||
d='M3.353.004a6.074 6.074 0 01-.265.045C2.63.12 2.092.348 1.71.633 1.426.846.717 1.575.557 1.819a3.359 3.359 0 00-.23 3.296c.154.322.35.59.71.972.187.198.434.486.55.64a6.629 6.629 0 011.305 3.546c.01.138.035 1.607.057 3.264.043 3.273.038 3.18.203 3.485.266.494.94.79 1.474.648.29-.077.463-.204 1.353-.986.957-.84 1.092-.932 1.446-.98.124-.017.631 0 1.66.053 1.513.08 1.622.079 1.85-.016.393-.164.539-.4.661-1.074.247-1.36 1.296-2.56 2.64-3.022.116-.04.373-.104.572-.144.198-.04.426-.102.506-.138.296-.136.515-.424.566-.744.017-.11-.007-.549-.089-1.602-.091-1.179-.107-1.483-.083-1.621.057-.342.139-.46 1.01-1.448.447-.506.85-.976.895-1.043.262-.39.288-.91.068-1.345a1.44 1.44 0 00-.822-.67c-.1-.029-.834-.037-3.544-.038H9.897l-.335-.063c-.958-.179-1.765-.49-2.484-.958-.362-.236-.583-.41-1.018-.804-.408-.37-.59-.502-.921-.67A3.018 3.018 0 003.744.005a3.942 3.942 0 00-.391 0zm15.728 5.858c-.132.049-.217.127-.48.44-.592.707-.74 1.336-.531 2.256.106.466.163.572.361.673.105.054.169.055 2.637.046l2.53-.009.118-.063a.551.551 0 00.095-.895 184.88 184.88 0 00-2.223-1.254c-2.293-1.282-2.281-1.276-2.507-1.194zm-3.216 6.71a9.258 9.258 0 00-1.364.696c-.844.557-1.454 1.36-1.923 2.53-.211.525-.202.75.04.935.111.087 6.478 3.14 6.667 3.198.153.047.27.027.43-.074a.538.538 0 00.24-.434c0-.06-.03-.18-.065-.264-.156-.368-3.098-6.467-3.158-6.545-.168-.222-.394-.232-.867-.042zm-8.48 5.457c-.453.102-.83.32-1.285.745-.296.277-.336.468-.167.798.053.103.61 1.104 1.236 2.224 1.297 2.317 1.22 2.206 1.558 2.202.152-.002.198-.015.296-.084a.662.662 0 00.173-.193c.058-.11.06-.152.08-2.595.018-1.93.015-2.51-.011-2.606a.569.569 0 00-.138-.227c-.09-.091-.14-.112-.406-.176-.582-.138-.992-.165-1.336-.088z'
|
||||
d='M240.269 49.8154L166.804 115.963L115.966 159.44L181.335 220.585L249.784 162.048L196.78 112.47L253.068 61.7881L362.605 164.246L178.739 321.489L3.14502 157.242L187.011 0L240.269 49.8154Z'
|
||||
fill='#3D3B4F'
|
||||
/>
|
||||
<rect
|
||||
width='236.453'
|
||||
height='83.4566'
|
||||
transform='matrix(0.75471 -0.656059 0 1 188.017 336.544)'
|
||||
fill='#3D3B4F'
|
||||
/>
|
||||
<rect
|
||||
width='236.453'
|
||||
height='83.4566'
|
||||
transform='matrix(0.731354 0.681998 0 1 0 174.962)'
|
||||
fill='#3D3B4F'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeSSODomain } from '@/lib/auth/sso/domain'
|
||||
|
||||
describe('normalizeSSODomain', () => {
|
||||
it('lowercases and trims', () => {
|
||||
expect(normalizeSSODomain(' Company.COM ')).toBe('company.com')
|
||||
})
|
||||
|
||||
it('strips protocol, path, query, and port', () => {
|
||||
expect(normalizeSSODomain('https://company.com/sso?x=1')).toBe('company.com')
|
||||
expect(normalizeSSODomain('company.com:8443')).toBe('company.com')
|
||||
})
|
||||
|
||||
it('strips wildcard, leading @, and email local part', () => {
|
||||
expect(normalizeSSODomain('*.company.com')).toBe('company.com')
|
||||
expect(normalizeSSODomain('@company.com')).toBe('company.com')
|
||||
expect(normalizeSSODomain('user@company.com')).toBe('company.com')
|
||||
})
|
||||
|
||||
it('drops a trailing dot', () => {
|
||||
expect(normalizeSSODomain('company.com.')).toBe('company.com')
|
||||
})
|
||||
|
||||
it('treats casing and formatting variants as the same domain', () => {
|
||||
expect(normalizeSSODomain('Company.COM')).toBe(normalizeSSODomain('company.com'))
|
||||
expect(normalizeSSODomain('user@Company.com')).toBe(normalizeSSODomain('company.com'))
|
||||
})
|
||||
|
||||
it('rejects values that are not registrable domains', () => {
|
||||
expect(normalizeSSODomain('')).toBeNull()
|
||||
expect(normalizeSSODomain('localhost')).toBeNull()
|
||||
expect(normalizeSSODomain('not a domain')).toBeNull()
|
||||
expect(normalizeSSODomain('company')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Normalizes a user-supplied SSO email domain to a canonical, comparable form:
|
||||
* strips protocol, path, query, port, a leading wildcard/`@`, an email local
|
||||
* part, and a trailing dot, then lowercases. Returns `null` for inputs that are
|
||||
* not a registrable domain (e.g. `example.com`), which callers treat as invalid.
|
||||
*/
|
||||
export function normalizeSSODomain(input: string): string | null {
|
||||
if (typeof input !== 'string') return null
|
||||
|
||||
let value = input.trim().toLowerCase()
|
||||
if (!value) return null
|
||||
|
||||
value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//, '')
|
||||
value = value.replace(/^\*\./, '').replace(/^@/, '')
|
||||
value = value.split('/')[0]
|
||||
value = value.split('?')[0]
|
||||
value = value.split('@').pop() ?? value
|
||||
value = value.split(':')[0]
|
||||
value = value.replace(/\.$/, '')
|
||||
|
||||
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(value)) return null
|
||||
if (value.split('.').some((label) => label.length === 0 || label.length > 63)) return null
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
shouldRecreateExternalWebhookSubscription,
|
||||
} from '@/lib/webhooks/provider-subscriptions'
|
||||
import { getProviderHandler } from '@/lib/webhooks/providers'
|
||||
import { syncWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
import {
|
||||
findConflictingWebhookPathOwner,
|
||||
syncWebhooksForCredentialSet,
|
||||
} from '@/lib/webhooks/utils.server'
|
||||
import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
@@ -538,6 +541,23 @@ export async function saveTriggerWebhooksForDeploy({
|
||||
}
|
||||
}
|
||||
|
||||
const pathConflict = await findConflictingWebhookPathOwner({
|
||||
path: triggerPath,
|
||||
workflowId,
|
||||
})
|
||||
if (pathConflict) {
|
||||
logger.warn(
|
||||
`[${requestId}] Webhook path conflict for "${triggerPath}": already owned by workflow ${pathConflict}`
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Webhook path "${triggerPath}" is already in use. Choose a different path.`,
|
||||
status: 409,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
webhookConfigs.set(block.id, { provider, providerConfig, triggerPath, triggerDef })
|
||||
|
||||
if (providerConfig.credentialSetId) {
|
||||
|
||||
@@ -10,23 +10,36 @@ import {
|
||||
} from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockGenerateId, mockEnqueue, mockGetJobQueue, mockShouldExecuteInline } = vi.hoisted(
|
||||
() => ({
|
||||
mockGenerateId: vi.fn(),
|
||||
mockEnqueue: vi.fn(),
|
||||
mockGetJobQueue: vi.fn(),
|
||||
mockShouldExecuteInline: vi.fn(),
|
||||
})
|
||||
)
|
||||
const {
|
||||
mockGenerateId,
|
||||
mockEnqueue,
|
||||
mockGetJobQueue,
|
||||
mockShouldExecuteInline,
|
||||
mockWebhookLookupResult,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGenerateId: vi.fn(),
|
||||
mockEnqueue: vi.fn(),
|
||||
mockGetJobQueue: vi.fn(),
|
||||
mockShouldExecuteInline: vi.fn(),
|
||||
mockWebhookLookupResult: { rows: [] as Array<{ webhook: any; workflow: any }> },
|
||||
}))
|
||||
|
||||
const mockPreprocessExecution = executionPreprocessingMockFns.mockPreprocessExecution
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: {},
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
workflowDeploymentVersion: {},
|
||||
}))
|
||||
vi.mock('@sim/db', () => {
|
||||
const selectChain = {
|
||||
from: () => selectChain,
|
||||
innerJoin: () => selectChain,
|
||||
leftJoin: () => selectChain,
|
||||
where: () => Promise.resolve(mockWebhookLookupResult.rows),
|
||||
}
|
||||
return {
|
||||
db: { select: () => selectChain },
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
workflowDeploymentVersion: {},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
and: vi.fn(),
|
||||
@@ -109,7 +122,74 @@ vi.mock('@/triggers/jira/utils', () => ({
|
||||
isJiraEventMatch: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
import { checkWebhookPreprocessing, queueWebhookExecution } from '@/lib/webhooks/processor'
|
||||
import {
|
||||
checkWebhookPreprocessing,
|
||||
findAllWebhooksForPath,
|
||||
queueWebhookExecution,
|
||||
} from '@/lib/webhooks/processor'
|
||||
|
||||
describe('findAllWebhooksForPath cross-tenant collision', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWebhookLookupResult.rows = []
|
||||
})
|
||||
|
||||
const makeRow = (workflowId: string, webhookId: string, createdAt: Date) => ({
|
||||
webhook: { id: webhookId, workflowId, path: 'shared-path', createdAt },
|
||||
workflow: { id: workflowId },
|
||||
})
|
||||
|
||||
it('returns all rows when they belong to a single workflow (credential set fan-out)', async () => {
|
||||
mockWebhookLookupResult.rows = [
|
||||
makeRow('workflow-1', 'wh-a', new Date('2026-01-01')),
|
||||
makeRow('workflow-1', 'wh-b', new Date('2026-01-02')),
|
||||
]
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-1', path: 'shared-path' })
|
||||
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results.map((r) => r.webhook.id)).toEqual(['wh-a', 'wh-b'])
|
||||
})
|
||||
|
||||
it('drops foreign rows when a path collides across workflows, keeping the earliest owner', async () => {
|
||||
const victim = makeRow('victim-workflow', 'victim-wh', new Date('2026-01-01'))
|
||||
const attacker = makeRow('attacker-workflow', 'attacker-wh', new Date('2026-05-01'))
|
||||
mockWebhookLookupResult.rows = [attacker, victim]
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-2', path: 'shared-path' })
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].webhook.id).toBe('victim-wh')
|
||||
expect(results[0].webhook.workflowId).toBe('victim-workflow')
|
||||
})
|
||||
|
||||
it("preserves the owner's full credential-set fan-out while dropping a foreign row", async () => {
|
||||
const victimA = makeRow('victim-workflow', 'victim-wh-a', new Date('2026-01-01'))
|
||||
const victimB = makeRow('victim-workflow', 'victim-wh-b', new Date('2026-01-03'))
|
||||
const attacker = makeRow('attacker-workflow', 'attacker-wh', new Date('2026-05-01'))
|
||||
mockWebhookLookupResult.rows = [victimB, attacker, victimA]
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-5', path: 'shared-path' })
|
||||
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results.every((r) => r.webhook.workflowId === 'victim-workflow')).toBe(true)
|
||||
expect(results.map((r) => r.webhook.id).sort()).toEqual(['victim-wh-a', 'victim-wh-b'])
|
||||
})
|
||||
|
||||
it('returns an empty array when no webhooks match', async () => {
|
||||
mockWebhookLookupResult.rows = []
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-3', path: 'missing' })
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
it('returns an empty array when path is not provided', async () => {
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-4' })
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('webhook processor execution identity', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -305,8 +305,12 @@ async function findWebhookAndWorkflow(
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ALL webhooks matching a path.
|
||||
* Used for credential sets where multiple webhooks share the same path.
|
||||
* Finds all webhooks matching a path, scoped to a single workflow.
|
||||
*
|
||||
* Legitimate fan-out (credential sets) is always within one workflow, but paths
|
||||
* are user-controlled and only unique per deployment version, so two tenants can
|
||||
* register the same path. On collision we keep only the workflow that registered
|
||||
* the path first, so one tenant can never receive another's webhook deliveries.
|
||||
*/
|
||||
export async function findAllWebhooksForPath(
|
||||
options: WebhookProcessorOptions
|
||||
@@ -344,7 +348,31 @@ export async function findAllWebhooksForPath(
|
||||
|
||||
if (results.length === 0) {
|
||||
logger.warn(`[${options.requestId}] No active webhooks found for path: ${options.path}`)
|
||||
} else if (results.length > 1) {
|
||||
return results
|
||||
}
|
||||
|
||||
const distinctWorkflowIds = new Set(results.map((result) => result.webhook.workflowId))
|
||||
|
||||
if (distinctWorkflowIds.size > 1) {
|
||||
const owner = results.reduce((earliest, candidate) => {
|
||||
const candidateTime = new Date(candidate.webhook.createdAt).getTime()
|
||||
const earliestTime = new Date(earliest.webhook.createdAt).getTime()
|
||||
if (candidateTime !== earliestTime) {
|
||||
return candidateTime < earliestTime ? candidate : earliest
|
||||
}
|
||||
return candidate.webhook.id < earliest.webhook.id ? candidate : earliest
|
||||
})
|
||||
const ownerWorkflowId = owner.webhook.workflowId
|
||||
const ownerResults = results.filter((result) => result.webhook.workflowId === ownerWorkflowId)
|
||||
|
||||
logger.error(
|
||||
`[${options.requestId}] Cross-tenant webhook path collision for path: ${options.path}. Found ${results.length} active webhooks across ${distinctWorkflowIds.size} workflows. Dispatching only to owner workflow ${ownerWorkflowId} and dropping ${results.length - ownerResults.length} foreign webhook(s).`
|
||||
)
|
||||
|
||||
return ownerResults
|
||||
}
|
||||
|
||||
if (results.length > 1) {
|
||||
logger.info(
|
||||
`[${options.requestId}] Found ${results.length} webhooks for path: ${options.path} (credential set fan-out)`
|
||||
)
|
||||
|
||||
@@ -10,6 +10,43 @@ import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
|
||||
import { getCredentialsForCredentialSet } from '@/app/api/auth/oauth/utils'
|
||||
import { isPollingWebhookProvider } from '@/triggers/constants'
|
||||
|
||||
/**
|
||||
* Returns the id of a different workflow that already owns an active webhook on
|
||||
* the given path, or `null` if the path is free or owned by `workflowId`.
|
||||
*
|
||||
* Webhook paths are user-controlled and the database only enforces uniqueness
|
||||
* per deployment version, so this is the single guard against cross-tenant path
|
||||
* collisions for every webhook creation path. The filter mirrors the runtime
|
||||
* dispatcher (`findAllWebhooksForPath`): an active, non-archived webhook on a
|
||||
* non-archived workflow — inactive or archived webhooks never receive
|
||||
* deliveries, so they must not reserve a path. All matching rows are scanned so
|
||||
* a same-workflow row can never mask a foreign collision.
|
||||
*/
|
||||
export async function findConflictingWebhookPathOwner(params: {
|
||||
path: string
|
||||
workflowId: string
|
||||
tx?: DbOrTx
|
||||
}): Promise<string | null> {
|
||||
const { path, workflowId, tx } = params
|
||||
const dbCtx = tx ?? db
|
||||
|
||||
const existing = await dbCtx
|
||||
.select({ workflowId: webhook.workflowId })
|
||||
.from(webhook)
|
||||
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
|
||||
.where(
|
||||
and(
|
||||
eq(webhook.path, path),
|
||||
eq(webhook.isActive, true),
|
||||
isNull(webhook.archivedAt),
|
||||
isNull(workflow.archivedAt)
|
||||
)
|
||||
)
|
||||
|
||||
const conflict = existing.find((row) => row.workflowId !== workflowId)
|
||||
return conflict ? conflict.workflowId : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of syncing webhooks for a credential set
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user