{"version":3,"file":"memories.d.ts","sourceRoot":"","sources":["../../src/server/memories.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EACX,iBAAiB,EAIjB,gBAAgB,EAChB,uBAAuB,EACvB,cAAc,EACd,MAAM,uBAAuB,CAAC;AAG/B,MAAM,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3F,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAC7C,eAAO,MAAM,wBAAwB,QAAc,CAAC;AAmMpD,qBAAa,SAAS;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFrB,YACkB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,cAAc,EACjC;IAEE,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAgD9D;IAEK,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEhF;YAEa,eAAe;IA+CvB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAEpG;YAEa,oBAAoB;IAc5B,YAAY,CACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,uBAAuB,CAAC,CAoBlC;IAEK,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,uBAAuB,CAAC,CA8ClC;YAEa,YAAY;YAMZ,2BAA2B;YAI3B,0BAA0B;YAM1B,qBAAqB;YAIrB,+BAA+B;CAW7C","sourcesContent":["/**\n * Dashboard memory API — dreb-only global/project memory editor.\n *\n * Scope ids are derived from the server's current cwd inventory. Clients can\n * select only those ids; absolute target paths never cross the wire as\n * authority. All path handling fails closed and re-checks symlink containment\n * immediately before atomic replacement.\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport type { Stats } from \"node:fs\";\nimport { open, readdir, readFile, realpath, rename, stat, unlink } from \"node:fs/promises\";\nimport { basename, join, resolve, sep } from \"node:path\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { findGitRoot, parseFrontmatter } from \"@dreb/coding-agent\";\nimport type {\n\tMemoryDocumentDto,\n\tMemoryEntryMetadataDto,\n\tMemoryEntrySummaryDto,\n\tMemoryEntryTypeDto,\n\tMemoryListingDto,\n\tMemoryMutationResultDto,\n\tMemoryScopeDto,\n} from \"../shared/protocol.js\";\nimport { canonicalizePath } from \"./files.js\";\n\nexport type MemoryOpLogger = (operation: string, scopeId: string, detail?: string) => void;\n\nexport const MEMORY_INDEX_FILE = \"MEMORY.md\";\nexport const MAX_MEMORY_CONTENT_BYTES = 1024 * 1024;\nconst VALID_ENTRY_TYPES = new Set<MemoryEntryTypeDto>([\"user-preferences\", \"good-practices\", \"project\", \"navigation\"]);\n\nfunction httpError(status: number, message: string, cause?: unknown): Error & { status: number } {\n\treturn Object.assign(new Error(message), { status, ...(cause === undefined ? {} : { cause }) });\n}\n\nfunction sha256Hex(content: string): string {\n\treturn createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\");\n}\n\nfunction projectScopeId(canonicalRoot: string): string {\n\treturn `project-${sha256Hex(canonicalRoot).slice(0, 24)}`;\n}\n\nfunction isWithinCanonicalRoot(target: string, root: string): boolean {\n\tconst normalizedRoot = resolve(root);\n\tconst normalizedTarget = resolve(target);\n\treturn normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}${sep}`);\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n\ttry {\n\t\tawait stat(path);\n\t\treturn true;\n\t} catch (err: any) {\n\t\tif (err?.code === \"ENOENT\") return false;\n\t\tthrow err;\n\t}\n}\n\nasync function canonicalExistingDirectory(path: string): Promise<string | null> {\n\ttry {\n\t\tconst canonical = await realpath(path);\n\t\tconst info = await stat(canonical);\n\t\treturn info.isDirectory() ? canonical : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction assertContentLimit(content: unknown): asserts content is string {\n\tif (typeof content !== \"string\") throw httpError(400, \"content must be a string\");\n\tif (Buffer.byteLength(content, \"utf8\") > MAX_MEMORY_CONTENT_BYTES) {\n\t\tthrow httpError(413, `Memory document exceeds the ${MAX_MEMORY_CONTENT_BYTES} byte limit`);\n\t}\n}\n\nfunction validateEntryFile(file: string): void {\n\tif (typeof file !== \"string\" || file.length === 0) throw httpError(400, \"file is required\");\n\tif (file.includes(\"\\0\") || file.includes(\"/\") || file.includes(\"\\\\\"))\n\t\tthrow httpError(400, `Invalid memory file: ${file}`);\n\tif (file === \".\" || file === \"..\" || file.startsWith(\".\") || file.startsWith(\"_\")) {\n\t\tthrow httpError(400, `Invalid memory file: ${file}`);\n\t}\n\tif (file.toLowerCase() === MEMORY_INDEX_FILE.toLowerCase())\n\t\tthrow httpError(400, \"MEMORY.md is the index, not an entry\");\n\tif (!/^[A-Za-z0-9][A-Za-z0-9._-]*\\.md$/.test(file))\n\t\tthrow httpError(400, `Memory entries must be .md files: ${file}`);\n}\n\nfunction validateDocumentFile(file: string): \"index\" | \"entry\" {\n\tif (file === MEMORY_INDEX_FILE) return \"index\";\n\tvalidateEntryFile(file);\n\treturn \"entry\";\n}\n\nfunction validateMetadata(frontmatter: Record<string, unknown>): MemoryEntryMetadataDto {\n\tconst { name, description, type } = frontmatter;\n\tif (typeof name !== \"string\" || name.length === 0) throw new Error(\"frontmatter.name must be a non-empty string\");\n\tif (typeof description !== \"string\" || description.length === 0) {\n\t\tthrow new Error(\"frontmatter.description must be a non-empty string\");\n\t}\n\tif (typeof type !== \"string\" || !VALID_ENTRY_TYPES.has(type as MemoryEntryTypeDto)) {\n\t\tthrow new Error(\"frontmatter.type must be one of user-preferences, good-practices, project, navigation\");\n\t}\n\treturn { name, description, type: type as MemoryEntryTypeDto };\n}\n\nfunction parseEntryMetadata(content: string): { metadata?: MemoryEntryMetadataDto; metadataError?: string } {\n\ttry {\n\t\tconst { frontmatter } = parseFrontmatter<Record<string, unknown>>(content);\n\t\treturn { metadata: validateMetadata(frontmatter) };\n\t} catch (err) {\n\t\treturn { metadataError: err instanceof Error ? err.message : String(err) };\n\t}\n}\n\nasync function requireLimitedFile(path: string): Promise<Stats> {\n\tconst info = await stat(path);\n\tif (!info.isFile()) throw httpError(400, `Not a file: ${path}`);\n\tif (info.size > MAX_MEMORY_CONTENT_BYTES)\n\t\tthrow httpError(413, `Memory document exceeds the ${MAX_MEMORY_CONTENT_BYTES} byte limit`);\n\treturn info;\n}\n\nasync function readUtf8Limited(path: string): Promise<string> {\n\tawait requireLimitedFile(path);\n\treturn readFile(path, \"utf8\");\n}\n\nasync function readEntryMetadata(path: string): Promise<{\n\tinfo: Stats;\n\tparsed: ReturnType<typeof parseEntryMetadata>;\n}> {\n\tconst info = await requireLimitedFile(path);\n\tconst handle = await open(path, \"r\");\n\tconst decoder = new StringDecoder(\"utf8\");\n\tconst chunk = Buffer.allocUnsafe(16 * 1024);\n\tlet prefix = \"\";\n\tlet position = 0;\n\ttry {\n\t\twhile (position < info.size) {\n\t\t\tconst { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, info.size - position), position);\n\t\t\tif (bytesRead === 0) break;\n\t\t\tposition += bytesRead;\n\t\t\tprefix += decoder.write(chunk.subarray(0, bytesRead));\n\t\t\tif (position === bytesRead && !prefix.startsWith(\"---\")) break;\n\t\t\tconst end = prefix.indexOf(\"\\n---\", 3);\n\t\t\tif (end !== -1) {\n\t\t\t\tprefix = prefix.slice(0, end + 4);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tprefix += decoder.end();\n\t} finally {\n\t\tawait handle.close();\n\t}\n\treturn { info, parsed: parseEntryMetadata(prefix) };\n}\n\nasync function atomicReplace(path: string, content: string): Promise<void> {\n\tconst dir = resolve(path, \"..\");\n\tconst temp = join(dir, `.dreb-memory-${process.pid}-${Date.now()}-${randomBytes(6).toString(\"hex\")}.tmp`);\n\tlet handle: Awaited<ReturnType<typeof open>> | undefined;\n\ttry {\n\t\thandle = await open(temp, \"wx\");\n\t\tawait handle.writeFile(content, \"utf8\");\n\t\tawait handle.close();\n\t\thandle = undefined;\n\t\tawait rename(temp, path);\n\t} catch (err) {\n\t\tif (handle) await handle.close().catch(() => {});\n\t\tawait unlink(temp).catch(() => {});\n\t\tthrow err;\n\t}\n}\n\nfunction splitLinesPreserveEndings(content: string): string[] {\n\tconst matches = content.match(/.*(?:\\r\\n|\\n|\\r|$)/g) ?? [];\n\treturn matches.filter((part, index) => part.length > 0 || index < matches.length - 1);\n}\n\nfunction localMarkdownTargets(line: string): string[] {\n\tconst targets: string[] = [];\n\tconst regex = /\\[[^\\]]+\\]\\(([^)]+)\\)/g;\n\tlet match = regex.exec(line);\n\twhile (match) {\n\t\ttargets.push(match[1]);\n\t\tmatch = regex.exec(line);\n\t}\n\treturn targets;\n}\n\nfunction targetMatchesFilename(target: string, filename: string): boolean {\n\treturn target === filename || target === `./${filename}`;\n}\n\nfunction escapeRegExp(text: string): string {\n\treturn text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction removeIndexLinks(indexContent: string, filename: string): { content: string; changed: boolean } {\n\tconst lines = splitLinesPreserveEndings(indexContent);\n\tconst escaped = escapeRegExp(filename);\n\tconst safeLine = new RegExp(\n\t\t`^\\\\s*[-*+]\\\\s+\\\\[[^\\\\]]+\\\\]\\\\((?:\\\\./)?${escaped}\\\\)(?:\\\\s+(?:[-—:]|—)\\\\s+.*)?\\\\s*(?:\\\\r?\\\\n|\\\\r)?$`,\n\t\t\"u\",\n\t);\n\tlet changed = false;\n\tconst kept: string[] = [];\n\tfor (const line of lines) {\n\t\tconst matches = localMarkdownTargets(line).some((target) => targetMatchesFilename(target, filename));\n\t\tif (!matches) {\n\t\t\tkept.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!safeLine.test(line)) {\n\t\t\tthrow httpError(409, `Index line for ${filename} is not safe to remove automatically`);\n\t\t}\n\t\tchanged = true;\n\t}\n\treturn { content: kept.join(\"\"), changed };\n}\n\nexport class MemoryApi {\n\tconstructor(\n\t\tprivate readonly homeDir: string,\n\t\tprivate readonly log: MemoryOpLogger,\n\t) {}\n\n\tasync scopes(cwdInventory: string[]): Promise<MemoryScopeDto[]> {\n\t\tconst scopes: MemoryScopeDto[] = [];\n\t\tconst canonicalHome = (await canonicalExistingDirectory(this.homeDir)) ?? resolve(this.homeDir);\n\t\tconst globalMemoryDir = resolve(canonicalHome, \".dreb\", \"memory\");\n\t\tscopes.push({\n\t\t\tid: \"global\",\n\t\t\tkind: \"global\",\n\t\t\tlabel: \"global\",\n\t\t\tmemoryDir: globalMemoryDir,\n\t\t\texists: await pathExists(globalMemoryDir),\n\t\t});\n\n\t\tconst roots = new Map<string, string>();\n\t\tfor (const cwd of cwdInventory) {\n\t\t\tif (typeof cwd !== \"string\" || cwd.length === 0) continue;\n\t\t\tconst existingCwd = await canonicalExistingDirectory(cwd);\n\t\t\tif (!existingCwd) continue;\n\t\t\tconst root = findGitRoot(existingCwd) ?? existingCwd;\n\t\t\tconst canonicalRoot = await canonicalExistingDirectory(root);\n\t\t\tif (!canonicalRoot || canonicalRoot === canonicalHome) continue;\n\t\t\troots.set(canonicalRoot, canonicalRoot);\n\t\t}\n\t\tfor (const root of [...roots.keys()].sort((a, b) => a.localeCompare(b))) {\n\t\t\tconst memoryDir = join(root, \".dreb\", \"memory\");\n\t\t\tconst canonicalMemoryDir = await canonicalExistingDirectory(memoryDir);\n\t\t\tif (!canonicalMemoryDir) continue;\n\t\t\tconst dirents = await readdir(canonicalMemoryDir, { withFileTypes: true });\n\t\t\tconst browseable = dirents.some((dirent) => {\n\t\t\t\tif (!dirent.isFile()) return false;\n\t\t\t\tif (dirent.name === MEMORY_INDEX_FILE) return true;\n\t\t\t\ttry {\n\t\t\t\t\tvalidateEntryFile(dirent.name);\n\t\t\t\t\treturn true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (!browseable) continue;\n\t\t\tscopes.push({\n\t\t\t\tid: projectScopeId(root),\n\t\t\t\tkind: \"project\",\n\t\t\t\tlabel: basename(root) || root,\n\t\t\t\tprojectRoot: root,\n\t\t\t\tmemoryDir: canonicalMemoryDir,\n\t\t\t\texists: true,\n\t\t\t});\n\t\t}\n\t\treturn scopes;\n\t}\n\n\tasync listing(scopeId: string, cwdInventory: string[]): Promise<MemoryListingDto> {\n\t\treturn this.listingForScope(await this.requireScope(scopeId, cwdInventory));\n\t}\n\n\tprivate async listingForScope(scope: MemoryScopeDto): Promise<MemoryListingDto> {\n\t\tconst memoryRoot = await this.canonicalMemoryRootIfExists(scope);\n\t\tif (!memoryRoot) {\n\t\t\tthis.log(\"list\", scope.id);\n\t\t\treturn { scope, indexContent: null, indexRevision: null, indexOverLimit: false, entries: [] };\n\t\t}\n\n\t\tlet indexContent: string | null = null;\n\t\tlet indexRevision: string | null = null;\n\t\ttry {\n\t\t\tconst indexPath = await this.resolveExistingTargetWithinRoot(memoryRoot, MEMORY_INDEX_FILE, \"index\");\n\t\t\tindexContent = await readUtf8Limited(indexPath);\n\t\t\tindexRevision = sha256Hex(indexContent);\n\t\t} catch (err: any) {\n\t\t\tif (err?.status !== 404 && err?.code !== \"ENOENT\") throw err;\n\t\t}\n\n\t\tconst dirents = await readdir(memoryRoot, { withFileTypes: true });\n\t\tconst entries: MemoryEntrySummaryDto[] = [];\n\t\tfor (const dirent of dirents) {\n\t\t\tif (!dirent.isFile()) continue;\n\t\t\tif (dirent.name === MEMORY_INDEX_FILE) continue;\n\t\t\ttry {\n\t\t\t\tvalidateEntryFile(dirent.name);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst path = await this.resolveExistingTargetWithinRoot(memoryRoot, dirent.name, \"entry\");\n\t\t\tconst { info, parsed } = await readEntryMetadata(path);\n\t\t\tentries.push({\n\t\t\t\tfile: dirent.name,\n\t\t\t\t...parsed,\n\t\t\t\tmodified: info.mtime.toISOString(),\n\t\t\t\tsize: info.size,\n\t\t\t});\n\t\t}\n\t\tentries.sort((a, b) => a.file.localeCompare(b.file));\n\t\tthis.log(\"list\", scope.id);\n\t\treturn {\n\t\t\tscope: { ...scope, exists: true, memoryDir: memoryRoot },\n\t\t\tindexContent,\n\t\t\tindexRevision,\n\t\t\tindexOverLimit: (indexContent?.split(/\\r\\n|\\n|\\r/).length ?? 0) > 200,\n\t\t\tentries,\n\t\t};\n\t}\n\n\tasync readDocument(scopeId: string, file: string, cwdInventory: string[]): Promise<MemoryDocumentDto> {\n\t\treturn this.readDocumentForScope(await this.requireScope(scopeId, cwdInventory), file);\n\t}\n\n\tprivate async readDocumentForScope(scope: MemoryScopeDto, file: string): Promise<MemoryDocumentDto> {\n\t\tconst kind = validateDocumentFile(file);\n\t\tconst path = await this.resolveExistingTarget(scope, file, kind);\n\t\tconst content = await readUtf8Limited(path);\n\t\tthis.log(\"read\", scope.id, file);\n\t\treturn {\n\t\t\tkind,\n\t\t\tfile,\n\t\t\tcontent,\n\t\t\trevision: sha256Hex(content),\n\t\t\t...(kind === \"entry\" ? parseEntryMetadata(content) : {}),\n\t\t};\n\t}\n\n\tasync saveDocument(\n\t\tscopeId: string,\n\t\tfile: string,\n\t\tbody: unknown,\n\t\tcwdInventory: string[],\n\t): Promise<MemoryMutationResultDto> {\n\t\tconst kind = validateDocumentFile(file);\n\t\tif (!body || typeof body !== \"object\" || Array.isArray(body)) throw httpError(400, \"JSON body is required\");\n\t\tconst { content, revision } = body as Record<string, unknown>;\n\t\tassertContentLimit(content);\n\t\tif (typeof revision !== \"string\" || revision.length === 0) throw httpError(400, \"revision is required\");\n\t\tif (kind === \"entry\") {\n\t\t\tconst parsed = parseEntryMetadata(content);\n\t\t\tif (parsed.metadataError) throw httpError(400, parsed.metadataError);\n\t\t}\n\t\tconst scope = await this.requireScope(scopeId, cwdInventory);\n\t\tconst path = await this.resolveExistingTarget(scope, file, kind);\n\t\tconst current = await readUtf8Limited(path);\n\t\tif (sha256Hex(current) !== revision) throw httpError(409, \"Memory document is stale; refresh before saving\");\n\t\tconst beforeReplace = await this.resolveExistingTarget(scope, file, kind);\n\t\tawait atomicReplace(beforeReplace, content);\n\t\tconst document = await this.readDocumentForScope(scope, file);\n\t\tconst listing = await this.listingForScope(scope);\n\t\tthis.log(\"save\", scope.id, file);\n\t\treturn { listing, document };\n\t}\n\n\tasync deleteEntry(\n\t\tscopeId: string,\n\t\tfile: string,\n\t\tbody: unknown,\n\t\tcwdInventory: string[],\n\t): Promise<MemoryMutationResultDto> {\n\t\tvalidateEntryFile(file);\n\t\tif (!body || typeof body !== \"object\" || Array.isArray(body)) throw httpError(400, \"JSON body is required\");\n\t\tconst { revision, indexRevision } = body as Record<string, unknown>;\n\t\tif (typeof revision !== \"string\" || revision.length === 0) throw httpError(400, \"revision is required\");\n\t\tif (indexRevision !== null && typeof indexRevision !== \"string\")\n\t\t\tthrow httpError(400, \"indexRevision must be a string or null\");\n\t\tconst scope = await this.requireScope(scopeId, cwdInventory);\n\t\tconst entryPath = await this.resolveExistingTarget(scope, file, \"entry\");\n\t\tconst originalEntry = await readUtf8Limited(entryPath);\n\t\tif (sha256Hex(originalEntry) !== revision) throw httpError(409, \"Memory entry is stale; refresh before deleting\");\n\n\t\tconst memoryRoot = await this.requireCanonicalMemoryRoot(scope);\n\t\tlet indexPath = join(memoryRoot, MEMORY_INDEX_FILE);\n\t\tlet originalIndex: string | null = null;\n\t\ttry {\n\t\t\tindexPath = await this.resolveExistingTarget(scope, MEMORY_INDEX_FILE, \"index\");\n\t\t\toriginalIndex = await readUtf8Limited(indexPath);\n\t\t} catch (err: any) {\n\t\t\tif (err?.code !== \"ENOENT\" && err?.status !== 404) throw err;\n\t\t}\n\t\tconst actualIndexRevision = originalIndex === null ? null : sha256Hex(originalIndex);\n\t\tif (actualIndexRevision !== indexRevision) throw httpError(409, \"Memory index is stale; refresh before deleting\");\n\n\t\tlet updatedIndex: string | null = originalIndex;\n\t\tif (originalIndex !== null) updatedIndex = removeIndexLinks(originalIndex, file).content;\n\t\tif (updatedIndex !== null && updatedIndex !== originalIndex) {\n\t\t\tindexPath = await this.resolveExistingTarget(scope, MEMORY_INDEX_FILE, \"index\");\n\t\t\tawait atomicReplace(indexPath, updatedIndex);\n\t\t}\n\t\ttry {\n\t\t\tconst beforeUnlink = await this.resolveExistingTarget(scope, file, \"entry\");\n\t\t\tawait unlink(beforeUnlink);\n\t\t} catch (err) {\n\t\t\tif (originalIndex !== null && updatedIndex !== originalIndex) await atomicReplace(indexPath, originalIndex);\n\t\t\tthrow err;\n\t\t}\n\t\tconst listing = await this.listingForScope(scope);\n\t\tif (\n\t\t\tlisting.indexContent &&\n\t\t\tlocalMarkdownTargets(listing.indexContent).some((target) => targetMatchesFilename(target, file))\n\t\t) {\n\t\t\tthrow httpError(500, `Delete left a dangling index link for ${file}`);\n\t\t}\n\t\tthis.log(\"delete\", scope.id, file);\n\t\treturn { listing };\n\t}\n\n\tprivate async requireScope(scopeId: string, cwdInventory: string[]): Promise<MemoryScopeDto> {\n\t\tconst scope = (await this.scopes(cwdInventory)).find((item) => item.id === scopeId);\n\t\tif (!scope) throw httpError(404, `Unknown memory scope: ${scopeId}`);\n\t\treturn scope;\n\t}\n\n\tprivate async canonicalMemoryRootIfExists(scope: MemoryScopeDto): Promise<string | null> {\n\t\treturn canonicalExistingDirectory(scope.memoryDir);\n\t}\n\n\tprivate async requireCanonicalMemoryRoot(scope: MemoryScopeDto): Promise<string> {\n\t\tconst root = await this.canonicalMemoryRootIfExists(scope);\n\t\tif (!root) throw httpError(404, `Memory directory does not exist: ${scope.memoryDir}`);\n\t\treturn root;\n\t}\n\n\tprivate async resolveExistingTarget(scope: MemoryScopeDto, file: string, kind: \"index\" | \"entry\"): Promise<string> {\n\t\treturn this.resolveExistingTargetWithinRoot(await this.requireCanonicalMemoryRoot(scope), file, kind);\n\t}\n\n\tprivate async resolveExistingTargetWithinRoot(\n\t\tmemoryRoot: string,\n\t\tfile: string,\n\t\tkind: \"index\" | \"entry\",\n\t): Promise<string> {\n\t\tif (kind === \"index\" && file !== MEMORY_INDEX_FILE) throw httpError(400, \"Invalid index file\");\n\t\tif (kind === \"entry\") validateEntryFile(file);\n\t\tconst target = await canonicalizePath(join(memoryRoot, file), { mustExist: true });\n\t\tif (!isWithinCanonicalRoot(target, memoryRoot)) throw httpError(400, `Memory target escapes scope: ${file}`);\n\t\treturn target;\n\t}\n}\n"]}