import type { DurableMemoryScope, EmbeddingProvider, SemanticMemoryProvider, } from "@rakazo/adapter-kit"; import type { PrismaClient } from "@rakazo/db"; import type { SemanticMemoryDb } from "./builtin-memory-provider.js"; import { BUILTIN_MEMORY_PROVIDER_ID, builtinMemoryRequiresDeploymentOwner, createBuiltinMemoryEmbeddings, createBuiltinMemoryProvider, prepareBuiltinMemoryConnection, } from "./builtin-memory-provider.js"; import type { EncryptedSecretStore } from "./secrets.js"; import { classifySerenityConnectionSettings, createSerenityProvider, prepareSerenityConnection, SERENITY_PROVIDER_ID, serenityRequiresDeploymentOwner, } from "./serenity-memory-provider.js"; export { MemoryProviderDeploymentOwnerRequiredError } from "./serenity-memory-provider.js"; import { createSupermemoryProvider, decodeLegacySupermemoryCredentials, prepareSupermemoryConnection, SUPERMEMORY_PROVIDER_ID, supermemoryRequiresDeploymentOwner, } from "./supermemory-memory-provider.js"; export interface MemoryProviderConnectionInput { provider: string; settings: Record; credentials: Record; /** When false, Serenity must not probe endpoints that classify as private. */ allowPrivateEndpoint?: boolean; } export interface PreparedMemoryProviderConnection { provider: string; settings: Record; credentials: Record; } export interface ConfiguredMemoryProvider { provider: SemanticMemoryProvider; defaultScope: DurableMemoryScope; /** Embeddings served by the same connection, when the provider has one. */ embeddings?: EmbeddingProvider; } /** What a provider may need beyond its own connection, e.g. the database for built-in memory. */ export interface MemoryProviderDeps { prisma: SemanticMemoryDb; } export interface MemoryProviderResolveRequest { spaceId: string; userId: string; } export function memoryProviderResolveRequest( spaceId: string, userId: string, ): MemoryProviderResolveRequest { return { spaceId, userId }; } export interface MemoryProviderResolver { resolve(request: MemoryProviderResolveRequest): Promise; } async function isSpaceOwner( prisma: Pick, spaceId: string, userId: string, ): Promise { const member = await prisma.spaceMember.findUnique({ where: { spaceId_userId: { spaceId, userId } }, select: { role: true }, }); const roles = member?.role.split(",").map((role) => role.trim()); return roles?.includes("owner") ?? false; } interface MemoryProviderAdapter { requiresDeploymentOwner(settings: Record): boolean; /** Async trust classification (e.g. DNS) without probing credentials. */ classifySettings?(settings: Record): Promise>; prepare( settings: Record, credentials: Record, options?: { allowPrivateEndpoint?: boolean }, ): Promise<{ settings: Record; credentials: Record }>; create( settings: Record, credentials: Record, deps: MemoryProviderDeps, ): SemanticMemoryProvider; embeddings?( settings: Record, credentials: Record, ): EmbeddingProvider; decodeLegacyCredentials?(plaintext: string): Record | null; } const MEMORY_PROVIDER_ADAPTERS: ReadonlyMap = new Map([ [ SUPERMEMORY_PROVIDER_ID, { requiresDeploymentOwner: supermemoryRequiresDeploymentOwner, prepare: prepareSupermemoryConnection, create: createSupermemoryProvider, decodeLegacyCredentials: decodeLegacySupermemoryCredentials, }, ], [ SERENITY_PROVIDER_ID, { requiresDeploymentOwner: serenityRequiresDeploymentOwner, classifySettings: classifySerenityConnectionSettings, prepare: ( settings: Record, credentials: Record, options?: { allowPrivateEndpoint?: boolean }, ) => prepareSerenityConnection(settings, credentials, undefined, options), create: createSerenityProvider, }, ], [ BUILTIN_MEMORY_PROVIDER_ID, { requiresDeploymentOwner: builtinMemoryRequiresDeploymentOwner, prepare: (settings: Record, credentials: Record) => prepareBuiltinMemoryConnection(settings, credentials), create: createBuiltinMemoryProvider, embeddings: createBuiltinMemoryEmbeddings, }, ], ]); function memoryProviderAdapter(provider: string): MemoryProviderAdapter { const adapter = MEMORY_PROVIDER_ADAPTERS.get(provider); if (!adapter) throw new Error(`Unknown memory provider "${provider}".`); return adapter; } /** Adapters classify their settings; callers enforce the deployment trust boundary. */ export function memoryProviderRequiresDeploymentOwner( provider: string, settings: Record, ): boolean { return memoryProviderAdapter(provider).requiresDeploymentOwner(settings); } /** * Run provider-specific trust classification (DNS, etc.) without credentialed probes. * Callers must authorize deployment-owner endpoints before prepare/probe. */ export async function classifyMemoryProviderSettings( provider: string, settings: Record, ): Promise> { const adapter = memoryProviderAdapter(provider); return adapter.classifySettings ? adapter.classifySettings(settings) : settings; } export async function prepareMemoryProviderConnection( input: MemoryProviderConnectionInput, ): Promise { const prepared = await memoryProviderAdapter(input.provider).prepare( input.settings, input.credentials, { allowPrivateEndpoint: input.allowPrivateEndpoint }, ); return { provider: input.provider, ...prepared }; } export function createMemoryProvider( provider: string, settings: Record, credentials: Record, deps: MemoryProviderDeps, ): SemanticMemoryProvider { return memoryProviderAdapter(provider).create(settings, credentials, deps); } export function toStringRecord(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries( Object.entries(value).filter( (entry): entry is [string, string] => typeof entry[1] === "string", ), ); } function decodeCredentials(provider: string, plaintext: string): Record { try { const parsed: unknown = JSON.parse(plaintext); // A keyless connection (e.g. a local embeddings server) stores an empty object. if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return toStringRecord(parsed); } } catch { // Configurations created before the generic provider boundary stored the API key directly. } const legacyCredentials = memoryProviderAdapter(provider).decodeLegacyCredentials?.(plaintext); if (legacyCredentials) return legacyCredentials; throw new Error(`Stored credentials for memory provider "${provider}" are invalid.`); } export class SpaceMemoryProviderResolver implements MemoryProviderResolver { constructor( private readonly prisma: Pick< PrismaClient, "spaceMemoryConfig" | "deploymentSettings" | "spaceMember" | "semanticMemoryItem" >, private readonly secrets: EncryptedSecretStore, ) {} async resolve({ spaceId, userId, }: MemoryProviderResolveRequest): Promise { const config = await this.prisma.spaceMemoryConfig.findUnique({ where: { spaceId }, include: { secret: true }, }); if (!config) return null; const settings = toStringRecord(config.settings); if (memoryProviderRequiresDeploymentOwner(config.provider, settings)) { const deployment = await this.prisma.deploymentSettings.findUnique({ where: { id: "default" }, select: { ownerUserId: true }, }); // Also disable pre-existing local configurations authored outside the deployment boundary. if (!deployment?.ownerUserId || deployment.ownerUserId !== config.userId) return null; } else if (!(await isSpaceOwner(this.prisma, spaceId, userId))) { // Cloud/public providers are owner-managed infrastructure; members must not // load credentials or hit external endpoints through the runtime path. return null; } const credentials = decodeCredentials( config.provider, this.secrets.load(config.secret.ciphertext, config.secret.id), ); const embeddings = memoryProviderAdapter(config.provider).embeddings?.(settings, credentials); return { provider: createMemoryProvider(config.provider, settings, credentials, { prisma: this.prisma, }), defaultScope: config.defaultMemoryScope === "shared" ? "shared" : "isolated", ...(embeddings ? { embeddings } : {}), }; } }