UNPKG

agentlang

Version:

The easiest way to build the most reliable AI agents - enterprise-grade teams of AI agents that collaborate with each other and humans

849 lines 34.9 kB
import chalk from 'chalk'; import express from 'express'; import * as path from 'path'; import { getAllChildRelationships, getAllEntityNames, getAllEventNames, Instance, isBetweenRelationship, makeInstance, objectAsInstanceAttributes, fetchModule, getModuleNames, Record, fetchRefTarget, getAttributeNames, } from '../runtime/module.js'; import { isNodeEnv } from '../utils/runtime.js'; import { parseAndEvaluateStatement } from '../runtime/interpreter.js'; import { logger } from '../runtime/logger.js'; import { requireAuth, verifySession } from '../runtime/modules/auth.js'; import { BypassSession, isNoSession, NoSession } from '../runtime/auth/defs.js'; import { escapeFqName, forceAsEscapedName, forceAsFqName, isString, isStringNumeric, makeFqName, restoreFqName, nameToPath, walkDownInstancePath, DefaultFileHandlingDirectory, splitRefs, splitFqName, escapeSepInPath, } from '../runtime/util.js'; import { BadRequestError, isPathAttribute, PathAttributeNameQuery, UnauthorisedError, } from '../runtime/defs.js'; import { evaluate } from '../runtime/interpreter.js'; import { findFileByFilename, createFileRecord, deleteFileRecord, } from '../runtime/modules/files.js'; export async function startServer(appSpec, port, host, config) { const app = express(); app.use(express.json()); // Add CORS middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Handle preflight requests if (req.method === 'OPTIONS') { res.sendStatus(200); return; } next(); }); let uploadDir = null; let upload = null; if (isNodeEnv) { const multer = (await import('multer')).default; const fs = await import('fs'); uploadDir = path.join(process.cwd(), DefaultFileHandlingDirectory); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, uploadDir); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const ext = path.extname(file.originalname); const basename = path.basename(file.originalname, ext); cb(null, `${basename}-${uniqueSuffix}${ext}`); }, }); upload = multer({ storage: storage, limits: { fileSize: 1024 * 1024 * 1024, }, }); } const appName = appSpec.name; const appVersion = appSpec.version; app.get('/', (req, res) => { res.send({ agentlang: { application: `${appName}@${appVersion}` } }); }); app.get('/meta', (req, res) => { handleMetaGet(req, res); }); if (isNodeEnv && upload && uploadDir) { app.post('/uploadFile', upload.single('file'), (req, res) => { handleFileUpload(req, res, config); }); app.get('/downloadFile/:filename', (req, res) => { handleFileDownload(req, res, uploadDir, config); }); app.post('/deleteFile/:filename', (req, res) => { handleFileDelete(req, res, uploadDir, config); }); } else { app.post('/uploadFile', (req, res) => { res.status(501).send({ error: 'File upload is only supported in Node.js environment' }); }); app.get('/downloadFile/:filename', (req, res) => { res.status(501).send({ error: 'File download is only supported in Node.js environment' }); }); app.post('/deleteFile/:filename', (req, res) => { res.status(501).send({ error: 'File delete is only supported in Node.js environment' }); }); } getAllEventNames().forEach((eventNames, moduleName) => { const m = fetchModule(moduleName); eventNames.forEach((n) => { if (m.eventIsPublic(n)) app.post(`/${moduleName}/${n}`, (req, res) => { handleEventPost(moduleName, n, req, res); }); }); }); getAllEntityNames().forEach((entityNames, moduleName) => { entityNames.forEach((n) => { app.get(`/${moduleName}/${n}`, (req, res) => { handleEntityGet(moduleName, n, req, res); }); app.get(`/${moduleName}/${n}/*path`, (req, res) => { handleEntityGet(moduleName, n, req, res); }); app.post(`/${moduleName}/${n}`, (req, res) => { handleEntityPost(moduleName, n, req, res); }); app.post(`/${moduleName}/${n}/*path`, (req, res) => { handleEntityPost(moduleName, n, req, res); }); app.put(`/${moduleName}/${n}/*path`, (req, res) => { handleEntityPut(moduleName, n, req, res); }); app.patch(`/${moduleName}/${n}/*path`, (req, res) => { handleEntityPut(moduleName, n, req, res); }); app.delete(`/${moduleName}/${n}/*path`, (req, res) => { handleEntityDelete(moduleName, n, req, res); }); }); }); const cb = () => { console.log(chalk.green(`Application ${chalk.bold(appName + ' version ' + appVersion)} started on port ${chalk.bold(port)}`)); }; if (host) { app.listen(port, host, cb); } else { app.listen(port, cb); } } function ok(res) { return (value) => { const result = normalizedResult(value); res.contentType('application/json'); res.send(JSON.stringify(result)); }; } function statusFromErrorType(err) { if (err instanceof UnauthorisedError) { return 401; } else if (err instanceof BadRequestError) { return 400; } else { return 500; } } function internalError(res) { return (reason) => { logger.error(reason); res.status(statusFromErrorType(reason)).send(reason.message); }; } function patternFromAttributes(moduleName, recName, attrs) { const attrsStrs = new Array(); attrs.forEach((v, n) => { let av = isString(v) ? `"${v}"` : v; if (av instanceof Object) { av = JSON.stringify(av); } if (isPathAttribute(n)) { av = escapeSepInPath(av); } attrsStrs.push(`${n} ${av}`); }); return `{${moduleName}/${recName} { ${attrsStrs.join(',\n')} }}`; } function normalizeRequestPath(path, moduleName) { if (path.length <= 1) { return path; } const result = new Array(); result.push(path[0]); for (let i = 1; i < path.length; ++i) { const rn = forceAsEscapedName(path[i], moduleName); const en = forceAsEscapedName(path[++i], moduleName); result.push(rn); result.push(en); if (i < path.length) { result.push(path[++i]); } } return result; } function pathFromRequest(moduleName, entryName, req) { const path = req.params.path; if (!path) { const url = req.url; const i = url.indexOf('?'); if (i > 0) { return url.substring(0, i); } return url; } let p = ''; if (path instanceof Array) { p = normalizeRequestPath(path, moduleName).join('/'); } else { p = path.toString(); } p = p.trim(); if (p.endsWith('/')) { p = p.substring(0, p.length - 1); } return `${escapeFqName(makeFqName(moduleName, entryName))}/${p}`; } async function handleEventPost(moduleName, eventName, req, res) { try { const sessionInfo = await verifyAuth(moduleName, eventName, req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const inst = makeInstance(moduleName, eventName, objectAsInstanceAttributes(req.body)).setAuthContext(sessionInfo); evaluate(inst, ok(res)).catch(internalError(res)); } catch (err) { logger.error(err); res.status(500).send(err.toString()); } } async function handleEntityPost(moduleName, entityName, req, res) { try { const sessionInfo = await verifyAuth(moduleName, entityName, req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const pattern = req.params.path ? createChildPattern(moduleName, entityName, req) : patternFromAttributes(moduleName, entityName, objectAsInstanceAttributes(req.body)); parseAndEvaluateStatement(pattern, sessionInfo.userId).then(ok(res)).catch(internalError(res)); } catch (err) { logger.error(err); res.status(500).send(err.toString()); } } async function handleEntityGet(moduleName, entityName, req, res) { try { const path = pathFromRequest(moduleName, entityName, req); const sessionInfo = await verifyAuth(moduleName, entityName, req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } let pattern = ''; if (req.query.tree) { pattern = fetchTreePattern(makeFqName(moduleName, entityName), path); } else { pattern = queryPatternFromPath(path, req); } parseAndEvaluateStatement(pattern, sessionInfo.userId).then(ok(res)).catch(internalError(res)); } catch (err) { logger.error(err); res.status(500).send(err.toString()); } } const joinTags = new Map() .set('@joinOn', '@join') .set('@leftJoinOn', '@left_join') .set('@rightJoinOn', '@right_join'); function objectAsAttributesPattern(entityFqName, obj) { const attrs = new Array(); let joinType; let joinOnAttr; Object.keys(obj).forEach(key => { const s = obj[key]; if (joinTags.has(key)) { joinType = joinTags.get(key); joinOnAttr = s; } else { let v = s; if (!s.startsWith('"')) { if (!isStringNumeric(s) && s != 'true' && s != 'false') { v = `"${s}"`; } } attrs.push(`${key}? ${v}`); } }); const hasQueryAttrs = attrs.length > 0; const pat = `{ ${attrs.join(',')} }`; if (joinType && joinOnAttr) { const [targetEntity, targetAttr, reverseJoin] = fetchRefTarget(entityFqName, joinOnAttr); const intoSpec = new Array(); const en1 = splitFqName(entityFqName)[1]; getAttributeNames(entityFqName).forEach((n) => { intoSpec.push(`${en1}_${n} ${entityFqName}.${n}`); }); const en2 = splitFqName(targetEntity)[1]; getAttributeNames(targetEntity).forEach((n) => { intoSpec.push(`${en2}_${n} ${targetEntity}.${n}`); }); const intoPat = `@into {${intoSpec.join(', ')}}`; joinOnAttr = reverseJoin ? splitRefs(joinOnAttr)[1] : joinOnAttr; return [ `${pat},\n${joinType} ${targetEntity} {${targetAttr}? ${entityFqName}.${joinOnAttr}}, \n${intoPat}`, hasQueryAttrs, ]; } else { return [pat, hasQueryAttrs]; } } function queryPatternFromPath(path, req) { const r = walkDownInstancePath(path); let moduleName = r[0]; let entityName = r[1]; const id = r[2]; const parts = r[3]; const fqName = `${moduleName}/${entityName}`; if (parts.length == 2 && id === undefined) { if (req.query && Object.keys(req.query).length > 0) { const [pat, hasQueryAttrs] = objectAsAttributesPattern(fqName, req.query); const n = hasQueryAttrs ? fqName : `${fqName}?`; return `{${n} ${pat}}`; } else { return `{${fqName}? {}}`; } } else { moduleName = restoreFqName(moduleName); const relName = restoreFqName(parts[parts.length - 2]); if (relName && isBetweenRelationship(relName, moduleName)) { const n = restoreFqName(parts[0]); const ns = nameToPath(n); const pe = ns.getEntryName(); const pm = ns.hasModule() ? ns.getModuleName() : moduleName; const p = escapeSepInPath(parts.slice(0, parts.length - 2).join('/')); return `{${pm}/${pe} {${PathAttributeNameQuery} "${p}"}, ${relName} {${moduleName}/${entityName}? {}}}`; } entityName = restoreFqName(entityName); path = escapeSepInPath(path); if (id === undefined) { return `{${moduleName}/${entityName} {${PathAttributeNameQuery}like "${path}%"}}`; } else { return `{${moduleName}/${entityName} {${PathAttributeNameQuery} "${path}"}}`; } } } async function handleEntityPut(moduleName, entityName, req, res) { try { const path = pathFromRequest(moduleName, entityName, req); const sessionInfo = await verifyAuth(moduleName, entityName, req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const attrs = objectAsInstanceAttributes(req.body); attrs.set(PathAttributeNameQuery, path); const r = walkDownInstancePath(path); moduleName = r[0]; entityName = r[1]; const pattern = patternFromAttributes(moduleName, entityName, attrs); parseAndEvaluateStatement(pattern, sessionInfo.userId).then(ok(res)).catch(internalError(res)); } catch (err) { logger.error(err); res.status(500).send(err.toString()); } } async function handleEntityDelete(moduleName, entityName, req, res) { try { const path = pathFromRequest(moduleName, entityName, req); const sessionInfo = await verifyAuth(moduleName, entityName, req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const cmd = req.query.purge == 'true' ? 'purge' : 'delete'; const pattern = `${cmd} ${queryPatternFromPath(path, req)}`; parseAndEvaluateStatement(pattern, sessionInfo.userId).then(ok(res)).catch(internalError(res)); } catch (err) { logger.error(err); res.status(500).send(err.toString()); } } function fetchTreePattern(fqName, path) { let pattern = path ? `{${fqName} {${PathAttributeNameQuery} "${path}"}` : `{${fqName}? {}`; const rels = getAllChildRelationships(fqName); if (rels.length > 0) { const treePats = new Array(); rels.forEach((rel) => { treePats.push(`${rel.getFqName()} ${fetchTreePattern(rel.getChildFqName())}`); }); pattern = pattern.concat(',', treePats.join(',')); } return `${pattern}}`; } function createChildPattern(moduleName, entityName, req) { const path = pathFromRequest(moduleName, entityName, req); try { const parts = path.split('/'); const pinfo = parts.slice(-4); const parentFqname = forceAsFqName(pinfo[0], moduleName); const relName = forceAsFqName(pinfo[2], moduleName); const parentPath = escapeSepInPath(parts.slice(0, parts.length - 2).join('/')); const childFqName = forceAsFqName(pinfo[3], moduleName); const cparts = nameToPath(childFqName); const childModuleName = cparts.getModuleName(); const childName = cparts.getEntryName(); const cp = patternFromAttributes(childModuleName, childName, objectAsInstanceAttributes(req.body)); return `{${parentFqname} {${PathAttributeNameQuery} "${parentPath}"}, ${relName} ${cp}}`; } catch (err) { throw new BadRequestError(err.message); } } async function verifyAuth(moduleName, eventName, authValue) { if (requireAuth(moduleName, eventName)) { if (authValue) { const token = authValue.substring(authValue.indexOf(' ')).trim(); return await verifySession(token); } else { return NoSession; } } return BypassSession; } function normalizedResult(r) { if (r instanceof Array) { return r.map((x) => { return normalizedResult(x); }); } else if (Instance.IsInstance(r)) { r.mergeRelatedInstances(); Array.from(r.attributes.keys()).forEach(k => { const v = r.attributes.get(k); if (v instanceof Array || Instance.IsInstance(v)) { r.attributes.set(k, normalizedResult(v)); } }); return r.asObject(); } else { if (r instanceof Map) { return Object.fromEntries(r.entries()); } return r; } } async function handleMetaGet(req, res) { try { const sessionInfo = await verifyAuth('', '', req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const moduleFilter = req.query.module; const entityFilter = req.query.entity; const eventFilter = req.query.event; const entities = []; const events = []; const entityNames = getAllEntityNames(); const eventNames = getAllEventNames(); // entities // skip entities if eventFilter is provided if (!eventFilter || eventFilter === '') { entityNames.forEach((entityNames, moduleName) => { if (moduleFilter && moduleName !== moduleFilter) { return; } entityNames.forEach((entityName) => { if (entityFilter && !entityName.toLowerCase().includes(entityFilter.toLowerCase())) { return; } try { const module = fetchModule(moduleName); const entity = module.getEntry(entityName); const attributes = []; if (entity instanceof Record && entity.schema) { entity.schema.forEach((attrSpec, attrName) => { let properties = {}; if (attrSpec.properties) { const propsObj = {}; attrSpec.properties.forEach((value, key) => { if (value instanceof Set) { propsObj[key] = Array.from(value); } else { propsObj[key] = value; } }); properties = propsObj; } const attrInfo = { name: attrName, type: attrSpec.type, properties: JSON.stringify(properties), }; attributes.push(attrInfo); }); } const relationships = []; const allModules = getModuleNames(); allModules.forEach((modName) => { const mod = fetchModule(modName); const rels = mod.getRelationshipEntries(); rels.forEach((rel) => { const parentNode = rel.parentNode(); const childNode = rel.childNode(); if (parentNode.path.getModuleName() === moduleName && parentNode.path.getEntryName() === entityName) { relationships.push({ name: rel.name, type: rel.isContains() ? 'contains' : 'between', direction: 'parent', target: childNode.path.asFqName(), cardinality: rel.isOneToOne() ? 'one-to-one' : rel.isOneToMany() ? 'one-to-many' : 'many-to-many', }); } else if (childNode.path.getModuleName() === moduleName && childNode.path.getEntryName() === entityName) { relationships.push({ name: rel.name, type: rel.isContains() ? 'contains' : 'between', direction: 'child', target: parentNode.path.asFqName(), cardinality: rel.isOneToOne() ? 'one-to-one' : rel.isOneToMany() ? 'one-to-many' : 'many-to-many', }); } }); }); const entityInfo = { name: entityName, module: moduleName, fqName: makeFqName(moduleName, entityName), type: 'entity', attributes: attributes, relationships: relationships, meta: entity instanceof Record && entity.meta ? Object.fromEntries(entity.meta) : {}, }; entities.push(entityInfo); } catch (err) { logger.warn(`Could not get detailed info for entity ${moduleName}/${entityName}: ${err.message}`); const entityInfo = { name: entityName, module: moduleName, fqName: makeFqName(moduleName, entityName), type: 'entity', error: 'Could not load detailed information', }; entities.push(entityInfo); } }); }); } // events if (!entityFilter || entityFilter === '') { eventNames.forEach((eventNames, moduleName) => { if (moduleFilter && moduleName !== moduleFilter) { return; } eventNames.forEach((eventName) => { if (eventFilter && !eventName.toLowerCase().includes(eventFilter.toLowerCase())) { return; } try { const module = fetchModule(moduleName); const event = module.getEntry(eventName); const attributes = []; if (event instanceof Record && event.schema) { event.schema.forEach((attrSpec, attrName) => { let properties = {}; if (attrSpec.properties) { const propsObj = {}; attrSpec.properties.forEach((value, key) => { if (value instanceof Set) { propsObj[key] = Array.from(value); } else { propsObj[key] = value; } }); properties = propsObj; } const attrInfo = { name: attrName, type: attrSpec.type, properties: JSON.stringify(properties), }; attributes.push(attrInfo); }); } const eventInfo = { name: eventName, module: moduleName, fqName: makeFqName(moduleName, eventName), type: 'event', attributes: attributes, meta: event instanceof Record && event.meta ? Object.fromEntries(event.meta) : {}, }; events.push(eventInfo); } catch (err) { logger.warn(`Could not get detailed info for event ${moduleName}/${eventName}: ${err.message}`); const eventInfo = { name: eventName, module: moduleName, fqName: makeFqName(moduleName, eventName), type: 'event', error: 'Could not load detailed information', }; events.push(eventInfo); } }); }); } const entitiesByModule = {}; const eventsByModule = {}; entities.forEach(entity => { if (!entitiesByModule[entity.module]) { entitiesByModule[entity.module] = []; } entitiesByModule[entity.module].push(entity); }); events.forEach(event => { if (!eventsByModule[event.module]) { eventsByModule[event.module] = []; } eventsByModule[event.module].push(event); }); const result = { entities: entitiesByModule, events: eventsByModule, modules: Array.from(new Set([...entities.map(e => e.module), ...events.map(e => e.module)])), }; res.contentType('application/json'); res.send(result); } catch (err) { logger.error(err); res.status(500).send(err.toString()); } } async function handleFileUpload(req, res, config) { var _a; try { if (!isNodeEnv) { res.status(501).send({ error: 'File upload is only supported in Node.js environment' }); return; } if (!((_a = config === null || config === void 0 ? void 0 : config.service) === null || _a === void 0 ? void 0 : _a.httpFileHandling)) { res .status(403) .send({ error: 'File handling is not enabled. Set httpFileHandling: true in config.' }); return; } const sessionInfo = await verifyAuth('', '', req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } if (!req.file) { res.status(400).send({ error: 'No file uploaded' }); return; } const file = req.file; try { await createFileRecord({ filename: file.filename, originalName: file.originalname, mimetype: file.mimetype, size: file.size, path: file.path, uploadedBy: sessionInfo.userId, }, sessionInfo); } catch (dbErr) { logger.error(`Failed to create file record in database: ${dbErr.message}`); } const fileInfo = { success: true, filename: file.filename, originalName: file.originalname, mimetype: file.mimetype, size: file.size, path: file.path, uploadedAt: new Date().toISOString(), uploadedBy: sessionInfo.userId, }; logger.info(`File uploaded successfully: ${file.originalname} -> ${file.filename}`); res.contentType('application/json'); res.send(fileInfo); } catch (err) { logger.error(`File upload error: ${err}`); res.status(500).send({ error: err.message || 'File upload failed' }); } } async function handleFileDownload(req, res, uploadDir, config) { var _a; try { if (!isNodeEnv) { res.status(501).send({ error: 'File download is only supported in Node.js environment' }); return; } if (!((_a = config === null || config === void 0 ? void 0 : config.service) === null || _a === void 0 ? void 0 : _a.httpFileHandling)) { res .status(403) .send({ error: 'File handling is not enabled. Set httpFileHandling: true in config.' }); return; } const sessionInfo = await verifyAuth('', '', req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const filename = req.params.filename; if (!filename) { res.status(400).send({ error: 'Filename is required' }); return; } const file = await findFileByFilename(filename, sessionInfo); if (!file) { res.status(404).send({ error: 'File not found' }); return; } const sanitizedFilename = path.basename(filename); const fs = await import('fs'); const filePath = path.join(uploadDir, sanitizedFilename); if (!fs.existsSync(filePath)) { res.status(404).send({ error: 'File not found' }); return; } const realPath = fs.realpathSync(filePath); const realUploadDir = fs.realpathSync(uploadDir); if (!realPath.startsWith(realUploadDir)) { res.status(403).send({ error: 'Access denied' }); return; } const stats = fs.statSync(filePath); const ext = path.extname(sanitizedFilename).toLowerCase(); const mimeTypes = { '.pdf': 'application/pdf', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.txt': 'text/plain', '.json': 'application/json', '.xml': 'application/xml', '.zip': 'application/zip', '.csv': 'text/csv', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }; const mimeType = mimeTypes[ext] || 'application/octet-stream'; res.setHeader('Content-Type', mimeType); res.setHeader('Content-Length', stats.size); res.setHeader('Content-Disposition', `attachment; filename="${sanitizedFilename}"`); res.setHeader('Cache-Control', 'no-cache'); logger.info(`File download: ${sanitizedFilename}`); const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); fileStream.on('error', (err) => { logger.error(`File stream error: ${err}`); if (!res.headersSent) { res.status(500).send({ error: 'Error streaming file' }); } }); } catch (err) { logger.error(`File download error: ${err}`); if (!res.headersSent) { res.status(500).send({ error: err.message || 'File download failed' }); } } } async function handleFileDelete(req, res, uploadDir, config) { var _a; try { if (!isNodeEnv) { res.status(501).send({ error: 'File delete is only supported in Node.js environment' }); return; } if (!((_a = config === null || config === void 0 ? void 0 : config.service) === null || _a === void 0 ? void 0 : _a.httpFileHandling)) { res .status(403) .send({ error: 'File handling is not enabled. Set httpFileHandling: true in config.' }); return; } const sessionInfo = await verifyAuth('', '', req.headers.authorization); if (isNoSession(sessionInfo)) { res.status(401).send('Authorization required'); return; } const filename = req.params.filename; if (!filename) { res.status(400).send({ error: 'Filename is required' }); return; } const file = await findFileByFilename(filename, sessionInfo); if (!file) { res.status(404).send({ error: 'File not found' }); return; } const sanitizedFilename = path.basename(filename); const fs = await import('fs'); const filePath = path.join(uploadDir, sanitizedFilename); if (!fs.existsSync(filePath)) { res.status(404).send({ error: 'File not found' }); return; } const realPath = fs.realpathSync(filePath); const realUploadDir = fs.realpathSync(uploadDir); if (!realPath.startsWith(realUploadDir)) { res.status(403).send({ error: 'Access denied' }); return; } await deleteFileRecord(filename, sessionInfo); fs.unlinkSync(filePath); logger.info(`File deleted: ${sanitizedFilename}`); res.status(200).send({ message: 'File deleted successfully', filename: sanitizedFilename, }); } catch (err) { logger.error(`File delete error: ${err}`); if (!res.headersSent) { res.status(500).send({ error: err.message || 'File delete failed' }); } } } //# sourceMappingURL=http.js.map