Skip to content

Commit a2176b1

Browse files
committed
address comments
1 parent dca9afa commit a2176b1

6 files changed

Lines changed: 522 additions & 77 deletions

File tree

apps/sim/lib/knowledge/documents/service.ts

Lines changed: 153 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ import {
9393
} from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
9494
import { deleteFile } from '@/lib/uploads/core/storage-service'
9595
import {
96-
deleteFileMetadata,
96+
deleteFileMetadataByIdentity,
9797
type FileMetadataRecord,
9898
getFileMetadataByKeys,
9999
} from '@/lib/uploads/server/metadata'
@@ -132,28 +132,65 @@ export class KnowledgeBaseFileOwnershipError extends Error {
132132
* referenced bindings are resolved in one query (no N+1 inside the `FOR UPDATE`
133133
* window). Single-document callers pass a one-element array.
134134
*/
135+
function getKnowledgeBaseStorageKeys(fileUrls: readonly string[]): string[] {
136+
return [
137+
...new Set(
138+
fileUrls
139+
.map((url) => getKnowledgeBaseStorageKey(url))
140+
.filter((key): key is string => typeof key === 'string' && key.startsWith('kb/'))
141+
),
142+
]
143+
}
144+
145+
function getWorkspaceSourceStorageKeys(fileUrls: readonly string[]): string[] {
146+
return [
147+
...new Set(
148+
fileUrls
149+
.map((url) => getKnowledgeBaseStorageKey(url))
150+
.filter((key): key is string => typeof key === 'string' && key.startsWith('workspace/'))
151+
),
152+
]
153+
}
154+
155+
async function loadKnowledgeBaseFileBindings(
156+
fileUrls: readonly string[],
157+
executor: DbExecutor = db
158+
): Promise<Map<string, FileMetadataRecord>> {
159+
const keys = getKnowledgeBaseStorageKeys(fileUrls)
160+
const bindings =
161+
keys.length > 0 ? await getFileMetadataByKeys(keys, 'knowledge-base', executor) : []
162+
163+
return new Map(bindings.map((binding) => [binding.key, binding]))
164+
}
165+
166+
async function loadWorkspaceSourceFileBindings(
167+
fileUrls: readonly string[],
168+
executor: DbExecutor = db
169+
): Promise<Map<string, FileMetadataRecord>> {
170+
const keys = getWorkspaceSourceStorageKeys(fileUrls)
171+
if (keys.length === 0) return new Map()
172+
173+
const workspaceBindings = await getFileMetadataByKeys(keys, 'workspace', executor)
174+
const mothershipBindings = await getFileMetadataByKeys(keys, 'mothership', executor)
175+
176+
return new Map(
177+
[...workspaceBindings, ...mothershipBindings].map((binding) => [binding.key, binding])
178+
)
179+
}
180+
135181
async function assertKnowledgeBaseFileUrlsOwnership(
136182
fileUrls: string[],
137183
kbWorkspaceId: string | null,
138184
kbUserId: string,
139185
requestId: string,
140186
executor: DbExecutor = db
141187
): Promise<Map<string, FileMetadataRecord>> {
142-
const keys = [
143-
...new Set(
144-
fileUrls
145-
.map((url) => getKnowledgeBaseStorageKey(url))
146-
.filter((key): key is string => typeof key === 'string' && key.startsWith('kb/'))
147-
),
148-
]
188+
const keys = getKnowledgeBaseStorageKeys(fileUrls)
149189
if (keys.length === 0) {
150190
return new Map()
151191
}
152192

153-
// Read bindings on the caller's transaction so the security check shares the
154-
// same connection/lock context as the FOR UPDATE'd insert that follows.
155-
const bindings = await getFileMetadataByKeys(keys, 'knowledge-base', executor)
156-
const bindingByKey = new Map(bindings.map((binding) => [binding.key, binding]))
193+
const bindingByKey = await loadKnowledgeBaseFileBindings(fileUrls, executor)
157194

158195
for (const key of keys) {
159196
const binding = bindingByKey.get(key)
@@ -189,6 +226,21 @@ async function assertKnowledgeBaseFileUrlsOwnership(
189226
return bindingByKey
190227
}
191228

229+
async function loadCurrentWorkspaceSourceFileSecretProvenance(options: {
230+
fileUrl: string
231+
}): Promise<DurableSecretProvenance | undefined> {
232+
const storageKey = getKnowledgeBaseStorageKey(options.fileUrl)
233+
if (!storageKey?.startsWith('workspace/')) return undefined
234+
235+
const bindingByKey = await loadWorkspaceSourceFileBindings([options.fileUrl])
236+
const binding = bindingByKey.get(storageKey)
237+
if (!binding) return undefined
238+
239+
const provenanceById = await getBoundWorkspaceFileSecretProvenanceByMetadata(db, [binding])
240+
const provenance = provenanceById.get(binding.id) ?? { status: 'unknown' as const }
241+
return durableSecretProvenanceFromWorkspaceFile(provenance, binding)
242+
}
243+
192244
const TIMEOUTS = {
193245
OVERALL_PROCESSING: envNumber(env.KB_CONFIG_MAX_DURATION, 600) * 1000,
194246
} as const
@@ -722,6 +774,10 @@ export async function processDocumentAsync(
722774
embeddingModel: knowledgeBase.embeddingModel,
723775
billedAccountUserId: workspaceTable.billedAccountUserId,
724776
uploadedBy: document.uploadedBy,
777+
filename: document.filename,
778+
fileUrl: document.fileUrl,
779+
fileSize: document.fileSize,
780+
mimeType: document.mimeType,
725781
tag1: document.tag1,
726782
tag2: document.tag2,
727783
tag3: document.tag3,
@@ -773,6 +829,12 @@ export async function processDocumentAsync(
773829
}
774830

775831
const ctx = contextRows[0]
832+
const persistedDocData = {
833+
filename: ctx.filename,
834+
fileUrl: ctx.fileUrl,
835+
fileSize: ctx.fileSize,
836+
mimeType: ctx.mimeType,
837+
}
776838

777839
await db
778840
.update(document)
@@ -861,19 +923,26 @@ export async function processDocumentAsync(
861923
let embeddingModelName = kbEmbeddingModel
862924
let embeddingPricingId = kbEmbeddingModel
863925

864-
const documentSecretContext = await loadKnowledgeDocumentSecretRegistry(documentId, {
865-
userId: documentActorUserId,
866-
...(ctx.workspaceId ? { workspaceId: ctx.workspaceId } : {}),
926+
const currentSourceFileProvenance = await loadCurrentWorkspaceSourceFileSecretProvenance({
927+
fileUrl: persistedDocData.fileUrl,
867928
})
929+
const documentSecretContext = await loadKnowledgeDocumentSecretRegistry(
930+
documentId,
931+
{
932+
userId: documentActorUserId,
933+
...(ctx.workspaceId ? { workspaceId: ctx.workspaceId } : {}),
934+
},
935+
currentSourceFileProvenance
936+
)
868937

869938
await withTimeout(
870939
runWithKnowledgeModelInputProvenance(
871940
documentSecretContext.registry,
872941
async () => {
873942
const processed = await processDocument(
874-
docData.fileUrl,
875-
docData.filename,
876-
docData.mimeType,
943+
persistedDocData.fileUrl,
944+
persistedDocData.filename,
945+
persistedDocData.mimeType,
877946
kbConfig.maxSize,
878947
kbConfig.overlap,
879948
kbConfig.minSize,
@@ -1171,15 +1240,9 @@ function getServerKnownDocumentSize(
11711240
async function resolveServerKnownDocumentSizes<
11721241
T extends { readonly fileUrl: string; readonly fileSize: number },
11731242
>(documents: readonly T[]): Promise<Array<T & { fileSize: number }>> {
1174-
const keys = [
1175-
...new Set(
1176-
documents
1177-
.map((docData) => getKnowledgeBaseStorageKey(docData.fileUrl))
1178-
.filter((key): key is string => typeof key === 'string' && key.startsWith('kb/'))
1179-
),
1180-
]
1181-
const bindings = keys.length > 0 ? await getFileMetadataByKeys(keys, 'knowledge-base') : []
1182-
const bindingByKey = new Map(bindings.map((binding) => [binding.key, binding]))
1243+
const bindingByKey = await loadKnowledgeBaseFileBindings(
1244+
documents.map((document) => document.fileUrl)
1245+
)
11831246
return documents.map((docData) => ({
11841247
...docData,
11851248
fileSize: getServerKnownDocumentSize(docData.fileUrl, docData.fileSize, bindingByKey),
@@ -1302,9 +1365,14 @@ export async function createDocumentRecords(
13021365
requestId,
13031366
tx
13041367
)
1305-
const trackedBindings = [...bindingByKey.values()].filter(
1306-
(binding) => binding.secretProvenanceVersion !== null
1368+
const sourceBindingByKey = await loadWorkspaceSourceFileBindings(
1369+
resolvedDocuments.map((docData) => docData.fileUrl),
1370+
tx
13071371
)
1372+
const trackedBindings = [
1373+
...[...bindingByKey.values()].filter((binding) => binding.secretProvenanceVersion !== null),
1374+
...sourceBindingByKey.values(),
1375+
]
13081376
const boundFileProvenanceById = await getBoundWorkspaceFileSecretProvenanceByMetadata(
13091377
tx,
13101378
trackedBindings
@@ -1411,17 +1479,22 @@ export async function createDocumentRecords(
14111479
boolean3: processedTags.boolean3 ?? null,
14121480
}
14131481
const source = createKnowledgeDocumentSourceValue(baseDocument)
1414-
const binding = storageKey ? bindingByKey.get(storageKey) : undefined
1415-
const boundFileProvenance =
1416-
binding && binding.secretProvenanceVersion !== null
1417-
? (boundFileProvenanceById.get(binding.id) ?? { status: 'unknown' as const })
1482+
const binding = storageKey
1483+
? (bindingByKey.get(storageKey) ?? sourceBindingByKey.get(storageKey))
1484+
: undefined
1485+
const provenanceBinding =
1486+
binding && (binding.secretProvenanceVersion !== null || sourceBindingByKey.has(binding.key))
1487+
? binding
14181488
: undefined
1489+
const boundFileProvenance = provenanceBinding
1490+
? (boundFileProvenanceById.get(provenanceBinding.id) ?? { status: 'unknown' as const })
1491+
: undefined
14191492
const provenance = bindKnowledgeDocumentWriteSecretProvenance({
14201493
source,
14211494
provenance: secretProvenances?.[documentIndex],
14221495
tagDefinitions,
1423-
...(binding && boundFileProvenance
1424-
? { boundFile: { binding, provenance: boundFileProvenance } }
1496+
...(provenanceBinding && boundFileProvenance
1497+
? { boundFile: { binding: provenanceBinding, provenance: boundFileProvenance } }
14251498
: {}),
14261499
})
14271500
const newDocument = {
@@ -1833,11 +1906,21 @@ export async function createSingleDocument(
18331906
requestId,
18341907
tx
18351908
)
1909+
const sourceBindingByKey = await loadWorkspaceSourceFileBindings(
1910+
[resolvedDocumentData.fileUrl],
1911+
tx
1912+
)
18361913
const storageKey = getKnowledgeBaseStorageKey(resolvedDocumentData.fileUrl)
1837-
const binding = storageKey ? bindingByKey.get(storageKey) : undefined
1914+
const binding = storageKey
1915+
? (bindingByKey.get(storageKey) ?? sourceBindingByKey.get(storageKey))
1916+
: undefined
1917+
const provenanceBinding =
1918+
binding && (binding.secretProvenanceVersion !== null || sourceBindingByKey.has(binding.key))
1919+
? binding
1920+
: undefined
18381921
const boundFileProvenanceById = await getBoundWorkspaceFileSecretProvenanceByMetadata(
18391922
tx,
1840-
binding && binding.secretProvenanceVersion !== null ? [binding] : []
1923+
provenanceBinding ? [provenanceBinding] : []
18411924
)
18421925
const currentSize = getServerKnownDocumentSize(
18431926
resolvedDocumentData.fileUrl,
@@ -1873,16 +1956,15 @@ export async function createSingleDocument(
18731956
}
18741957

18751958
const source = createKnowledgeDocumentSourceValue(newDocument)
1876-
const boundFileProvenance =
1877-
binding && binding.secretProvenanceVersion !== null
1878-
? (boundFileProvenanceById.get(binding.id) ?? { status: 'unknown' as const })
1879-
: undefined
1959+
const boundFileProvenance = provenanceBinding
1960+
? (boundFileProvenanceById.get(provenanceBinding.id) ?? { status: 'unknown' as const })
1961+
: undefined
18801962
const documentProvenance = bindKnowledgeDocumentWriteSecretProvenance({
18811963
source,
18821964
provenance: secretProvenance,
18831965
tagDefinitions,
1884-
...(binding && boundFileProvenance
1885-
? { boundFile: { binding, provenance: boundFileProvenance } }
1966+
...(provenanceBinding && boundFileProvenance
1967+
? { boundFile: { binding: provenanceBinding, provenance: boundFileProvenance } }
18861968
: {}),
18871969
})
18881970
await tx.insert(document).values({
@@ -2464,20 +2546,18 @@ export async function deleteDocumentStorageFiles(
24642546
storageKey: getKnowledgeBaseStorageKey(doc.fileUrl),
24652547
}))
24662548

2467-
// Resolve all kb/ ownership bindings in one query (avoids an N+1 across the
2468-
// delete fan-out below).
2469-
const kbKeys = [
2549+
const storageKeys = [
24702550
...new Set(
24712551
entries
24722552
.map((entry) => entry.storageKey)
2473-
.filter((key): key is string => typeof key === 'string' && key.startsWith('kb/'))
2553+
.filter((key): key is string => typeof key === 'string')
24742554
),
24752555
]
2476-
const ownerByKey = new Map<string, string | null>()
2477-
if (kbKeys.length > 0) {
2478-
const bindings = await getFileMetadataByKeys(kbKeys, 'knowledge-base')
2556+
const bindingByKey = new Map<string, FileMetadataRecord>()
2557+
if (storageKeys.length > 0) {
2558+
const bindings = await getFileMetadataByKeys(storageKeys, 'knowledge-base')
24792559
for (const binding of bindings) {
2480-
ownerByKey.set(binding.key, binding.workspaceId)
2560+
bindingByKey.set(binding.key, binding)
24812561
}
24822562
}
24832563

@@ -2486,32 +2566,32 @@ export async function deleteDocumentStorageFiles(
24862566
return
24872567
}
24882568

2489-
// Only delete a kb/ object when its trusted ownership binding confirms the
2490-
// deleting document's workspace owns it. Prevents deleting another tenant's
2491-
// object via a document with a planted fileUrl.
2492-
if (storageKey.startsWith('kb/')) {
2493-
const bindingWorkspaceId = ownerByKey.get(storageKey)
2494-
if (!bindingWorkspaceId) {
2495-
logger.warn(`[${requestId}] Skipping storage delete: no ownership binding for key`, {
2496-
documentId: doc.id,
2497-
storageKey,
2498-
})
2499-
return
2500-
}
2501-
if (!doc.workspaceId || bindingWorkspaceId !== doc.workspaceId) {
2502-
logger.warn(`[${requestId}] Skipping storage delete: ownership binding mismatch`, {
2503-
documentId: doc.id,
2504-
storageKey,
2505-
bindingWorkspaceId,
2506-
documentWorkspaceId: doc.workspaceId ?? null,
2507-
})
2508-
return
2509-
}
2569+
const binding = bindingByKey.get(storageKey)
2570+
if (!binding?.workspaceId) {
2571+
logger.warn(`[${requestId}] Skipping storage delete: no ownership binding for key`, {
2572+
documentId: doc.id,
2573+
storageKey,
2574+
})
2575+
return
2576+
}
2577+
if (!doc.workspaceId || binding.workspaceId !== doc.workspaceId) {
2578+
logger.warn(`[${requestId}] Skipping storage delete: ownership binding mismatch`, {
2579+
documentId: doc.id,
2580+
storageKey,
2581+
bindingWorkspaceId: binding.workspaceId,
2582+
documentWorkspaceId: doc.workspaceId ?? null,
2583+
})
2584+
return
25102585
}
25112586

25122587
try {
25132588
await deleteFile({ key: storageKey, context: 'knowledge-base' })
2514-
await deleteFileMetadata(storageKey)
2589+
await deleteFileMetadataByIdentity({
2590+
id: binding.id,
2591+
key: binding.key,
2592+
context: binding.context,
2593+
contentUpdatedAt: binding.contentUpdatedAt,
2594+
})
25152595
} catch (error) {
25162596
logger.warn(`[${requestId}] Failed to delete document storage file`, {
25172597
documentId: doc.id,

0 commit comments

Comments
 (0)