@@ -104,8 +104,8 @@ import { calculateCost } from '@/providers/utils'
104104const logger = createLogger ( 'DocumentService' )
105105
106106/**
107- * Thrown when a knowledge-base document's `fileUrl` references an internal `kb/`
108- * storage object that is not owned by the target knowledge base's workspace.
107+ * Thrown when a knowledge-base document's `fileUrl` references an internal
108+ * knowledge-base storage object not owned by the target knowledge base's workspace.
109109 * Routes map this to a 403.
110110 */
111111export class KnowledgeBaseFileOwnershipError extends Error {
@@ -117,17 +117,17 @@ export class KnowledgeBaseFileOwnershipError extends Error {
117117
118118/**
119119 * Guard document `fileUrl`s at creation time. When a URL points at an internal
120- * `kb/` storage object, require that the target knowledge base owns the object,
120+ * knowledge-base storage object, require that the target knowledge base owns the object,
121121 * resolved from the trusted `workspace_files` binding:
122122 *
123123 * - Workspace KB (`kbWorkspaceId` set): the binding's `workspaceId` must match.
124124 * - Personal KB (`kbWorkspaceId` null): the binding's `userId` must be the KB
125125 * owner. A key bound to another tenant is rejected; an unbound key (legacy /
126126 * never reserved) passes since it carries no cross-tenant ownership.
127127 *
128- * External `http(s)`/`data:` URLs (ingestion sources) and non-`kb/` internal keys
128+ * External `http(s)`/`data:` URLs (ingestion sources) and other internal keys
129129 * pass through unchanged. This blocks a user from asserting ownership of another
130- * tenant's `kb/` key via a planted `fileUrl` — including in a personal KB, which
130+ * tenant's object via a planted `fileUrl` — including in a personal KB, which
131131 * otherwise could be moved into a workspace to launder the binding. All
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.
@@ -2557,8 +2557,7 @@ export async function deleteDocumentStorageFiles(
25572557 entries
25582558 . map ( ( entry ) => entry . storageKey )
25592559 . filter (
2560- ( key ) : key is string =>
2561- typeof key === 'string' && ( key . startsWith ( 'kb/' ) || key . startsWith ( 'knowledge-base/' ) )
2560+ ( key ) : key is string => typeof key === 'string' && isKnowledgeBaseOwnedStorageKey ( key )
25622561 )
25632562 ) ,
25642563 ]
@@ -2575,7 +2574,7 @@ export async function deleteDocumentStorageFiles(
25752574 return
25762575 }
25772576
2578- if ( ! storageKey . startsWith ( 'kb/' ) && ! storageKey . startsWith ( 'knowledge-base/' ) ) {
2577+ if ( ! isKnowledgeBaseOwnedStorageKey ( storageKey ) ) {
25792578 return
25802579 }
25812580
@@ -2598,13 +2597,20 @@ export async function deleteDocumentStorageFiles(
25982597 }
25992598
26002599 try {
2601- await deleteFile ( { key : storageKey , context : 'knowledge-base' } )
2602- await deleteFileMetadataByIdentity ( {
2600+ const metadataDeleted = await deleteFileMetadataByIdentity ( {
26032601 id : binding . id ,
26042602 key : binding . key ,
26052603 context : binding . context ,
26062604 contentUpdatedAt : binding . contentUpdatedAt ,
26072605 } )
2606+ if ( ! metadataDeleted ) {
2607+ logger . warn ( `[${ requestId } ] Skipping storage delete: ownership binding changed` , {
2608+ documentId : doc . id ,
2609+ storageKey,
2610+ } )
2611+ return
2612+ }
2613+ await deleteFile ( { key : storageKey , context : 'knowledge-base' } )
26082614 } catch ( error ) {
26092615 logger . warn ( `[${ requestId } ] Failed to delete document storage file` , {
26102616 documentId : doc . id ,
0 commit comments