diff --git a/apps/api/src/memory-provider-config.ts b/apps/api/src/memory-provider-config.ts index e493046..f638bbe 100644 --- a/apps/api/src/memory-provider-config.ts +++ b/apps/api/src/memory-provider-config.ts @@ -134,6 +134,15 @@ export async function persistMemoryProviderConfig( return serializeSpaceMemoryConfig(config); } +export async function getMemoryProviderConfig( + deps: Pick, + actor: Actor, +) { + await requireSpaceOwner(deps.prisma, actor); + const config = await findSpaceMemoryConfig(deps.prisma, actor.spaceId); + return config ? serializeSpaceMemoryConfig(config) : null; +} + export async function updateMemoryProviderDefaultScope( deps: MemoryProviderConfigDeps, actor: Actor, diff --git a/apps/api/src/persist-memory-provider-config.test.ts b/apps/api/src/persist-memory-provider-config.test.ts index 484618e..c0a6478 100644 --- a/apps/api/src/persist-memory-provider-config.test.ts +++ b/apps/api/src/persist-memory-provider-config.test.ts @@ -3,6 +3,7 @@ import type { PrismaClient } from "@rakazo/db"; import { describe, expect, it, vi } from "vitest"; import { disconnectMemoryProvider, + getMemoryProviderConfig, persistMemoryProviderConfig, updateMemoryProviderDefaultScope, } from "./memory-provider-config.js"; @@ -366,6 +367,36 @@ describe("disconnectMemoryProvider", () => { }); }); +describe("getMemoryProviderConfig", () => { + it("rejects non-owners before reading provider configuration", async () => { + const { deps, findUnique } = makeDeps({ + existing: { id: "cfg-1", secretId: "secret-existing" }, + spaceOwner: false, + }); + + await expect(getMemoryProviderConfig(deps, actor)).rejects.toMatchObject({ code: "FORBIDDEN" }); + expect(findUnique).not.toHaveBeenCalled(); + }); + + it("returns serialized config for Space owners", async () => { + const config = { + provider: "supermemory", + settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, + defaultMemoryScope: "isolated", + updatedAt: new Date("2026-08-19T00:00:00.000Z"), + }; + const { deps } = makeDeps({ existing: { id: "cfg-1", secretId: "secret-existing" } }); + deps.prisma.spaceMemoryConfig.findUnique = vi.fn().mockResolvedValue(config); + + await expect(getMemoryProviderConfig(deps, actor)).resolves.toEqual({ + provider: "supermemory", + settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, + defaultMemoryScope: "isolated", + updatedAt: "2026-08-19T00:00:00.000Z", + }); + }); +}); + describe("updateMemoryProviderDefaultScope", () => { it("accepts owners with additional Better Auth roles", async () => { const { deps, update } = makeDeps({ diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index b8f1235..ee678fd 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -117,7 +117,6 @@ import { findDefaultModelCredential, findDefaultVoiceCredential, findModelCredential, - findSpaceMemoryConfig, formatMessagingLinkCode, InvalidSpaceNameError, IsolationError, @@ -155,8 +154,8 @@ import { searchIntegrationCatalog } from "./integration-catalog.js"; import { buildMcpUpdateMaterial } from "./mcp-material.js"; import { disconnectMemoryProvider, + getMemoryProviderConfig, persistMemoryProviderConfig, - serializeSpaceMemoryConfig, updateMemoryProviderDefaultScope, } from "./memory-provider-config.js"; import { @@ -2366,10 +2365,9 @@ export function createRouter(deps: RouterDeps) { }); return docs.map((d) => `# ${d.path}\n\n${d.content}`).join("\n\n"); }), - providerConfig: authed.memory.providerConfig.handler(async ({ context }) => { - const config = await findSpaceMemoryConfig(deps.prisma, context.actor.spaceId); - return config ? serializeSpaceMemoryConfig(config) : null; - }), + providerConfig: authed.memory.providerConfig.handler(async ({ context }) => + getMemoryProviderConfig(deps, context.actor), + ), connectProvider: authed.memory.connectProvider.handler(async ({ context, input }) => persistMemoryProviderConfig(deps, context.actor, input), ), diff --git a/packages/testkit/src/authorization.test.ts b/packages/testkit/src/authorization.test.ts index b2c2184..0da7396 100644 --- a/packages/testkit/src/authorization.test.ts +++ b/packages/testkit/src/authorization.test.ts @@ -608,6 +608,65 @@ describeWithDatabase("API authorization and resource isolation", () => { ).not.toBeNull(); }); + it("keeps memory provider configuration readable only by Space owners", async () => { + const owner = await signup(app, `memory-owner-${stamp}@rakazo.test`, "Memory Owner"); + const ownerActor = await rpc(app, owner, "me"); + const shared = await rpc(app, owner, "spaces/create", { name: "Memory shared" }); + const sharedRow = await handles.prisma.space.findUniqueOrThrow({ + where: { id: shared.id }, + select: { organizationId: true }, + }); + const secret = await handles.prisma.secret.create({ + data: { + userId: ownerActor.userId, + spaceId: shared.id, + kind: "memory-provider", + ciphertext: "encrypted-test-key", + }, + }); + await handles.prisma.spaceMemoryConfig.create({ + data: { + spaceId: shared.id, + userId: ownerActor.userId, + provider: "supermemory", + settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, + secretId: secret.id, + defaultMemoryScope: "isolated", + }, + }); + + const member = await signup(app, `memory-member-${stamp}@rakazo.test`, "Memory Member"); + const memberActor = await rpc(app, member, "me"); + await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); + await handles.prisma.member.create({ + data: { + id: `memory-org-member-${stamp}`, + organizationId: sharedRow.organizationId, + userId: memberActor.userId, + role: "member", + createdAt: new Date(), + }, + }); + await handles.prisma.spaceMember.create({ + data: { + id: `memory-space-member-${stamp}`, + spaceId: shared.id, + organizationId: sharedRow.organizationId, + userId: memberActor.userId, + role: "member", + createdAt: new Date(), + }, + }); + + expect(await rpc(app, owner, "memory/providerConfig", {}, shared.id)).toMatchObject({ + provider: "supermemory", + settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, + }); + await expect( + raw(app, member, "memory/providerConfig", {}, shared.id), + ).resolves.toMatchObject({ status: 403 }); + }); + it("keeps space data and computers behind the selected space boundary", async () => { const cookie = await signup(app, `spaces-${stamp}@rakazo.test`, "Space Owner"); const original = await rpc(app, cookie, "me");