Skip to content

Commit 76c37b2

Browse files
committed
fix(release): harden Entra sync against mid-read drift, keep loaded People views on refresh errors, refuse credentialed citation URLs
1 parent 6ff4f32 commit 76c37b2

17 files changed

Lines changed: 60 additions & 65 deletions

File tree

‎apps/sim/app/o/[organizationId]/people/[memberId]/person.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export function Person({ organizationId, memberId }: PersonProps) {
100100
const [editing, setEditing] = useState(false)
101101
const routes = organizationRoutes(organizationId)
102102
const originQuery = serializePeopleView({ ...origin, view: origin.view ?? 'directory' })
103-
const data = query.isError ? undefined : query.data
103+
const notFound = isApiClientError(query.error) && query.error.status === 404
104+
// A failed background refresh keeps the loaded profile, and any edit open on it.
105+
const loadError = query.data && !notFound ? null : query.error
106+
const data = loadError ? undefined : query.data
104107

105108
const chartHref = serializePeopleView(routes.people, {
106109
...origin,
@@ -129,14 +132,14 @@ export function Person({ organizationId, memberId }: PersonProps) {
129132
) : undefined
130133
}
131134
>
132-
{query.error ? (
133-
isApiClientError(query.error) && query.error.status === 404 ? (
135+
{loadError ? (
136+
notFound ? (
134137
<SettingsEmptyState variant='inline'>
135138
This person isn't in your organization.
136139
</SettingsEmptyState>
137140
) : (
138141
<SettingsQueryErrorState
139-
error={query.error}
142+
error={loadError}
140143
fallback='Could not load this person'
141144
isRetrying={query.isFetching}
142145
onRetry={() => void query.refetch()}

‎apps/sim/app/o/[organizationId]/people/people-chart.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ export function PeopleChart({ organizationId, viewSwitch, onImport, onSync }: Pe
102102
const { focus, page } = origin
103103
const size = useIsMobile() ? 1 : 4
104104
const chart = usePeopleChart(organizationId, focus, Math.max(1, Math.min(page, 1_000_000)), size)
105-
const branch = chart.isError ? undefined : chart.data
105+
const notFound = isApiClientError(chart.error) && chart.error.status === 404
106+
// A failed background refresh keeps the chart on screen; only a missing person replaces it.
107+
const branch = notFound ? undefined : chart.data
106108
const profileQuery = serializePeopleView({
107109
...origin,
108110
view: 'chart',
@@ -264,7 +266,7 @@ export function PeopleChart({ organizationId, viewSwitch, onImport, onSync }: Pe
264266
</div>
265267
</>
266268
) : chart.error ? (
267-
isApiClientError(chart.error) && chart.error.status === 404 ? (
269+
notFound ? (
268270
<EmptyState
269271
title='Person unavailable'
270272
description='This person is no longer in your organization’s directory.'

‎apps/sim/app/o/[organizationId]/people/people-directory.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function PeopleDirectory({ organizationId }: PeopleProps) {
190190
rows={rows}
191191
onRowClick={(memberId) => router.push(profileHref(memberId))}
192192
onLoadMore={() => void people.fetchNextPage()}
193-
hasMore={people.hasNextPage}
193+
hasMore={people.hasNextPage && !changingFilter}
194194
isLoadingMore={people.isFetchingNextPage}
195195
isFetching={people.isFetching}
196196
loadMoreFailed={people.isFetchNextPageError}

‎apps/sim/app/o/[organizationId]/search/search.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ function OrganizationSearchContent({ userId, chatId }: OrganizationSearchProps)
167167
const { recordQuery } = useSearchHistoryActions()
168168
const router = useRouter()
169169
const pathname = usePathname()
170-
const hasSearchParams = useSearchParams().size > 0
170+
const hasSearchParams = useSearchParams().toString() !== ''
171171
const routes = organizationRoutes(organization.id)
172172
const hasConversation = Boolean(chatId) || pathname.startsWith(`${routes.search}/chat/`)
173173
const queryClient = useQueryClient()
@@ -187,11 +187,7 @@ function OrganizationSearchContent({ userId, chatId }: OrganizationSearchProps)
187187
[]
188188
)
189189
useEffect(() => {
190-
if (!mothershipAvailable) return
191-
if (routeMode) {
192-
BrowserStorage.setItem(preferenceKey, routeMode)
193-
return
194-
}
190+
if (!mothershipAvailable || routeMode) return
195191
const savedMode = BrowserStorage.getItem<unknown>(preferenceKey, null)
196192
// Pin the mode to this history entry so Back survives a later preference change.
197193
void setParams({ mode: savedMode === 'assistant' ? 'assistant' : 'search' })
@@ -200,6 +196,7 @@ function OrganizationSearchContent({ userId, chatId }: OrganizationSearchProps)
200196
if (activeMode === 'assistant' && !assistantOpened) setAssistantOpened(true)
201197

202198
const changeMode = (next: 'search' | 'assistant') => {
199+
BrowserStorage.setItem(preferenceKey, next)
203200
void setParams({ mode: next }, { history: 'push' })
204201
}
205202
const startNew = () => {

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/resolve-citations.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ describe('evidence-linked citations', () => {
160160
true
161161
)
162162
expect(persisted.sources).toEqual(live.sources)
163+
const disguised = {
164+
...webOutput,
165+
results: [{ ...webOutput.results[0], url: 'https://docs.example.test@attacker.test/cli' }],
166+
}
167+
expect(
168+
resolveMessageCitations(webBlocks('success', true, disguised), '', true).sources
169+
).toEqual([])
163170
expect(
164171
resolveMessageCitations(webBlocks('error', false, webOutput), '', true).blocks[1].content
165172
).toBe('Install it ')

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,7 @@ export function useWorkflowExecution() {
10761076
selectedOutputs = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
10771077
}
10781078

1079-
let startBlockId: string | undefined
1079+
let startBlockId: string
10801080
let finalWorkflowInput = workflowInput
10811081

10821082
if (isExecutingFromChat) {
@@ -1128,18 +1128,6 @@ export function useWorkflowExecution() {
11281128
}
11291129
}
11301130

1131-
if (!startBlockId) {
1132-
const error = new WorkflowValidationError(
1133-
'No valid trigger block found to start execution',
1134-
'validation',
1135-
'validation',
1136-
'Workflow Validation'
1137-
)
1138-
logger.error('No startBlockId found after trigger search')
1139-
if (activeWorkflowId) finishOwnedExecution(activeWorkflowId, persistenceExecution)
1140-
throw error
1141-
}
1142-
11431131
logger.info('Final execution setup:', {
11441132
startBlockId,
11451133
isExecutingFromChat,

‎apps/sim/lib/mcp/utils.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export function categorizeMcpServerError(
189189
}
190190

191191
/**
192-
* Enhanced error categorization for more specific HTTP status codes.
193-
* Returns safe, generic messages to prevent leaking internal details.
192+
* HTTP status and safe, generic message for any error: Sim's own MCP error classes
193+
* first, then message matching for SDK and third-party errors.
194194
*/
195195
export function categorizeError(error: unknown): { message: string; status: number } {
196196
if (!(error instanceof Error)) {

‎apps/sim/lib/mothership/chat/citation-evidence.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ type RetrievalCitationSource = {
1414
snippet?: string
1515
}
1616

17-
/** Tools whose successful results supply sources: connected-source retrieval and public web research. */
1817
const WEB_TOOLS = new Set(['web_search', 'web_fetch'])
18+
/** Tools whose successful results supply sources: connected-source retrieval and public web research. */
1919
const CITATION_TOOLS = new Set(['search_workspace', 'read_document', ...WEB_TOOLS])
2020

2121
/** Whether a successful result of this tool may supply source destinations. */
@@ -80,7 +80,9 @@ export function collectRetrievalCitationEvidence(blocks: readonly RetrievalCitat
8080
continue
8181
try {
8282
const url = new URL(result.citationUrl)
83+
// A web page can steer a fetched URL, so `https://trusted.example@other.example` never cites.
8384
if (url.protocol !== 'https:' && url.protocol !== 'http:') continue
85+
if (url.username || url.password) continue
8486
} catch {
8587
continue
8688
}

‎apps/sim/lib/people/sync/entra.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ async function readReportPages(
101101
const timing = { get: (name: string) => headers.get(name) ?? null }
102102
const retryAfterMs = directoryRetryDelay(response.status, timing)
103103
if (retryAfterMs !== null) return { managerId: page.managerId, retryAfterMs }
104+
// A user deleted after the users phase has no reports left to read.
105+
if (response.status === 404) return { managerId: page.managerId, reportIds: [], next: null }
104106
requireDirectorySuccess(response.status, timing)
105107
const parsedReports = reportsResponse.safeParse(response.body)
106108
if (!parsedReports.success)

‎apps/sim/lib/people/sync/large-directory.integration.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ it('rotates credentials without bypassing a provider cooldown or publishing the
312312
expect(rows).toEqual([{ id: director, title: 'President' }])
313313
})
314314

315-
it('resumes a large Entra hierarchy without replaying successful batch siblings or creating accounts', async () => {
315+
it('resumes a large Entra hierarchy past a user deleted mid-read without replaying successful batch siblings or creating accounts', async () => {
316316
const total = 10007
317317
const tenantId = generateId()
318318
const graphConfig = {
@@ -378,6 +378,12 @@ it('resumes a large Entra hierarchy without replaying successful batch siblings
378378
body: { error: graphConfig.clientSecret },
379379
}
380380
}
381+
if (managerId === employee(2))
382+
return {
383+
id: request.id,
384+
status: 404,
385+
body: { error: { code: 'Request_ResourceNotFound' } },
386+
}
381387
if (managerId !== lead) return { id: request.id, status: 200, body: { value: [] } }
382388
const offset = Number(target.searchParams.get('$skiptoken') || 0)
383389
const length = Math.min(1000, total - 1 - offset)

0 commit comments

Comments
 (0)