diff --git a/server/src/knowledge/acl.ts b/server/src/knowledge/acl.ts deleted file mode 100644 index d9dc3e97..00000000 --- a/server/src/knowledge/acl.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { KnowledgeAclEntry, KnowledgeActor } from "./types"; - -export function canRead( - actor: KnowledgeActor, - entries: KnowledgeAclEntry[], -): boolean { - let allowed = false; - - for (const entry of entries) { - if (!matchesPrincipal(actor, entry.principal)) continue; - if (entry.effect === "deny") return false; - allowed = true; - } - - return allowed; -} - -function matchesPrincipal(actor: KnowledgeActor, principal: string): boolean { - if (principal === `user:${actor.userId}`) return true; - if (!principal.startsWith("group:")) return false; - return actor.groups.includes(principal.slice("group:".length)); -} diff --git a/server/src/knowledge/repository.ts b/server/src/knowledge/repository.ts deleted file mode 100644 index 883cf9e3..00000000 --- a/server/src/knowledge/repository.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { canRead } from "./acl"; -import type { KnowledgeAclEntry, KnowledgeActor } from "./types"; - -type SourceChange = { - connectorInstanceId: string; - sourceId: string; - title: string; - canonicalUrl: string; - contentHash: string; - chunks: { position: number; content: string }[]; - acls: KnowledgeAclEntry[]; -}; - -type Citation = { - documentId: string; - title: string; - canonicalUrl: string; - chunkId: string; - content: string; -}; - -export class InMemoryKnowledgeRepository { - #sources = new Map(); - #deleted = new Set(); - - apply(change: SourceChange) { - const key = sourceKey(change.connectorInstanceId, change.sourceId); - this.#sources.set(key, structuredClone(change)); - this.#deleted.delete(key); - } - - delete(connectorInstanceId: string, sourceId: string) { - this.#deleted.add(sourceKey(connectorInstanceId, sourceId)); - } - - documents(): SourceChange[] { - return [...this.#sources.values()].map((source) => structuredClone(source)); - } - - search(actor: KnowledgeActor): Citation[] { - return [...this.#sources.entries()].flatMap(([key, source]) => { - if (this.#deleted.has(key) || !canRead(actor, source.acls)) return []; - const documentId = key; - return source.chunks.map((chunk) => ({ - documentId, - title: source.title, - canonicalUrl: source.canonicalUrl, - chunkId: `${documentId}:${chunk.position}`, - content: chunk.content, - })); - }); - } -} - -function sourceKey(connectorInstanceId: string, sourceId: string) { - return `${connectorInstanceId}:${sourceId}`; -} diff --git a/server/src/knowledge/types.ts b/server/src/knowledge/types.ts deleted file mode 100644 index 5ef9b202..00000000 --- a/server/src/knowledge/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type KnowledgeActor = { - userId: string; - groups: string[]; -}; - -export type KnowledgeAclEntry = { - principal: string; - effect: "allow" | "deny"; -}; diff --git a/server/tests/knowledge-acl.test.ts b/server/tests/knowledge-acl.test.ts deleted file mode 100644 index e7608922..00000000 --- a/server/tests/knowledge-acl.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { canRead } from "../src/knowledge/acl"; - -describe("knowledge ACL evaluation", () => { - test("allows a matching user principal", () => { - expect( - canRead({ userId: "u1", groups: [] }, [ - { principal: "user:u1", effect: "allow" }, - ]), - ).toBe(true); - }); - - test("fails closed for an unmatched or empty ACL", () => { - expect( - canRead({ userId: "u1", groups: ["finance"] }, [ - { principal: "group:engineering", effect: "allow" }, - ]), - ).toBe(false); - expect(canRead({ userId: "u1", groups: [] }, [])).toBe(false); - }); - - test("makes a matching deny override a matching allow", () => { - expect( - canRead({ userId: "u1", groups: ["finance"] }, [ - { principal: "group:finance", effect: "allow" }, - { principal: "user:u1", effect: "deny" }, - ]), - ).toBe(false); - }); -}); diff --git a/server/tests/knowledge-repository.test.ts b/server/tests/knowledge-repository.test.ts deleted file mode 100644 index d3863a1c..00000000 --- a/server/tests/knowledge-repository.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { InMemoryKnowledgeRepository } from "../src/knowledge/repository"; - -const change = { - connectorInstanceId: "connector-1", - sourceId: "source-1", - title: "Finance policy", - canonicalUrl: "https://example.test/policy", - contentHash: "hash-1", - chunks: [{ position: 0, content: "finance content" }], - acls: [{ principal: "group:finance", effect: "allow" as const }], -}; - -describe("knowledge repository", () => { - test("replaces source content idempotently", () => { - const repository = new InMemoryKnowledgeRepository(); - repository.apply(change); - repository.apply({ - ...change, - contentHash: "hash-2", - chunks: [{ position: 0, content: "updated" }], - }); - - expect(repository.documents()).toEqual([ - { - ...change, - contentHash: "hash-2", - chunks: [{ position: 0, content: "updated" }], - }, - ]); - }); - - test("returns citations only to authorized actors and hides deleted sources", () => { - const repository = new InMemoryKnowledgeRepository(); - repository.apply(change); - expect(repository.search({ userId: "u1", groups: ["finance"] })).toEqual([ - { - documentId: "connector-1:source-1", - title: "Finance policy", - canonicalUrl: "https://example.test/policy", - chunkId: "connector-1:source-1:0", - content: "finance content", - }, - ]); - expect(repository.search({ userId: "u2", groups: [] })).toEqual([]); - repository.delete("connector-1", "source-1"); - expect(repository.search({ userId: "u1", groups: ["finance"] })).toEqual( - [], - ); - }); -});