Skip to content

Commit b4201fc

Browse files
committed
refactor(knowledge): tighten the per-member access surface
- The credential-group policy canonicaliser carries knowledge-connector statements through instead of rejecting a third statement - Google Docs and Box report an unreachable scope so a member's access is withdrawn rather than retried forever; Notion leaves members mode because its page access is granted to the shared integration bot - Provisioning reuses a Credential Group only when other members-mode connectors already sync through it, never a group curated for something else; the fast dispatch on connect accepts the same statuses as the sweep - Enrollment opens its tab inside the click so popup blockers cannot swallow it, and the membership poll stops after ten minutes - Session-only routes use session auth; members mode stores the cap-stripped config; one liveness rule for members and managed bindings - Drop dead code: the access barrel, system.ts and rank.ts folded into their siblings, unused ACL helpers, the unread change_cursor_at column, stale documentation, and the engine/provisioning/queue import cycle
1 parent 4724422 commit b4201fc

54 files changed

Lines changed: 378 additions & 425 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/access/route.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import { updateKnowledgeConnectorAccessContract } from '@/lib/api/contracts/knowledge'
2-
import { defineInternalJsonRoute, internalRateLimits } from '@/lib/api/server/routes'
2+
import {
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
37
import {
48
resolveInternalKnowledgeBillingAttribution,
59
toInternalKnowledgeConnector,
610
} from '@/lib/knowledge/api/internal-route'
7-
import {
8-
internalKnowledgeErrorPolicies,
9-
internalKnowledgeSessionOrExecutorAuth,
10-
} from '@/lib/knowledge/api/route-policies'
11+
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
1112
import { updateKnowledgeConnectorAccess } from '@/lib/knowledge/application/connector-access'
1213
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
1314

1415
export const PATCH = defineInternalJsonRoute({
1516
contract: updateKnowledgeConnectorAccessContract,
16-
auth: internalKnowledgeSessionOrExecutorAuth,
17+
auth: internalSessionAuth,
1718
operation: knowledgeOperations.updateConnectorAccess,
1819
rateLimit: internalRateLimits.none({
1920
reason: 'A settings action an admin performs by hand; the switch itself is bounded',

apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/enroll/route.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { startKnowledgeConnectorMemberEnrollmentContract } from '@/lib/api/contracts/knowledge'
2-
import { defineInternalJsonRoute, internalRateLimits } from '@/lib/api/server/routes'
32
import {
4-
internalKnowledgeErrorPolicies,
5-
internalKnowledgeSessionOrExecutorAuth,
6-
} from '@/lib/knowledge/api/route-policies'
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
7+
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
78
import { startKnowledgeConnectorMemberEnrollment } from '@/lib/knowledge/application/connector-access'
89
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
910

1011
export const POST = defineInternalJsonRoute({
1112
contract: startKnowledgeConnectorMemberEnrollmentContract,
12-
auth: internalKnowledgeSessionOrExecutorAuth,
13+
auth: internalSessionAuth,
1314
operation: knowledgeOperations.enrollConnectorMember,
1415
rateLimit: internalRateLimits.none({
1516
reason:

apps/sim/app/api/knowledge/search/utils.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@ afterEach(() => {
4848
})
4949

5050
import { WORKSPACE_ACCESS_SCOPE } from '@/lib/knowledge/access/scope'
51+
import { generateSearchEmbedding } from '@/lib/knowledge/embeddings'
5152
import {
5253
executeKeywordSearch,
5354
executeKnowledgeSearch,
5455
fuseByReciprocalRank,
55-
generateSearchEmbedding,
5656
getQueryStrategy,
5757
handleTagAndVectorSearch,
5858
handleTagOnlySearch,
5959
handleVectorOnlySearch,
6060
type SearchResult,
6161
} from '@/lib/knowledge/search/queries'
62-
import { RRF_K } from '@/lib/knowledge/search/rank'
62+
import { RRF_K } from '@/lib/knowledge/search/recency'
6363

6464
/** Minimal SearchResult builder — only the fields fusion and ordering read. */
6565
function makeResult(id: string, distance = 0.1): SearchResult {

apps/sim/app/api/knowledge/utils.ts

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -18,89 +18,6 @@ interface KnowledgeBaseData {
1818
updatedAt: Date
1919
}
2020

21-
interface DocumentData {
22-
id: string
23-
knowledgeBaseId: string
24-
filename: string
25-
fileUrl: string
26-
fileSize: number
27-
mimeType: string
28-
chunkCount: number
29-
tokenCount: number
30-
characterCount: number
31-
processingStatus: string
32-
processingStartedAt?: Date | null
33-
processingCompletedAt?: Date | null
34-
processingError?: string | null
35-
enabled: boolean
36-
deletedAt?: Date | null
37-
uploadedAt: Date
38-
// Text tags
39-
tag1?: string | null
40-
tag2?: string | null
41-
tag3?: string | null
42-
tag4?: string | null
43-
tag5?: string | null
44-
tag6?: string | null
45-
tag7?: string | null
46-
// Number tags (5 slots)
47-
number1?: number | null
48-
number2?: number | null
49-
number3?: number | null
50-
number4?: number | null
51-
number5?: number | null
52-
// Date tags (2 slots)
53-
date1?: Date | null
54-
date2?: Date | null
55-
// Boolean tags (3 slots)
56-
boolean1?: boolean | null
57-
boolean2?: boolean | null
58-
boolean3?: boolean | null
59-
// Connector fields
60-
connectorId?: string | null
61-
sourceUrl?: string | null
62-
externalId?: string | null
63-
}
64-
65-
interface EmbeddingData {
66-
id: string
67-
knowledgeBaseId: string
68-
documentId: string
69-
chunkIndex: number
70-
chunkHash: string
71-
content: string
72-
contentLength: number
73-
tokenCount: number
74-
embedding?: number[] | null
75-
embeddingModel: string
76-
startOffset: number
77-
endOffset: number
78-
// Text tags
79-
tag1?: string | null
80-
tag2?: string | null
81-
tag3?: string | null
82-
tag4?: string | null
83-
tag5?: string | null
84-
tag6?: string | null
85-
tag7?: string | null
86-
// Number tags (5 slots)
87-
number1?: number | null
88-
number2?: number | null
89-
number3?: number | null
90-
number4?: number | null
91-
number5?: number | null
92-
// Date tags (2 slots)
93-
date1?: Date | null
94-
date2?: Date | null
95-
// Boolean tags (3 slots)
96-
boolean1?: boolean | null
97-
boolean2?: boolean | null
98-
boolean3?: boolean | null
99-
enabled: boolean
100-
createdAt: Date
101-
updatedAt: Date
102-
}
103-
10421
export interface KnowledgeBaseAccessResult {
10522
hasAccess: true
10623
knowledgeBase: Pick<

apps/sim/app/api/v1/knowledge/search/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ const SYSTEM_BILLING_ATTRIBUTION = {
4646

4747
vi.mock('@/lib/knowledge/search/queries', () => ({
4848
executeKnowledgeSearch: mockExecuteKnowledgeSearch,
49-
generateSearchEmbedding: mockGenerateSearchEmbedding,
5049
getDocumentMetadataByIds: mockGetDocumentMetadataByIds,
5150
}))
5251

@@ -63,6 +62,7 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({
6362
}))
6463

6564
vi.mock('@/lib/knowledge/embeddings', () => ({
65+
generateSearchEmbedding: mockGenerateSearchEmbedding,
6666
recordSearchEmbeddingUsage: mockRecordSearchEmbeddingUsage,
6767
}))
6868

apps/sim/app/api/v1/knowledge/search/route.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ import {
88
} from '@/lib/billing/core/billing-attribution'
99
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1010
import { ALL_TAG_SLOTS } from '@/lib/knowledge/constants'
11-
import { recordSearchEmbeddingUsage } from '@/lib/knowledge/embeddings'
11+
import { generateSearchEmbedding, recordSearchEmbeddingUsage } from '@/lib/knowledge/embeddings'
1212
import { resolveKnowledgeSearchDefaults } from '@/lib/knowledge/search/defaults'
1313
import {
1414
executeKnowledgeSearch,
15-
generateSearchEmbedding,
1615
getDocumentMetadataByIds,
1716
type SearchResult,
1817
} from '@/lib/knowledge/search/queries'

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function ConnectorAccessField({
6262
if (value.accessMode !== 'members') return null
6363
return (
6464
<ChipModalField type='custom' title='Access'>
65-
<ButtonGroup value='members' onValueChange={() => undefined}>
65+
<ButtonGroup value='members'>
6666
<ButtonGroupItem value='workspace' disabled>
6767
Workspace
6868
</ButtonGroupItem>

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
CONNECTOR_SYNC_STALE_LOCK_TTL_MS,
3636
MEMBER_SYNC_STALE_LOCK_TTL_MS,
3737
} from '@/lib/knowledge/connectors/sync-limits'
38+
import type { MemberSyncStatus } from '@/lib/knowledge/types'
3839
import { getCanonicalScopesForProvider, getProviderIdFromServiceId } from '@/lib/oauth'
3940
import { getMissingRequiredScopes } from '@/lib/oauth/utils'
4041
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
@@ -99,7 +100,7 @@ const MEMBER_SYNC_STATUS_AS_CONNECTOR_STATUS = {
99100
running: 'syncing',
100101
error: 'error',
101102
disabled: 'disabled',
102-
} as const satisfies Record<string, keyof typeof STATUS_CONFIG>
103+
} as const satisfies Record<MemberSyncStatus, keyof typeof STATUS_CONFIG>
103104

104105
const CONNECTOR_ACTION_BUTTON_CLASSES =
105106
'size-7 rounded-lg p-0 text-[var(--text-muted)] hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
@@ -316,9 +317,7 @@ function ConnectorCard({
316317
*/
317318
const effectiveStatus =
318319
connector.accessMode === 'members' && connector.status === 'active'
319-
? (MEMBER_SYNC_STATUS_AS_CONNECTOR_STATUS[
320-
connector.memberSyncStatus as keyof typeof MEMBER_SYNC_STATUS_AS_CONNECTOR_STATUS
321-
] ?? 'active')
320+
? MEMBER_SYNC_STATUS_AS_CONNECTOR_STATUS[connector.memberSyncStatus]
322321
: connector.status
323322
const statusConfig =
324323
STATUS_CONFIG[effectiveStatus as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.active

apps/sim/app/workspace/[workspaceId]/search/components/member-connectors-section/member-connectors-section.tsx

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -78,42 +78,44 @@ export function MemberConnectorsSection({ workspaceId, search }: MemberConnector
7878
if (visible.length === 0) return null
7979

8080
return (
81-
<IntegrationSection label={SHARED_WITH_YOU_LABEL}>
82-
{visible.map((connector) => {
83-
const meta = CONNECTOR_META_REGISTRY[connector.connectorType]
84-
const waiting = isAwaiting(connector.connectorId)
85-
const connectable = CONNECTABLE.has(connector.viewerMembership)
86-
return (
87-
<SettingsResourceRow
88-
key={connector.connectorId}
89-
iconVariant='custom'
90-
icon={
91-
meta ? (
92-
<IntegrationTile blockType={connector.connectorType} icon={meta.icon} />
93-
) : undefined
94-
}
95-
title={meta?.name ?? connector.connectorType}
96-
description={describe(connector, waiting)}
97-
trailing={
98-
connectable ? (
99-
<Button
100-
variant='primary'
101-
size='sm'
102-
onClick={() => connect(connector.knowledgeBaseId, connector.connectorId)}
103-
disabled={isPending}
104-
>
105-
{waiting
106-
? 'Open again'
107-
: connector.viewerMembership === 'needs_reauth'
108-
? 'Reconnect'
109-
: 'Connect'}
110-
</Button>
111-
) : undefined
112-
}
113-
/>
114-
)
115-
})}
81+
<>
82+
<IntegrationSection label={SHARED_WITH_YOU_LABEL}>
83+
{visible.map((connector) => {
84+
const meta = CONNECTOR_META_REGISTRY[connector.connectorType]
85+
const waiting = isAwaiting(connector.connectorId)
86+
const connectable = CONNECTABLE.has(connector.viewerMembership)
87+
return (
88+
<SettingsResourceRow
89+
key={connector.connectorId}
90+
iconVariant='custom'
91+
icon={
92+
meta ? (
93+
<IntegrationTile blockType={connector.connectorType} icon={meta.icon} />
94+
) : undefined
95+
}
96+
title={meta?.name ?? connector.connectorType}
97+
description={describe(connector, waiting)}
98+
trailing={
99+
connectable ? (
100+
<Button
101+
variant='primary'
102+
size='sm'
103+
onClick={() => connect(connector.knowledgeBaseId, connector.connectorId)}
104+
disabled={isPending}
105+
>
106+
{waiting
107+
? 'Open again'
108+
: connector.viewerMembership === 'needs_reauth'
109+
? 'Reconnect'
110+
: 'Connect'}
111+
</Button>
112+
) : undefined
113+
}
114+
/>
115+
)
116+
})}
117+
</IntegrationSection>
116118
{error && <p className='text-[var(--text-error)] text-caption'>{error}</p>}
117-
</IntegrationSection>
119+
</>
118120
)
119121
}

apps/sim/app/workspace/[workspaceId]/search/search.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ vi.mock('@/hooks/queries/credentials', () => ({
107107
}),
108108
}))
109109

110+
vi.mock('@/hooks/queries/kb/connectors', () => ({
111+
memberConnectorKeys: { list: (workspaceId?: string) => ['member-connectors', workspaceId] },
112+
useWorkspaceMemberConnectors: () => ({ data: [] }),
113+
}))
114+
vi.mock('@/hooks/use-member-enrollment', () => ({
115+
useMemberEnrollment: () => ({
116+
connect: vi.fn(),
117+
isAwaiting: () => false,
118+
isPending: false,
119+
error: null,
120+
}),
121+
}))
122+
110123
import { Search } from '@/app/workspace/[workspaceId]/search/search'
111124

112125
let root: Root | null = null

0 commit comments

Comments
 (0)