Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
2604f08
feat(search): add Sim Search mode, connectors tab, and cited sources
emir-karabeg Sep 2, 2026
3c319f0
fix(search): harden source parsing, availability, and provider matching
emir-karabeg Sep 2, 2026
cf5873f
feat(knowledge): add document ACL schema and members-mode tables
waleedlatif1 Sep 2, 2026
70beb00
feat(knowledge): add access scope, token, and predicate libraries
waleedlatif1 Sep 2, 2026
ee8958b
feat(knowledge): enforce document access scope on every read path
waleedlatif1 Sep 2, 2026
92766f5
refactor(knowledge): extract connector sync primitives from the sync …
waleedlatif1 Sep 2, 2026
2efec76
feat(knowledge): plumb credential groups into knowledge connectors
waleedlatif1 Sep 2, 2026
c7bf992
feat(knowledge): add the members-mode sync engine
waleedlatif1 Sep 2, 2026
22d243e
feat(knowledge): let connectors enter members mode
waleedlatif1 Sep 2, 2026
740f581
feat(knowledge): per-member access controls in connector settings
waleedlatif1 Sep 2, 2026
361fe54
feat(knowledge): note member-scoped retrieval on the knowledge block
waleedlatif1 Sep 2, 2026
b28af49
feat(knowledge): per-member change feeds for members-mode connectors
waleedlatif1 Sep 2, 2026
36b05a8
feat(knowledge): hybrid retrieval by default with a source-recency boost
waleedlatif1 Sep 2, 2026
b530701
feat(knowledge): gate permission-aware knowledge behind a flag and ha…
waleedlatif1 Sep 2, 2026
65df840
test(knowledge): cover retrieval default resolution behind the flag
waleedlatif1 Sep 2, 2026
a6dda5c
fix(knowledge): judge members mode by the workspace alone and fail cl…
waleedlatif1 Sep 2, 2026
10c3998
feat(knowledge): provision per-member access in the background
waleedlatif1 Sep 2, 2026
64c3cc7
fix(knowledge): make per-member access self-serve end to end
waleedlatif1 Sep 2, 2026
83eb6e2
feat(knowledge): list a workspace's per-member connectors for the viewer
waleedlatif1 Sep 2, 2026
d790074
merge: staging into feat/permission-aware-knowledge
waleedlatif1 Sep 2, 2026
27f03fe
merge: feat/search (#7376) into feat/permission-aware-knowledge
waleedlatif1 Sep 2, 2026
37db0a2
feat(search): show per-member knowledge connectors on the Search tab
waleedlatif1 Sep 2, 2026
4724422
feat(knowledge): enable per-member access on every permission-scoped …
waleedlatif1 Sep 2, 2026
b4201fc
refactor(knowledge): tighten the per-member access surface
waleedlatif1 Sep 2, 2026
1610baf
fix(knowledge): repair CI and apply cleanup passes
waleedlatif1 Sep 2, 2026
b930ead
fix(knowledge): apply the audit swarm's findings
waleedlatif1 Sep 2, 2026
1628ba9
merge: staging into feat/permission-aware-knowledge
waleedlatif1 Sep 2, 2026
88fbe59
feat(knowledge): search results a person can open, from Chat
waleedlatif1 Sep 2, 2026
caabe9b
fix(knowledge): land member observations and ACLs only under the lease
waleedlatif1 Sep 2, 2026
9ea67ba
fix(knowledge): disable a member sync only under the lease
waleedlatif1 Sep 2, 2026
087d6d2
fix(knowledge): record a member failure only under the lease
waleedlatif1 Sep 2, 2026
9549f17
fix(knowledge): tombstone and resurrect only under the lease
waleedlatif1 Sep 2, 2026
b7f638b
fix(knowledge): prove the lease before a batch's document writes
waleedlatif1 Sep 2, 2026
75df3e5
feat(knowledge): one click connects a Sim Search source and indexes i…
waleedlatif1 Sep 2, 2026
ea8563f
feat(knowledge): the composer's Search mode shows every source and re…
waleedlatif1 Sep 2, 2026
dba3510
fix(knowledge): process connector-owned documents as the system
waleedlatif1 Sep 2, 2026
662119a
feat(search): connect every personal source in place
waleedlatif1 Sep 2, 2026
ff6ffb5
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
3bbcaa3
feat(search): keep the results familiar to a search page
waleedlatif1 Sep 2, 2026
686236a
feat(knowledge): sync Google Chat, Meet, Sheets, Bitbucket, and Airta…
waleedlatif1 Sep 2, 2026
25c58b6
Merge branch 'worktree-agent-a8731ffa2f354a3a9' into feat/permission-…
waleedlatif1 Sep 2, 2026
1fc7efc
feat(knowledge): sync Microsoft connectors per member
waleedlatif1 Sep 2, 2026
7a2b6d5
fix(knowledge): close the connector access-switch and members-mode up…
waleedlatif1 Sep 2, 2026
afea91e
Merge branch 'worktree-agent-a32fccb34dd98368c' into feat/permission-…
waleedlatif1 Sep 2, 2026
fe363cf
Merge branch 'worktree-agent-a785aaacb6123a93c' into feat/permission-…
waleedlatif1 Sep 2, 2026
9624c01
fix(knowledge): close member-sync races found in review
waleedlatif1 Sep 2, 2026
e747589
Merge branch 'worktree-agent-aab7b4df909f66238' into feat/permission-…
waleedlatif1 Sep 2, 2026
884681b
fix(knowledge): resolve Sim Search review findings
waleedlatif1 Sep 2, 2026
8f0e872
Merge branch 'worktree-agent-a6f2c5ca969d7750e' into feat/permission-…
waleedlatif1 Sep 2, 2026
37ec269
fix(connectors): withdraw member access on unreachable scopes and pag…
waleedlatif1 Sep 2, 2026
84123ac
feat(search): make a search a shareable link
waleedlatif1 Sep 2, 2026
d64b302
Merge branch 'worktree-agent-af6c283d34b3ff98c' into feat/permission-…
waleedlatif1 Sep 2, 2026
a94cdef
test(knowledge): pin member-access availability in the v1 search rout…
waleedlatif1 Sep 2, 2026
6b621bd
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
d6968e4
refactor(search): apply the cleanup passes to the Sim Search surface
waleedlatif1 Sep 2, 2026
540bda1
refactor(search): apply the simplify passes to the Sim Search surface
waleedlatif1 Sep 2, 2026
8dabb98
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
0c5a030
fix(connectors): keep per-member listings complete across partial sco…
waleedlatif1 Sep 2, 2026
240e321
fix(knowledge): address review threads on search surface, lease-guard…
waleedlatif1 Sep 2, 2026
f7a44a1
Merge branch 'worktree-agent-a703bff345dcfce4d' into feat/permission-…
waleedlatif1 Sep 2, 2026
de8c070
fix(connectors): validate Gmail's thread cap with the sync parser and…
waleedlatif1 Sep 2, 2026
209f328
fix(knowledge): prove the lease inside every ACL rewrite batch and ha…
waleedlatif1 Sep 2, 2026
dde4920
fix(knowledge): claim a member only under a proved lease and read ema…
waleedlatif1 Sep 2, 2026
fff2aa6
feat(search): show the matching passage and the author on each result
waleedlatif1 Sep 2, 2026
cedf424
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
4d88736
refactor(search): style the results and sources as one surface with t…
waleedlatif1 Sep 2, 2026
4b42c2c
fix(search): safe result links, script-aware term matching, and hones…
waleedlatif1 Sep 2, 2026
3a15ed2
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
67f068a
fix(search): no cached member rows with access off, no double enrollm…
waleedlatif1 Sep 2, 2026
e15d551
fix(knowledge): create a Sim Search source without holding a transact…
waleedlatif1 Sep 2, 2026
fe856cb
fix(search): admin-gated first connect on the surfaces, live indexing…
waleedlatif1 Sep 2, 2026
250181b
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
bf3ac25
improvement(routes): log the deepest cause's stack outside production
waleedlatif1 Sep 2, 2026
b520971
feat(home): add Ask mode, an agent turn grounded in the searched sources
waleedlatif1 Sep 2, 2026
1c03b3a
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
b7f5f1b
fix(home): keep the composer on the live search query, not a mount-ti…
waleedlatif1 Sep 2, 2026
d2432b8
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
e9a9e48
feat(home): make Ask turns answer from the searched sources with the …
waleedlatif1 Sep 2, 2026
63f0159
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
4437b39
improvement(chat): name the query in the knowledge search row and lis…
waleedlatif1 Sep 2, 2026
86427f8
fix(home): carry the Ask request mode through every send lane and ref…
waleedlatif1 Sep 2, 2026
c7a676b
improvement(chat): put a reply's sources behind a counted button in t…
waleedlatif1 Sep 2, 2026
108356d
feat(chat): let the agent use the person's own Credential Group crede…
waleedlatif1 Sep 2, 2026
961eb78
fix(knowledge): treat a reclaimed lease during a member purge as supe…
waleedlatif1 Sep 2, 2026
42edb67
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
f52b352
improvement(home): fold Ask into Search behind an Answer toggle
waleedlatif1 Sep 2, 2026
09775ce
fix(credentials): prove a Chat managed-credential use only from a rea…
waleedlatif1 Sep 2, 2026
4740899
improvement(home): name both choices in Sources mode with a Search / …
waleedlatif1 Sep 2, 2026
d0744a8
improvement(home): make Assistant a peer of Build and Search in the m…
waleedlatif1 Sep 2, 2026
af2557b
improvement(home): keep the composer mode in the URL and drop the mod…
waleedlatif1 Sep 2, 2026
b9e2986
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
cb68ed3
fix(home): let Search be chosen inside a chat now that the mode lives…
waleedlatif1 Sep 2, 2026
d54596a
fix(chat): scope the managed-credential listing to the group's worksp…
waleedlatif1 Sep 2, 2026
6da5af5
fix(credentials): refuse a managed credential whose group or option i…
waleedlatif1 Sep 2, 2026
dfeaf87
fix(home): clear the composer when a result is handed to the Assistan…
waleedlatif1 Sep 2, 2026
9f2756b
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
ed3d7ab
fix(knowledge): budget the pending ACL rewrite per run, and converge …
waleedlatif1 Sep 2, 2026
02bf4c7
fix(home): ground an Assistant send from the query cache instead of g…
waleedlatif1 Sep 2, 2026
59d2161
Merge remote-tracking branch 'origin/staging' into feat/permission-aw…
waleedlatif1 Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions apps/docs/content/docs/integrations/knowledge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Integrate Knowledge into the workflow. Perform full CRUD operations on documents

### Knowledge Search

Search for similar content in a knowledge base using vector similarity
Search for similar content in a knowledge base by relevance

#### Input

Expand All @@ -43,7 +43,7 @@ Search for similar content in a knowledge base using vector similarity
| `query` | string | No | Search query text \(optional when using tag filters\) |
| `topK` | number | No | Number of most similar results to return \(1-100\) |
| `tagFilters` | array | No | Array of tag filters with tagName and tagValue properties |
| `searchMode` | string | No | Retrieval mode: 'vector' \(default\) uses semantic similarity only, 'hybrid' also runs a full-text leg and fuses both |
| `searchMode` | string | No | Retrieval mode: 'hybrid' fuses a full-text leg with semantic similarity, 'vector' uses semantic similarity only; omit for the workspace's default |
| `rerankerEnabled` | boolean | No | Whether to apply Cohere reranking to vector search results |
| `rerankerModel` | string | No | Cohere rerank model to use \(one of: rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5\) |
| `rerankerInputCount` | number | No | Number of vector results sent to the Cohere reranker \(1–100\). Defaults to topK × 4 capped at 100. |
Expand Down
1 change: 0 additions & 1 deletion apps/docs/openapi-v2-knowledge.json
Original file line number Diff line number Diff line change
Expand Up @@ -6144,7 +6144,6 @@
},
"searchMode": {
"description": "Retrieval strategy: vector is semantic-only, while hybrid also runs full-text search.",
"default": "vector",
"anyOf": [
{
"type": "string",
Expand Down
1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# FORKING_ENABLED= # Workspace forks
# CREDENTIAL_GROUPS= # Enterprise managed OAuth collections
# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup
# KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval
# ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only

# Instance organization (Optional). Most enterprise features read their settings from the
Expand Down
51 changes: 44 additions & 7 deletions apps/sim/app/api/files/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { createLogger } from '@sim/logger'
import { permissionSatisfies } from '@sim/platform-authz/workspace'
import { and, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { knowledgeAccessCondition } from '@/lib/knowledge/access/predicate'
import {
resolveUserKnowledgeAccessScope,
WORKSPACE_ACCESS_SCOPE,
} from '@/lib/knowledge/access/scope'
import type { KnowledgeAccessScope, SystemAccessScope } from '@/lib/knowledge/access/types'
import { getFileMetadata } from '@/lib/uploads'
import type { StorageContext } from '@/lib/uploads/config'
import type { StorageConfig } from '@/lib/uploads/core/storage-client'
Expand Down Expand Up @@ -141,7 +147,7 @@ export async function verifyFileAccess(
customConfig?: StorageConfig,
context?: StorageContext | 'general',
isLocal?: boolean,
options?: { requireWrite?: boolean }
options?: { requireWrite?: boolean; knowledgeAccess?: KnowledgeFileAccess }
): Promise<boolean> {
const requireWrite = options?.requireWrite ?? false
try {
Expand Down Expand Up @@ -182,7 +188,7 @@ export async function verifyFileAccess(

// 4. KB files: kb/filename
if (inferredContext === 'knowledge-base') {
return await verifyKBFileAccess(cloudKey, userId, customConfig)
return await verifyKBFileAccess(cloudKey, userId, customConfig, options?.knowledgeAccess)
}

// 5. Chat files: chat/filename
Expand Down Expand Up @@ -485,7 +491,14 @@ async function verifyCopilotFileAccess(
* signal only: it reflects whether the file is still part of a live KB, not who
* owns it (ownership comes from the binding).
*/
async function hasActiveKbDocumentForKey(cloudKey: string, workspaceId: string): Promise<boolean> {
/** A reader once resolved: a person's or the workspace's tokens, or the system reading its own rows. */
type ResolvedKnowledgeFileAccess = KnowledgeAccessScope | SystemAccessScope

async function hasActiveKbDocumentForKey(
cloudKey: string,
workspaceId: string,
access: ResolvedKnowledgeFileAccess
): Promise<boolean> {
const rows = await db
.select({ id: document.id })
.from(document)
Expand All @@ -497,14 +510,35 @@ async function hasActiveKbDocumentForKey(cloudKey: string, workspaceId: string):
eq(document.userExcluded, false),
isNull(document.archivedAt),
isNull(document.deletedAt),
isNull(knowledgeBase.deletedAt)
isNull(knowledgeBase.deletedAt),
knowledgeAccessCondition(access)
)
)
.limit(1)

return rows.length > 0
}

/**
* How a KB file read identifies the reader for document access. `'user'` is
* for a session-authenticated person; a resolved scope is for a caller that
* already holds one (an execution with a principal). The system scope is for
* a background job reading a connector-owned row it is processing, which in
* members mode is hidden until the sync materializes its readers. Anything
* else — an internal token, a tool running with the workflow owner's id —
* reads as the workspace, never as the person whose id it happens to carry.
*/
export type KnowledgeFileAccess = 'user' | ResolvedKnowledgeFileAccess

async function resolveKnowledgeFileAccess(
knowledgeAccess: KnowledgeFileAccess | undefined,
userId: string,
workspaceId: string
): Promise<ResolvedKnowledgeFileAccess> {
if (knowledgeAccess === 'user') return resolveUserKnowledgeAccessScope(userId, workspaceId)
return knowledgeAccess ?? WORKSPACE_ACCESS_SCOPE
}

/**
* Verify access to KB files (`kb/<key>`).
*
Expand All @@ -522,7 +556,8 @@ async function hasActiveKbDocumentForKey(cloudKey: string, workspaceId: string):
async function verifyKBFileAccess(
cloudKey: string,
userId: string,
customConfig?: StorageConfig
customConfig?: StorageConfig,
knowledgeAccess?: KnowledgeFileAccess
): Promise<boolean> {
try {
const binding = await getFileMetadataByKey(cloudKey, 'knowledge-base', {
Expand Down Expand Up @@ -552,10 +587,12 @@ async function verifyKBFileAccess(
return false
}

if (!(await hasActiveKbDocumentForKey(cloudKey, binding.workspaceId))) {
logger.warn('KB file access denied: no active document references the file', {
const access = await resolveKnowledgeFileAccess(knowledgeAccess, userId, binding.workspaceId)
if (!(await hasActiveKbDocumentForKey(cloudKey, binding.workspaceId, access))) {
logger.warn('KB file access denied: no readable document references the file', {
userId,
cloudKey,
accessScopeKind: access.kind,
})
return false
}
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/files/export/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ function embeds(...ids: string[]) {
mockExtractEmbeddedFileRefs.mockReturnValue({ keys: [], ids })
}

vi.mock('@/lib/auth/hybrid', () => ({ checkSessionOrInternalAuth: mockCheckAuth }))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkSessionOrInternalAuth: mockCheckAuth,
}))
vi.mock('@/lib/uploads/server/metadata', () => ({
getFileMetadataById: mockGetFileMetadataById,
}))
Expand Down
15 changes: 12 additions & 3 deletions apps/sim/app/api/files/export/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { fileExportContract } from '@/lib/api/contracts/storage-transfer'
import { parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency'
import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
Expand Down Expand Up @@ -81,7 +81,10 @@ export const GET = withRouteHandler(
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}

const hasAccess = await verifyFileAccess(record.key, userId)
const knowledgeAccess = authResult.authType === AuthType.SESSION ? 'user' : undefined
const hasAccess = await verifyFileAccess(record.key, userId, undefined, undefined, undefined, {
knowledgeAccess,
})
if (!hasAccess) {
logger.warn('Unauthorized file export attempt', { id, userId })
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
Expand Down Expand Up @@ -164,7 +167,13 @@ export const GET = withRouteHandler(
try {
const imgRecord = await getFileMetadataById(storedFileId(imageId))
if (!imgRecord) return null
if (!(await verifyFileAccess(imgRecord.key, userId))) return null
if (
!(await verifyFileAccess(imgRecord.key, userId, undefined, undefined, undefined, {
knowledgeAccess,
}))
) {
return null
}
return { imageId, record: imgRecord, size: getWorkspaceFileSize(imgRecord) }
} catch (error) {
logger.warn('Failed to resolve asset for export', {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/files/serve/[...path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ describe('File Serve API Route', () => {
'test-user-id',
undefined,
'mothership',
false
false,
{ knowledgeAccess: undefined }
)
expect(storageServiceMockFns.mockDownloadFile).toHaveBeenCalledWith({
key: 'workspace/test-workspace-id/1234567890-photo.png',
Expand Down
37 changes: 29 additions & 8 deletions apps/sim/app/api/files/serve/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
concealCrossTenantResourceError,
InternalUnauthenticatedError,
} from '@/lib/api/server/routes'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { resolveServableDocBytes } from '@/lib/copilot/tools/server/files/doc-compile'
import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile-error'
import { asOrchestrationError } from '@/lib/core/orchestration/types'
Expand All @@ -26,7 +26,7 @@ import { internalWorkspaceFileServeAuth } from '@/lib/workspace-files/api'
import { readWorkspaceFileContentByKey } from '@/lib/workspace-files/application/read-workspace-file-content-by-key'
import { isSimPageSource, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile'
import { renderSimPageDocumentWithAssets } from '@/lib/workspace-files/page-document.server'
import { verifyFileAccess } from '@/app/api/files/authorization'
import { type KnowledgeFileAccess, verifyFileAccess } from '@/app/api/files/authorization'
import {
createErrorResponse,
createFileResponse,
Expand Down Expand Up @@ -271,12 +271,29 @@ export const GET = withRouteHandler(

const userId = legacyAuthResult?.userId
if (!userId) throw new Error('Authenticated file serve request is missing a user ID')
/** Only a session identifies a person; an internal token's user id reads as the workspace. */
const knowledgeAccess =
legacyAuthResult?.authType === AuthType.SESSION ? ('user' as const) : undefined

if (isUsingCloudStorage()) {
return await handleCloudProxy(cloudKey, userId, options, request.signal, storageContext)
return await handleCloudProxy(
cloudKey,
userId,
options,
request.signal,
storageContext,
knowledgeAccess
)
}

return await handleLocalFile(cloudKey, userId, options, request.signal, storageContext)
return await handleLocalFile(
cloudKey,
userId,
options,
request.signal,
storageContext,
knowledgeAccess
)
} catch (error) {
if (error instanceof InternalUnauthenticatedError) {
logger.warn('Unauthorized file access attempt', { error: error.message })
Expand Down Expand Up @@ -359,7 +376,8 @@ async function handleLocalFile(
userId: string,
options: ServeOptions,
signal: AbortSignal | undefined,
context: StorageContext
context: StorageContext,
knowledgeAccess: KnowledgeFileAccess | undefined
): Promise<NextResponse> {
const ownerKey = `user:${userId}`
try {
Expand All @@ -368,7 +386,8 @@ async function handleLocalFile(
userId,
undefined, // customConfig
context,
true // isLocal
true, // isLocal
{ knowledgeAccess }
)

if (!hasAccess) {
Expand Down Expand Up @@ -419,7 +438,8 @@ async function handleCloudProxy(
userId: string,
options: ServeOptions,
signal: AbortSignal | undefined,
context: StorageContext
context: StorageContext,
knowledgeAccess: KnowledgeFileAccess | undefined
): Promise<NextResponse> {
const ownerKey = `user:${userId}`
try {
Expand All @@ -430,7 +450,8 @@ async function handleCloudProxy(
userId,
undefined, // customConfig
context, // context
false // isLocal
false, // isLocal
{ knowledgeAccess }
)

if (!hasAccess) {
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/app/api/files/view/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { fileViewContract } from '@/lib/api/contracts/storage-transfer'
import { parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getServeStoragePrefix, type StorageContext } from '@/lib/uploads/config'
import { getFileMetadataById } from '@/lib/uploads/server/metadata'
Expand Down Expand Up @@ -37,7 +37,9 @@ export const GET = withRouteHandler(
record.key,
authResult.userId,
undefined,
record.context as StorageContext | 'general'
record.context as StorageContext | 'general',
undefined,
{ knowledgeAccess: authResult.authType === AuthType.SESSION ? 'user' : undefined }
)
if (!hasAccess) {
logger.warn('Unauthorized file view attempt', { id, userId: authResult.userId })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { updateKnowledgeConnectorAccessContract } from '@/lib/api/contracts/knowledge'
import {
defineInternalJsonRoute,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import {
resolveInternalKnowledgeBillingAttribution,
toInternalKnowledgeConnector,
} from '@/lib/knowledge/api/internal-route'
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
import { updateKnowledgeConnectorAccess } from '@/lib/knowledge/application/connector-access'
import { knowledgeOperations } from '@/lib/knowledge/application/operations'

export const PATCH = defineInternalJsonRoute({
contract: updateKnowledgeConnectorAccessContract,
auth: internalSessionAuth,
operation: knowledgeOperations.updateConnectorAccess,
rateLimit: internalRateLimits.none({
reason: 'A settings action an admin performs by hand; the switch itself is bounded',
}),
errorPolicy: internalKnowledgeErrorPolicies.connectors,
mapInput: ({ params, body }, { principal, request }) => ({
connectorId: params.connectorId,
knowledgeBaseId: params.id,
accessMode: body.accessMode,
credentialGroupId: body.credentialGroupId,
credentialGroupOptionId: body.credentialGroupOptionId,
credentialId: body.credentialId,
resolveBillingAttribution: (workspaceId: string) =>
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId),
source: 'ui' as const,
}),
useCase: updateKnowledgeConnectorAccess,
present: ({ connector }) => ({
success: true as const,
data: toInternalKnowledgeConnector(connector),
}),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { startKnowledgeConnectorMemberEnrollmentContract } from '@/lib/api/contracts/knowledge'
import {
defineInternalJsonRoute,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
import { startKnowledgeConnectorMemberEnrollment } from '@/lib/knowledge/application/connector-access'
import { knowledgeOperations } from '@/lib/knowledge/application/operations'

export const POST = defineInternalJsonRoute({
contract: startKnowledgeConnectorMemberEnrollmentContract,
auth: internalSessionAuth,
operation: knowledgeOperations.enrollConnectorMember,
rateLimit: internalRateLimits.none({
reason:
'A member connecting their own account by hand; each call only re-issues their own invitation',
}),
errorPolicy: internalKnowledgeErrorPolicies.connectors,
mapInput: ({ params }) => ({
connectorId: params.connectorId,
knowledgeBaseId: params.id,
}),
useCase: startKnowledgeConnectorMemberEnrollment,
present: ({ url }) => ({ success: true as const, data: { url } }),
})
3 changes: 3 additions & 0 deletions apps/sim/app/api/knowledge/[id]/connectors/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export const POST = defineInternalJsonRoute({
apiKey: body.apiKey,
sourceConfig: body.sourceConfig,
syncIntervalMinutes: body.syncIntervalMinutes,
accessMode: body.accessMode,
credentialGroupId: body.credentialGroupId,
credentialGroupOptionId: body.credentialGroupOptionId,
resolveBillingAttribution: (workspaceId: string) =>
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId),
source: 'ui' as const,
Expand Down
Loading
Loading