From 0300a4d8815f9819221569c9ee1da8a53ff3f81a Mon Sep 17 00:00:00 2001 From: Jyong Date: Thu, 23 Jul 2026 02:51:05 -0400 Subject: [PATCH] fix: support Dify-managed KnowledgeFS deployment --- knowledge-fs/apps/api/Dockerfile | 1 + knowledge-fs/apps/api/package.json | 2 +- .../api/src/durable-deletion-options.test.ts | 2 +- .../apps/api/src/durable-deletion-options.ts | 7 +++-- knowledge-fs/apps/api/src/index.ts | 1 + .../apps/api/src/server-options.test.ts | 2 ++ ...rable-deletion-target-capabilities.test.ts | 26 +++++++++++++++++++ ...se-durable-deletion-target-capabilities.ts | 12 ++++++++- 8 files changed, 48 insertions(+), 5 deletions(-) diff --git a/knowledge-fs/apps/api/Dockerfile b/knowledge-fs/apps/api/Dockerfile index 80ad61e6434..1404efcda45 100644 --- a/knowledge-fs/apps/api/Dockerfile +++ b/knowledge-fs/apps/api/Dockerfile @@ -43,6 +43,7 @@ ENV NODE_ENV=production WORKDIR /workspace COPY --from=builder /workspace/apps/api/dist/server.mjs ./server.mjs +COPY --from=builder /workspace/apps/api/dist/migrate.mjs ./migrate.mjs EXPOSE 8787 diff --git a/knowledge-fs/apps/api/package.json b/knowledge-fs/apps/api/package.json index 6aadc39636e..c3abce8b307 100644 --- a/knowledge-fs/apps/api/package.json +++ b/knowledge-fs/apps/api/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "tsc --noEmit", - "build:prod": "esbuild src/server.ts --bundle --platform=node --format=esm --target=node22 --banner:js=\"import { createRequire as __knowledgeCreateRequire } from 'node:module';const require = __knowledgeCreateRequire(import.meta.url);\" --outfile=dist/server.mjs", + "build:prod": "esbuild src/server.ts src/migrate.ts --bundle --platform=node --format=esm --target=node22 --banner:js=\"import { createRequire as __knowledgeCreateRequire } from 'node:module';const require = __knowledgeCreateRequire(import.meta.url);\" --outdir=dist --out-extension:.js=.mjs", "dev": "NODE_ENV=development node --env-file-if-exists=../../infra/local/.env --import tsx --watch src/server.ts", "start": "node dist/server.mjs", "test": "vitest run --passWithNoTests", diff --git a/knowledge-fs/apps/api/src/durable-deletion-options.test.ts b/knowledge-fs/apps/api/src/durable-deletion-options.test.ts index 676f72c8377..5f58f86b86b 100644 --- a/knowledge-fs/apps/api/src/durable-deletion-options.test.ts +++ b/knowledge-fs/apps/api/src/durable-deletion-options.test.ts @@ -116,10 +116,10 @@ describe("API durable deletion assembly", () => { const repository = repositoryStub(); const assembly = createApiDurableDeletionAssembly({ adapter: createNodePlatformAdapter({ env: {} }), + credentialMode: "dify-managed", enabled: true, production: true, repository, - secretStore: secretStoreStub(), usesDatabaseRepositories: true, }); diff --git a/knowledge-fs/apps/api/src/durable-deletion-options.ts b/knowledge-fs/apps/api/src/durable-deletion-options.ts index e3628f5aed2..fbc2253d312 100644 --- a/knowledge-fs/apps/api/src/durable-deletion-options.ts +++ b/knowledge-fs/apps/api/src/durable-deletion-options.ts @@ -23,6 +23,7 @@ export interface ApiDurableDeletionAssembly { export interface CreateApiDurableDeletionAssemblyOptions { readonly adapter: KnowledgeGatewayOptions["adapter"]; + readonly credentialMode?: "dify-managed" | "local" | undefined; readonly enabled: boolean; readonly production: boolean; readonly repository?: DurableDeletionRepository | undefined; @@ -44,6 +45,7 @@ export async function assertApiDurableDeletionDataReadiness({ /** Production is fail-closed: HTTP request persistence and both background loops are one unit. */ export function createApiDurableDeletionAssembly({ adapter, + credentialMode = "local", enabled, production, repository, @@ -62,15 +64,16 @@ export function createApiDurableDeletionAssembly({ } return undefined; } - if (!secretStore) { + if (credentialMode === "local" && !secretStore) { throw new Error("Durable deletion requires SourceSecretStore cleanup capability"); } const capabilities = createDatabaseDurableDeletionTargetCapabilities({ cache: adapter.cache, + credentialMode, database: adapter.database, objectStorage: adapter.objectStorage, - secretStore, + ...(secretStore ? { secretStore } : {}), }); const processor = createDurableDeletionTargetProcessors({ documentAsset: capabilities, diff --git a/knowledge-fs/apps/api/src/index.ts b/knowledge-fs/apps/api/src/index.ts index d75dc6f750e..ae67d6ba231 100644 --- a/knowledge-fs/apps/api/src/index.ts +++ b/knowledge-fs/apps/api/src/index.ts @@ -228,6 +228,7 @@ await assertApiDurableDeletionDataReadiness({ }); const durableDeletion = createApiDurableDeletionAssembly({ adapter, + credentialMode: "dify-managed", enabled: databaseRepositories.durableDeletionEnabled, production: process.env.NODE_ENV === "production", repository: databaseRepositories.durableDeletionRepository, diff --git a/knowledge-fs/apps/api/src/server-options.test.ts b/knowledge-fs/apps/api/src/server-options.test.ts index 4d259650ca6..fdb4029d4ca 100644 --- a/knowledge-fs/apps/api/src/server-options.test.ts +++ b/knowledge-fs/apps/api/src/server-options.test.ts @@ -23,6 +23,7 @@ describe("API Dockerfile production runtime", () => { const dockerfile = readFileSync(resolve(import.meta.dirname, "../Dockerfile"), "utf8"); expect(dockerfile).toContain('CMD ["node", "server.mjs"]'); + expect(dockerfile).toContain("dist/migrate.mjs ./migrate.mjs"); expect(dockerfile).toContain("USER node"); expect(dockerfile).not.toContain('"tsx"'); }); @@ -61,5 +62,6 @@ describe("API source dev script", () => { expect(packageJson.scripts?.["build:prod"]).toContain("--format=esm"); expect(packageJson.scripts?.["build:prod"]).toContain("createRequire"); + expect(packageJson.scripts?.["build:prod"]).toContain("src/migrate.ts"); }); }); diff --git a/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.test.ts b/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.test.ts index 61b432ac45c..6d471475cce 100644 --- a/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.test.ts +++ b/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.test.ts @@ -118,6 +118,32 @@ describe("database durable deletion target capabilities", () => { ).rejects.toThrow("has no cache key"); }); + it(`rejects legacy local credential residue in Dify-managed mode (${dialect})`, async () => { + const capabilities = createDatabaseDurableDeletionTargetCapabilities({ + cache: createMemoryCacheAdapter({ maxEntries: 10 }), + credentialMode: "dify-managed", + database: createSchemaDatabaseAdapter({ + executor: async () => result([]), + kind: dialect, + }), + objectStorage: createMemoryObjectStorageAdapter({ + kind: "memory", + maxObjectBytes: 1_024, + }), + }); + + await expect( + capabilities.executeExternalItem({ + item: deletionItem("secret_ref", { + credentialRef: "source-secret:v1:legacy", + resourceId: targetDocumentId, + }), + job: job(), + signal: new AbortController().signal, + }), + ).rejects.toThrow("legacy KnowledgeFS-managed source credential cleanup"); + }); + it(`resumes bounded document object inventory across database, manifest, and storage phases (${dialect})`, async () => { const prefix = `tenant-a/spaces/${spaceId}`; const artifactKey = `${prefix}/documents/${targetDocumentId}/artifact.json`; diff --git a/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.ts b/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.ts index f1c9e421378..6dcac9aa2e1 100644 --- a/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.ts +++ b/knowledge-fs/packages/api/src/database-durable-deletion-target-capabilities.ts @@ -44,10 +44,11 @@ import type { SourceSecretStore } from "./source-secret-store"; export interface DatabaseDurableDeletionTargetCapabilitiesOptions { readonly cache: CacheAdapter; + readonly credentialMode?: "dify-managed" | "local" | undefined; readonly database: DatabaseAdapter; readonly generatePublicationId?: (() => string) | undefined; readonly objectStorage: ObjectStorageAdapter; - readonly secretStore: Pick; + readonly secretStore?: Pick | undefined; } interface InventoryCursor { @@ -72,6 +73,7 @@ interface InventoryCursor { */ export function createDatabaseDurableDeletionTargetCapabilities({ cache, + credentialMode = "local", database, generatePublicationId = randomUUID, objectStorage, @@ -80,6 +82,9 @@ export function createDatabaseDurableDeletionTargetCapabilities({ if (!cache.deletePrefix) { throw new DeletionCleanupCapabilityUnavailableError("cache.deletePrefix"); } + if (credentialMode === "local" && !secretStore) { + throw new DeletionCleanupCapabilityUnavailableError("SourceSecretStore.delete"); + } const deleteCachePrefix = cache.deletePrefix.bind(cache); const retrievalExecutionLeases = createDatabaseRetrievalExecutionLeaseRepository({ database }); @@ -325,6 +330,11 @@ export function createDatabaseDurableDeletionTargetCapabilities({ if (!item.credentialRef || !item.resourceId) { throw new Error("Durable deletion secret item is incomplete"); } + if (!secretStore) { + throw new DeletionCleanupCapabilityUnavailableError( + "legacy KnowledgeFS-managed source credential cleanup", + ); + } await secretStore.delete({ knowledgeSpaceId: job.knowledgeSpaceId, ref: item.credentialRef,