fix: support Dify-managed KnowledgeFS deployment

This commit is contained in:
Jyong 2026-07-23 02:51:05 -04:00
parent 85c8d026a2
commit 0300a4d881
8 changed files with 48 additions and 5 deletions

View File

@ -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

View File

@ -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",

View File

@ -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,
});

View File

@ -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,

View File

@ -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,

View File

@ -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");
});
});

View File

@ -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`;

View File

@ -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<SourceSecretStore, "delete">;
readonly secretStore?: Pick<SourceSecretStore, "delete"> | 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,