From 94d5ade8728fe1060f69187117228d267cddfe64 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Apr 2026 21:15:38 -0700 Subject: [PATCH 1/6] fix(secrets): restore unsaved-changes guard for settings tab navigation - Add useSettingsDirtyStore (stores/settings/dirty) to track dirty state across the settings sidebar and section components - Wire credentials-manager and integrations-manager to sync dirty state to the store and clean up on unmount; also reset store synchronously in handleDiscardAndNavigate - Update settings-sidebar to check dirty state before tab switches and Back navigation, showing an Unsaved Changes dialog if needed - Remove dead stores/settings/environment directory; move EnvironmentVariable type into lib/environment/api --- apps/sim/app/api/environment/route.ts | 2 +- .../credentials/credentials-manager.tsx | 13 +++- .../integrations/integrations-manager.tsx | 10 +++ .../settings-sidebar/settings-sidebar.tsx | 75 +++++++++++++++++-- apps/sim/hooks/queries/environment.ts | 6 +- apps/sim/lib/environment/api.ts | 6 +- apps/sim/stores/settings/dirty/store.ts | 53 +++++++++++++ apps/sim/stores/settings/environment/index.ts | 1 - apps/sim/stores/settings/environment/types.ts | 17 ----- apps/sim/tools/utils.ts | 2 +- 10 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 apps/sim/stores/settings/dirty/store.ts delete mode 100644 apps/sim/stores/settings/environment/index.ts delete mode 100644 apps/sim/stores/settings/environment/types.ts diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts index 34195a055dd..229ba26382f 100644 --- a/apps/sim/app/api/environment/route.ts +++ b/apps/sim/app/api/environment/route.ts @@ -10,7 +10,7 @@ import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { generateId } from '@/lib/core/utils/uuid' import { syncPersonalEnvCredentialsForUser } from '@/lib/credentials/environment' -import type { EnvironmentVariable } from '@/stores/settings/environment' +import type { EnvironmentVariable } from '@/lib/environment/api' const logger = createLogger('EnvironmentAPI') diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx index f35949a382a..1f345d114c9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx @@ -51,6 +51,7 @@ import { type WorkspaceEnvironmentData, } from '@/hooks/queries/environment' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' const logger = createLogger('SecretsManager') @@ -482,6 +483,15 @@ export function CredentialsManager() { hasChangesRef.current = hasChanges shouldBlockNavRef.current = hasChanges || isDetailsDirty + const setNavGuardDirty = useSettingsDirtyStore((s) => s.setDirty) + const resetNavGuard = useSettingsDirtyStore((s) => s.reset) + + useEffect(() => { + setNavGuardDirty(hasChanges || isDetailsDirty) + }, [hasChanges, isDetailsDirty, setNavGuardDirty]) + + useEffect(() => () => resetNavGuard(), [resetNavGuard]) + // --- Effects --- useEffect(() => { if (hasSavedRef.current) return @@ -981,6 +991,7 @@ export function CredentialsManager() { const handleDiscardAndNavigate = useCallback(() => { shouldBlockNavRef.current = false + resetNavGuard() resetToSaved() setSelectedCredentialId(null) @@ -989,7 +1000,7 @@ export function CredentialsManager() { pendingNavigationUrlRef.current = null router.push(url) } - }, [router, resetToSaved]) + }, [router, resetToSaved, resetNavGuard]) const renderEnvVarRow = useCallback( (envVar: UIEnvironmentVariable, originalIndex: number) => { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx index d77e761f594..a688a44091a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx @@ -54,6 +54,7 @@ import { } from '@/hooks/queries/oauth/oauth-connections' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' import { useOAuthReturnRouter } from '@/hooks/use-oauth-return' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' const logger = createLogger('IntegrationsManager') @@ -247,6 +248,15 @@ export function IntegrationsManager() { const isDetailsDirty = isDescriptionDirty || isDisplayNameDirty + const setNavGuardDirty = useSettingsDirtyStore((s) => s.setDirty) + const resetNavGuard = useSettingsDirtyStore((s) => s.reset) + + useEffect(() => { + setNavGuardDirty(isDetailsDirty) + }, [isDetailsDirty, setNavGuardDirty]) + + useEffect(() => () => resetNavGuard(), [resetNavGuard]) + const handleSaveDetails = async () => { if (!selectedCredential || !isSelectedAdmin || !isDetailsDirty || updateCredential.isPending) return diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index 07062fc1081..3e5ea1afc27 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -1,9 +1,18 @@ 'use client' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' import { useParams, usePathname, useRouter } from 'next/navigation' -import { ChevronDown, Skeleton } from '@/components/emcn' +import { + Button, + ChevronDown, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Skeleton, +} from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client' import { isHosted } from '@/lib/core/config/feature-flags' @@ -23,6 +32,7 @@ import { useOrganizations } from '@/hooks/queries/organization' import { prefetchSubscriptionData, useSubscriptionData } from '@/hooks/queries/subscription' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' const SKELETON_SECTIONS = [3, 2, 2] as const @@ -41,6 +51,13 @@ export function SettingsSidebar({ const router = useRouter() const queryClient = useQueryClient() + + const requestNavigation = useSettingsDirtyStore((s) => s.requestNavigation) + const confirmNavigation = useSettingsDirtyStore((s) => s.confirmNavigation) + const cancelNavigation = useSettingsDirtyStore((s) => s.cancelNavigation) + const isDirty = useSettingsDirtyStore((s) => s.isDirty) + const [showDiscardDialog, setShowDiscardDialog] = useState(false) + const { data: session, isPending: sessionLoading } = useSession() const { data: organizationsData, isLoading: orgsLoading } = useOrganizations() const { data: generalSettings } = useGeneralSettings() @@ -180,8 +197,28 @@ export function SettingsSidebar({ const { popSettingsReturnUrl, getSettingsHref } = useSettingsNavigation() const handleBack = useCallback(() => { + if (isDirty) { + setShowDiscardDialog(true) + return + } router.push(popSettingsReturnUrl(`/workspace/${workspaceId}/home`)) - }, [router, popSettingsReturnUrl, workspaceId]) + }, [router, popSettingsReturnUrl, workspaceId, isDirty]) + + const handleConfirmDiscard = useCallback(() => { + const section = confirmNavigation() + setShowDiscardDialog(false) + if (section) { + router.replace(getSettingsHref({ section }), { scroll: false }) + } else { + // Triggered by the back button — no pending section was set + router.push(popSettingsReturnUrl(`/workspace/${workspaceId}/home`)) + } + }, [confirmNavigation, router, getSettingsHref, popSettingsReturnUrl, workspaceId]) + + const handleCancelDiscard = useCallback(() => { + cancelNavigation() + setShowDiscardDialog(false) + }, [cancelNavigation]) return ( <> @@ -286,11 +323,14 @@ export function SettingsSidebar({ className={itemClassName} onMouseEnter={() => handlePrefetch(item.id)} onFocus={() => handlePrefetch(item.id)} - onClick={() => - router.replace(getSettingsHref({ section: item.id as SettingsSection }), { - scroll: false, - }) - } + onClick={() => { + const section = item.id as SettingsSection + if (!requestNavigation(section)) { + setShowDiscardDialog(true) + return + } + router.replace(getSettingsHref({ section }), { scroll: false }) + }} > {content} @@ -312,6 +352,25 @@ export function SettingsSidebar({ }) )} + + !open && handleCancelDiscard()}> + + Unsaved Changes + +

+ You have unsaved changes. Are you sure you want to discard them? +

+
+ + + + +
+
) } diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts index 6d9c2dab94a..a2ecf4112a9 100644 --- a/apps/sim/hooks/queries/environment.ts +++ b/apps/sim/hooks/queries/environment.ts @@ -1,13 +1,11 @@ import { createLogger } from '@sim/logger' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import type { WorkspaceEnvironmentData } from '@/lib/environment/api' +import type { EnvironmentVariable, WorkspaceEnvironmentData } from '@/lib/environment/api' import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api' import { workspaceCredentialKeys } from '@/hooks/queries/credentials' import { API_ENDPOINTS } from '@/stores/constants' -import type { EnvironmentVariable } from '@/stores/settings/environment' -export type { WorkspaceEnvironmentData } from '@/lib/environment/api' -export type { EnvironmentVariable } from '@/stores/settings/environment' +export type { EnvironmentVariable, WorkspaceEnvironmentData } from '@/lib/environment/api' const logger = createLogger('EnvironmentQueries') diff --git a/apps/sim/lib/environment/api.ts b/apps/sim/lib/environment/api.ts index bdb22fc9f42..5c7c8c66ae5 100644 --- a/apps/sim/lib/environment/api.ts +++ b/apps/sim/lib/environment/api.ts @@ -1,5 +1,9 @@ import { API_ENDPOINTS } from '@/stores/constants' -import type { EnvironmentVariable } from '@/stores/settings/environment' + +export interface EnvironmentVariable { + key: string + value: string +} export interface WorkspaceEnvironmentData { workspace: Record diff --git a/apps/sim/stores/settings/dirty/store.ts b/apps/sim/stores/settings/dirty/store.ts new file mode 100644 index 00000000000..4dbec7bf720 --- /dev/null +++ b/apps/sim/stores/settings/dirty/store.ts @@ -0,0 +1,53 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' +import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' + +interface SettingsDirtyStore { + isDirty: boolean + pendingSection: SettingsSection | null + setDirty: (dirty: boolean) => void + /** + * Call before navigating to a new section. Returns `true` if navigation may + * proceed immediately; returns `false` if there are unsaved changes — in that + * case `pendingSection` is set so a confirmation dialog can be shown. + */ + requestNavigation: (section: SettingsSection) => boolean + /** Clears dirty + pending state and returns the section to navigate to. */ + confirmNavigation: () => SettingsSection | null + /** Cancels a pending navigation without clearing dirty state. */ + cancelNavigation: () => void + /** Resets all state — call on component unmount. */ + reset: () => void +} + +const initialState = { + isDirty: false, + pendingSection: null as SettingsSection | null, +} + +export const useSettingsDirtyStore = create()( + devtools( + (set, get) => ({ + ...initialState, + + setDirty: (dirty) => set({ isDirty: dirty }), + + requestNavigation: (section) => { + if (!get().isDirty) return true + set({ pendingSection: section }) + return false + }, + + confirmNavigation: () => { + const { pendingSection } = get() + set({ ...initialState }) + return pendingSection + }, + + cancelNavigation: () => set({ pendingSection: null }), + + reset: () => set({ ...initialState }), + }), + { name: 'settings-dirty-store' } + ) +) diff --git a/apps/sim/stores/settings/environment/index.ts b/apps/sim/stores/settings/environment/index.ts deleted file mode 100644 index 01e93a50d17..00000000000 --- a/apps/sim/stores/settings/environment/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { CachedWorkspaceEnvData, EnvironmentState, EnvironmentVariable } from './types' diff --git a/apps/sim/stores/settings/environment/types.ts b/apps/sim/stores/settings/environment/types.ts deleted file mode 100644 index 8dbb67caf77..00000000000 --- a/apps/sim/stores/settings/environment/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface EnvironmentVariable { - key: string - value: string -} - -export interface CachedWorkspaceEnvData { - workspace: Record - personal: Record - conflicts: string[] - cachedAt: number -} - -export interface EnvironmentState { - variables: Record - isLoading: boolean - error: string | null -} diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 534dc51797c..397309bbe2f 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -1,9 +1,9 @@ import { createLogger } from '@sim/logger' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' +import type { EnvironmentVariable } from '@/lib/environment/api' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { CustomToolDefinition } from '@/hooks/queries/custom-tools' import { environmentKeys } from '@/hooks/queries/environment' -import type { EnvironmentVariable } from '@/stores/settings/environment' import { tools } from '@/tools/registry' import type { ToolConfig } from '@/tools/types' From ec6a8e5f82725f80ae1d4c92195ad59aa566229e Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Apr 2026 21:15:45 -0700 Subject: [PATCH 2/6] fix(teams): harden Microsoft content URL validation - Add isMicrosoftContentUrl helper with typed allowlist covering SharePoint, OneDrive, and Teams CDN domains - Replace loose substring checks in Teams webhook handler with parsed-hostname matching to prevent bypass via partial domain names - Deduplicate OneDrive share-link detection into isOneDriveShareLink flag and use searchParams API instead of string splitting --- .../sim/lib/core/security/input-validation.ts | 61 +++++++++++++------ .../lib/webhooks/providers/microsoft-teams.ts | 35 ++++++----- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 52c4dde288a..2448d5c0f65 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -741,18 +741,8 @@ export function validateExternalUrl( } } - // Block suspicious ports commonly used for internal services const port = parsedUrl.port - const blockedPorts = [ - '22', // SSH - '23', // Telnet - '25', // SMTP - '3306', // MySQL - '5432', // PostgreSQL - '6379', // Redis - '27017', // MongoDB - '9200', // Elasticsearch - ] + const blockedPorts = ['22', '23', '25', '3306', '5432', '6379', '27017', '9200'] if (port && blockedPorts.includes(port)) { return { @@ -842,7 +832,6 @@ export function validateAirtableId( } } - // Airtable IDs: prefix (3 chars) + 14 alphanumeric characters = 17 chars total const airtableIdPattern = new RegExp(`^${expectedPrefix}[a-zA-Z0-9]{14}$`) if (!airtableIdPattern.test(value)) { @@ -893,11 +882,6 @@ export function validateAwsRegion( } } - // AWS region patterns: - // - Standard: af|ap|ca|eu|me|sa|us|il followed by direction and number - // - GovCloud: us-gov-east-1, us-gov-west-1 - // - China: cn-north-1, cn-northwest-1 - // - ISO: us-iso-east-1, us-iso-west-1, us-isob-east-1 const awsRegionPattern = /^(af|ap|ca|cn|eu|il|me|sa|us|us-gov|us-iso|us-isob)-(central|north|northeast|northwest|south|southeast|southwest|east|west)-\d{1,2}$/ @@ -1156,7 +1140,6 @@ export function validatePaginationCursor( } } - // Allow alphanumeric, base64 chars (+, /, =), and URL-safe chars (-, _, ., ~, %) const cursorPattern = /^[A-Za-z0-9+/=\-_.~%]+$/ if (!cursorPattern.test(value)) { logger.warn('Pagination cursor contains disallowed characters', { @@ -1224,3 +1207,45 @@ export function validateOktaDomain(rawDomain: string): string { } return domain } + +const MICROSOFT_CONTENT_SUFFIXES = [ + 'sharepoint.com', + 'sharepoint.us', + 'sharepoint.de', + 'sharepoint.cn', + 'sharepointonline.com', + 'onedrive.com', + 'onedrive.live.com', + '1drv.ms', + '1drv.com', + 'microsoftpersonalcontent.com', + 'smba.trafficmanager.net', +] as const + +/** + * Returns true if the given URL is hosted on a trusted Microsoft SharePoint or + * OneDrive domain. Validates the parsed hostname against an allowlist using exact + * match or subdomain suffix, preventing incomplete-substring bypasses. + * + * Covers SharePoint Online (commercial, GCC/GCC High/DoD, Germany, China), + * OneDrive business and consumer, OneDrive short-link and CDN domains, + * Microsoft personal content CDN, and the Azure Traffic Manager endpoint + * used for Teams inline image attachments. + * + * @see https://learn.microsoft.com/en-us/sharepoint/required-urls-and-ports + * @see https://learn.microsoft.com/en-us/microsoft-365/enterprise/microsoft-365-u-s-government-gcc-high-endpoints + * + * @param url - The URL to check + * @returns Whether the URL belongs to a trusted Microsoft content host + */ +export function isMicrosoftContentUrl(url: string): boolean { + let hostname: string + try { + hostname = new URL(url).hostname.toLowerCase() + } catch { + return false + } + return MICROSOFT_CONTENT_SUFFIXES.some( + (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`) + ) +} diff --git a/apps/sim/lib/webhooks/providers/microsoft-teams.ts b/apps/sim/lib/webhooks/providers/microsoft-teams.ts index 11af3634290..5e7a02f053a 100644 --- a/apps/sim/lib/webhooks/providers/microsoft-teams.ts +++ b/apps/sim/lib/webhooks/providers/microsoft-teams.ts @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { safeCompare } from '@/lib/core/security/encryption' +import { isMicrosoftContentUrl } from '@/lib/core/security/input-validation' import { type SecureFetchResponse, secureFetchWithPinnedIP, @@ -240,10 +241,23 @@ async function formatTeamsGraphNotification( if (!contentUrl) continue + let parsedContentUrl: URL + try { + parsedContentUrl = new URL(contentUrl) + } catch { + continue + } + const contentHost = parsedContentUrl.hostname.toLowerCase() + let buffer: Buffer | null = null let mimeType = 'application/octet-stream' - if (contentUrl.includes('sharepoint.com') || contentUrl.includes('onedrive')) { + const isOneDriveShareLink = + contentHost === '1drv.ms' || + contentHost === 'microsoftpersonalcontent.com' || + contentHost.endsWith('.microsoftpersonalcontent.com') + + if (isMicrosoftContentUrl(contentUrl) && !isOneDriveShareLink) { try { const directRes = await fetchWithDNSPinning( contentUrl, @@ -285,22 +299,15 @@ async function formatTeamsGraphNotification( } catch { continue } - } else if ( - contentUrl.includes('1drv.ms') || - contentUrl.includes('onedrive.live.com') || - contentUrl.includes('onedrive.com') || - contentUrl.includes('my.microsoftpersonalcontent.com') - ) { + } else if (isOneDriveShareLink) { try { let shareToken: string | null = null - if (contentUrl.includes('1drv.ms')) { - const urlParts = contentUrl.split('/').pop() - if (urlParts) shareToken = urlParts - } else if (contentUrl.includes('resid=')) { - const urlParams = new URL(contentUrl).searchParams - const resId = urlParams.get('resid') - if (resId) shareToken = resId + if (contentHost === '1drv.ms') { + const lastSegment = parsedContentUrl.pathname.split('/').pop() + if (lastSegment) shareToken = lastSegment + } else if (parsedContentUrl.searchParams.has('resid')) { + shareToken = parsedContentUrl.searchParams.get('resid') } if (!shareToken) { From e2ed88bce0e86f7d330c7c04a36c18bf315fbf23 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Apr 2026 21:22:33 -0700 Subject: [PATCH 3/6] fix(env): remove type re-exports from query file, drop keepPreviousData on static key --- .../components/credentials/credentials-manager.tsx | 2 +- .../components/sub-block/components/env-var-dropdown.tsx | 7 ++----- apps/sim/hooks/queries/environment.ts | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx index 1f345d114c9..d3916265d72 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx @@ -30,6 +30,7 @@ import { type PendingCredentialCreateRequest, readPendingCredentialCreateRequest, } from '@/lib/credentials/client-state' +import type { WorkspaceEnvironmentData } from '@/lib/environment/api' import { getUserColor } from '@/lib/workspaces/colors' import { isValidEnvVarName } from '@/executor/constants' import { @@ -48,7 +49,6 @@ import { useSavePersonalEnvironment, useUpsertWorkspaceEnvironment, useWorkspaceEnvironment, - type WorkspaceEnvironmentData, } from '@/hooks/queries/environment' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx index b26afd20752..49fbe2d306d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx @@ -10,11 +10,8 @@ import { } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { writePendingCredentialCreateRequest } from '@/lib/credentials/client-state' -import { - usePersonalEnvironment, - useWorkspaceEnvironment, - type WorkspaceEnvironmentData, -} from '@/hooks/queries/environment' +import type { WorkspaceEnvironmentData } from '@/lib/environment/api' +import { usePersonalEnvironment, useWorkspaceEnvironment } from '@/hooks/queries/environment' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' /** diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts index a2ecf4112a9..ebc5f2430bc 100644 --- a/apps/sim/hooks/queries/environment.ts +++ b/apps/sim/hooks/queries/environment.ts @@ -5,8 +5,6 @@ import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/envir import { workspaceCredentialKeys } from '@/hooks/queries/credentials' import { API_ENDPOINTS } from '@/stores/constants' -export type { EnvironmentVariable, WorkspaceEnvironmentData } from '@/lib/environment/api' - const logger = createLogger('EnvironmentQueries') /** @@ -25,8 +23,7 @@ export function usePersonalEnvironment() { return useQuery({ queryKey: environmentKeys.personal(), queryFn: ({ signal }) => fetchPersonalEnvironment(signal), - staleTime: 60 * 1000, // 1 minute - placeholderData: keepPreviousData, + staleTime: 60 * 1000, }) } From 263744d2cd45b4ea1f53749815c3034d421fbec4 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Apr 2026 21:23:08 -0700 Subject: [PATCH 4/6] fix(teams): remove smba.trafficmanager.net from Microsoft content allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subdomain check for smba.trafficmanager.net was unnecessary — Azure Traffic Manager does not support nested subdomains of existing profiles, but the pattern still raised a valid audit concern. Teams bot-framework attachment URLs from this host fall through to the generic fetchWithDNSPinning branch, which provides the same protection without the ambiguity. --- .../sidebar/components/settings-sidebar/settings-sidebar.tsx | 1 - apps/sim/hooks/queries/environment.ts | 3 +-- apps/sim/lib/core/security/input-validation.ts | 4 +--- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index 3e5ea1afc27..ac56fc455d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -210,7 +210,6 @@ export function SettingsSidebar({ if (section) { router.replace(getSettingsHref({ section }), { scroll: false }) } else { - // Triggered by the back button — no pending section was set router.push(popSettingsReturnUrl(`/workspace/${workspaceId}/home`)) } }, [confirmNavigation, router, getSettingsHref, popSettingsReturnUrl, workspaceId]) diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts index ebc5f2430bc..98a955a6771 100644 --- a/apps/sim/hooks/queries/environment.ts +++ b/apps/sim/hooks/queries/environment.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { EnvironmentVariable, WorkspaceEnvironmentData } from '@/lib/environment/api' import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api' import { workspaceCredentialKeys } from '@/hooks/queries/credentials' @@ -39,7 +39,6 @@ export function useWorkspaceEnvironment( queryFn: ({ signal }) => fetchWorkspaceEnvironment(workspaceId, signal), enabled: !!workspaceId, staleTime: 60 * 1000, // 1 minute - placeholderData: keepPreviousData, ...options, }) } diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 2448d5c0f65..2c8e401bf84 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1219,7 +1219,6 @@ const MICROSOFT_CONTENT_SUFFIXES = [ '1drv.ms', '1drv.com', 'microsoftpersonalcontent.com', - 'smba.trafficmanager.net', ] as const /** @@ -1229,8 +1228,7 @@ const MICROSOFT_CONTENT_SUFFIXES = [ * * Covers SharePoint Online (commercial, GCC/GCC High/DoD, Germany, China), * OneDrive business and consumer, OneDrive short-link and CDN domains, - * Microsoft personal content CDN, and the Azure Traffic Manager endpoint - * used for Teams inline image attachments. + * and Microsoft personal content CDN. * * @see https://learn.microsoft.com/en-us/sharepoint/required-urls-and-ports * @see https://learn.microsoft.com/en-us/microsoft-365/enterprise/microsoft-365-u-s-government-gcc-high-endpoints From ee9812cf06713eab4daab42a96518de4d4a42d29 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Apr 2026 21:34:52 -0700 Subject: [PATCH 5/6] fix(secrets): guard active-tab re-click, restore keepPreviousData on workspace env query --- .../sidebar/components/settings-sidebar/settings-sidebar.tsx | 1 + apps/sim/hooks/queries/environment.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index ac56fc455d8..ccb7cba760b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -324,6 +324,7 @@ export function SettingsSidebar({ onFocus={() => handlePrefetch(item.id)} onClick={() => { const section = item.id as SettingsSection + if (section === activeSection) return if (!requestNavigation(section)) { setShowDiscardDialog(true) return diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts index 98a955a6771..ebc5f2430bc 100644 --- a/apps/sim/hooks/queries/environment.ts +++ b/apps/sim/hooks/queries/environment.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { EnvironmentVariable, WorkspaceEnvironmentData } from '@/lib/environment/api' import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api' import { workspaceCredentialKeys } from '@/hooks/queries/credentials' @@ -39,6 +39,7 @@ export function useWorkspaceEnvironment( queryFn: ({ signal }) => fetchWorkspaceEnvironment(workspaceId, signal), enabled: !!workspaceId, staleTime: 60 * 1000, // 1 minute + placeholderData: keepPreviousData, ...options, }) } From 522f3e2e7596740b134d8a0f4af3de01599b0c10 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Apr 2026 21:51:27 -0700 Subject: [PATCH 6/6] fix(teams): add 1drv.com apex to OneDrive share-link branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1drv.com (apex) is a short-link domain functionally equivalent to 1drv.ms and requires share-token resolution, not direct fetch. CDN subdomains (files.1drv.com) are unaffected — the exact-match check leaves them on the direct-fetch path. --- apps/sim/lib/webhooks/providers/microsoft-teams.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/lib/webhooks/providers/microsoft-teams.ts b/apps/sim/lib/webhooks/providers/microsoft-teams.ts index 5e7a02f053a..62952d94c32 100644 --- a/apps/sim/lib/webhooks/providers/microsoft-teams.ts +++ b/apps/sim/lib/webhooks/providers/microsoft-teams.ts @@ -254,6 +254,7 @@ async function formatTeamsGraphNotification( const isOneDriveShareLink = contentHost === '1drv.ms' || + contentHost === '1drv.com' || contentHost === 'microsoftpersonalcontent.com' || contentHost.endsWith('.microsoftpersonalcontent.com')