diff --git a/frontends/nextjs/src/lib/db/functions/app-config/get-app-config.ts b/frontends/nextjs/src/lib/db/functions/app-config/get-app-config.ts new file mode 100644 index 000000000..a2bdcd5ff --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/app-config/get-app-config.ts @@ -0,0 +1,26 @@ +/** + * Get App Config + * Retrieves the application configuration from database + */ + +import { prisma } from '../../prisma' +import type { AppConfiguration } from '../../../types/level-types' + +/** + * Get the application configuration + * @returns AppConfiguration or null if not found + */ +export const getAppConfig = async (): Promise => { + const config = await prisma.appConfiguration.findFirst() + if (!config) return null + + return { + id: config.id, + name: config.name, + schemas: JSON.parse(config.schemas), + workflows: JSON.parse(config.workflows), + luaScripts: JSON.parse(config.luaScripts), + pages: JSON.parse(config.pages), + theme: JSON.parse(config.theme), + } +} diff --git a/frontends/nextjs/src/lib/db/functions/app-config/index.ts b/frontends/nextjs/src/lib/db/functions/app-config/index.ts new file mode 100644 index 000000000..da1014987 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/app-config/index.ts @@ -0,0 +1,7 @@ +/** + * App Config Index + * Exports all app configuration functions + */ + +export { getAppConfig } from './get-app-config' +export { setAppConfig } from './set-app-config' diff --git a/frontends/nextjs/src/lib/db/functions/app-config/set-app-config.ts b/frontends/nextjs/src/lib/db/functions/app-config/set-app-config.ts new file mode 100644 index 000000000..318ec645c --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/app-config/set-app-config.ts @@ -0,0 +1,26 @@ +/** + * Set App Config + * Saves the application configuration to database + */ + +import { prisma } from '../../prisma' +import type { AppConfiguration } from '../../../types/level-types' + +/** + * Set the application configuration + * @param config - The configuration to save + */ +export const setAppConfig = async (config: AppConfiguration): Promise => { + await prisma.appConfiguration.deleteMany() + await prisma.appConfiguration.create({ + data: { + id: config.id, + name: config.name, + schemas: JSON.stringify(config.schemas), + workflows: JSON.stringify(config.workflows), + luaScripts: JSON.stringify(config.luaScripts), + pages: JSON.stringify(config.pages), + theme: JSON.stringify(config.theme), + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/functions/comments/add-comment.ts b/frontends/nextjs/src/lib/db/functions/comments/add-comment.ts new file mode 100644 index 000000000..504f4bf2f --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/comments/add-comment.ts @@ -0,0 +1,24 @@ +/** + * Add Comment + * Adds a new comment to database + */ + +import { prisma } from '../../prisma' +import type { Comment } from '../../../types/level-types' + +/** + * Add a new comment + * @param comment - Comment to add + */ +export const addComment = async (comment: Comment): Promise => { + await prisma.comment.create({ + data: { + id: comment.id, + userId: comment.userId, + content: comment.content, + createdAt: BigInt(comment.createdAt), + updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null, + parentId: comment.parentId, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/functions/comments/delete-comment.ts b/frontends/nextjs/src/lib/db/functions/comments/delete-comment.ts new file mode 100644 index 000000000..d764baa45 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/comments/delete-comment.ts @@ -0,0 +1,14 @@ +/** + * Delete Comment + * Deletes a comment from database + */ + +import { prisma } from '../../prisma' + +/** + * Delete a comment + * @param commentId - ID of comment to delete + */ +export const deleteComment = async (commentId: string): Promise => { + await prisma.comment.delete({ where: { id: commentId } }) +} diff --git a/frontends/nextjs/src/lib/db/functions/comments/get-comments.ts b/frontends/nextjs/src/lib/db/functions/comments/get-comments.ts new file mode 100644 index 000000000..81053dff2 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/comments/get-comments.ts @@ -0,0 +1,23 @@ +/** + * Get Comments + * Retrieves all comments from database + */ + +import { prisma } from '../../prisma' +import type { Comment } from '../../../types/level-types' + +/** + * Get all comments + * @returns Array of comments + */ +export const getComments = async (): Promise => { + const comments = await prisma.comment.findMany() + return comments.map(c => ({ + id: c.id, + userId: c.userId, + content: c.content, + createdAt: Number(c.createdAt), + updatedAt: c.updatedAt ? Number(c.updatedAt) : undefined, + parentId: c.parentId || undefined, + })) +} diff --git a/frontends/nextjs/src/lib/db/functions/comments/index.ts b/frontends/nextjs/src/lib/db/functions/comments/index.ts new file mode 100644 index 000000000..3ad63dea5 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/comments/index.ts @@ -0,0 +1,10 @@ +/** + * Comments Index + * Exports all comment functions + */ + +export { getComments } from './get-comments' +export { setComments } from './set-comments' +export { addComment } from './add-comment' +export { updateComment } from './update-comment' +export { deleteComment } from './delete-comment' diff --git a/frontends/nextjs/src/lib/db/functions/comments/set-comments.ts b/frontends/nextjs/src/lib/db/functions/comments/set-comments.ts new file mode 100644 index 000000000..885248891 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/comments/set-comments.ts @@ -0,0 +1,27 @@ +/** + * Set Comments + * Replaces all comments in database + */ + +import { prisma } from '../../prisma' +import type { Comment } from '../../../types/level-types' + +/** + * Set all comments (replaces existing) + * @param comments - Array of comments to save + */ +export const setComments = async (comments: Comment[]): Promise => { + await prisma.comment.deleteMany() + for (const comment of comments) { + await prisma.comment.create({ + data: { + id: comment.id, + userId: comment.userId, + content: comment.content, + createdAt: BigInt(comment.createdAt), + updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null, + parentId: comment.parentId, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/functions/comments/update-comment.ts b/frontends/nextjs/src/lib/db/functions/comments/update-comment.ts new file mode 100644 index 000000000..7d6479f55 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/comments/update-comment.ts @@ -0,0 +1,23 @@ +/** + * Update Comment + * Updates an existing comment + */ + +import { prisma } from '../../prisma' +import type { Comment } from '../../../types/level-types' + +/** + * Update a comment + * @param commentId - ID of comment to update + * @param updates - Partial comment with updates + */ +export const updateComment = async (commentId: string, updates: Partial): Promise => { + const data: any = {} + if (updates.content !== undefined) data.content = updates.content + if (updates.updatedAt !== undefined) data.updatedAt = BigInt(updates.updatedAt) + + await prisma.comment.update({ + where: { id: commentId }, + data, + }) +} diff --git a/frontends/nextjs/src/lib/db/functions/components/add-component-node.ts b/frontends/nextjs/src/lib/db/functions/components/add-component-node.ts new file mode 100644 index 000000000..042ebf23f --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/add-component-node.ts @@ -0,0 +1,24 @@ +/** + * Add Component Node + * Adds a new component node to hierarchy + */ + +import { prisma } from '../../prisma' +import type { ComponentNode } from './types' + +/** + * Add a component node + * @param node - Component node to add + */ +export const addComponentNode = async (node: ComponentNode): Promise => { + await prisma.componentNode.create({ + data: { + id: node.id, + type: node.type, + parentId: node.parentId, + childIds: JSON.stringify(node.childIds), + order: node.order, + pageId: node.pageId, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/functions/components/delete-component-node.ts b/frontends/nextjs/src/lib/db/functions/components/delete-component-node.ts new file mode 100644 index 000000000..886fe3ca5 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/delete-component-node.ts @@ -0,0 +1,14 @@ +/** + * Delete Component Node + * Deletes a component node from hierarchy + */ + +import { prisma } from '../../prisma' + +/** + * Delete a component node + * @param nodeId - ID of node to delete + */ +export const deleteComponentNode = async (nodeId: string): Promise => { + await prisma.componentNode.delete({ where: { id: nodeId } }) +} diff --git a/frontends/nextjs/src/lib/db/functions/components/get-component-configs.ts b/frontends/nextjs/src/lib/db/functions/components/get-component-configs.ts new file mode 100644 index 000000000..505df3175 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/get-component-configs.ts @@ -0,0 +1,27 @@ +/** + * Get Component Configs + * Retrieves component configurations from database + */ + +import { prisma } from '../../prisma' +import type { ComponentConfig } from './types' + +/** + * Get all component configs + * @returns Record of component configs by ID + */ +export const getComponentConfigs = async (): Promise> => { + const configs = await prisma.componentConfig.findMany() + const result: Record = {} + for (const config of configs) { + result[config.id] = { + id: config.id, + componentId: config.componentId, + props: JSON.parse(config.props), + styles: JSON.parse(config.styles), + events: JSON.parse(config.events), + conditionalRendering: config.conditionalRendering ? JSON.parse(config.conditionalRendering) : undefined, + } + } + return result +} diff --git a/frontends/nextjs/src/lib/db/functions/components/get-component-hierarchy.ts b/frontends/nextjs/src/lib/db/functions/components/get-component-hierarchy.ts new file mode 100644 index 000000000..ec2f72534 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/get-component-hierarchy.ts @@ -0,0 +1,27 @@ +/** + * Get Component Hierarchy + * Retrieves component hierarchy from database + */ + +import { prisma } from '../../prisma' +import type { ComponentNode } from './types' + +/** + * Get the component hierarchy + * @returns Record of component nodes by ID + */ +export const getComponentHierarchy = async (): Promise> => { + const nodes = await prisma.componentNode.findMany() + const result: Record = {} + for (const node of nodes) { + result[node.id] = { + id: node.id, + type: node.type, + parentId: node.parentId || undefined, + childIds: JSON.parse(node.childIds), + order: node.order, + pageId: node.pageId, + } + } + return result +} diff --git a/frontends/nextjs/src/lib/db/functions/components/set-component-hierarchy.ts b/frontends/nextjs/src/lib/db/functions/components/set-component-hierarchy.ts new file mode 100644 index 000000000..9f0d87c33 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/set-component-hierarchy.ts @@ -0,0 +1,27 @@ +/** + * Set Component Hierarchy + * Replaces all component hierarchy in database + */ + +import { prisma } from '../../prisma' +import type { ComponentNode } from './types' + +/** + * Set the component hierarchy (replaces existing) + * @param hierarchy - Record of component nodes by ID + */ +export const setComponentHierarchy = async (hierarchy: Record): Promise => { + await prisma.componentNode.deleteMany() + for (const node of Object.values(hierarchy)) { + await prisma.componentNode.create({ + data: { + id: node.id, + type: node.type, + parentId: node.parentId, + childIds: JSON.stringify(node.childIds), + order: node.order, + pageId: node.pageId, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/functions/components/types.ts b/frontends/nextjs/src/lib/db/functions/components/types.ts new file mode 100644 index 000000000..cbc4e329b --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/types.ts @@ -0,0 +1,25 @@ +/** + * Component Types + * Shared types for component hierarchy and config + */ + +export interface ComponentNode { + id: string + type: string + parentId?: string + childIds: string[] + order: number + pageId: string +} + +export interface ComponentConfig { + id: string + componentId: string + props: Record + styles: Record + events: Record + conditionalRendering?: { + condition: string + luaScriptId?: string + } +} diff --git a/frontends/nextjs/src/lib/db/functions/components/update-component-node.ts b/frontends/nextjs/src/lib/db/functions/components/update-component-node.ts new file mode 100644 index 000000000..4a7f78182 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/components/update-component-node.ts @@ -0,0 +1,26 @@ +/** + * Update Component Node + * Updates an existing component node + */ + +import { prisma } from '../../prisma' +import type { ComponentNode } from './types' + +/** + * Update a component node + * @param nodeId - ID of node to update + * @param updates - Partial node with updates + */ +export const updateComponentNode = async (nodeId: string, updates: Partial): Promise => { + const data: any = {} + if (updates.type !== undefined) data.type = updates.type + if (updates.parentId !== undefined) data.parentId = updates.parentId + if (updates.childIds !== undefined) data.childIds = JSON.stringify(updates.childIds) + if (updates.order !== undefined) data.order = updates.order + if (updates.pageId !== undefined) data.pageId = updates.pageId + + await prisma.componentNode.update({ + where: { id: nodeId }, + data, + }) +} diff --git a/frontends/nextjs/src/lib/lua/LuaEngine.ts b/frontends/nextjs/src/lib/lua/LuaEngine.ts new file mode 100644 index 000000000..69c36158e --- /dev/null +++ b/frontends/nextjs/src/lib/lua/LuaEngine.ts @@ -0,0 +1,77 @@ +/** + * LuaEngine - Class wrapper for Lua execution operations + * + * This class serves as a container for lambda functions related to Lua execution. + * Each method delegates to an individual function file in the functions/ directory. + * + * Pattern: "class is container for lambdas" + * - Each lambda is defined in its own file under functions/ + * - This class wraps them for convenient stateful Lua execution + * - Maintains Lua state across multiple executions + */ + +import * as fengari from 'fengari-web' +import type { LuaExecutionContext, LuaExecutionResult } from './functions/types' +import { setupContextAPI } from './functions/setup/setup-context-api' +import { executeLuaCode } from './functions/execution/execute-lua-code' + +const lua = fengari.lua +const lauxlib = fengari.lauxlib +const lualib = fengari.lualib + +// Re-export types +export type { LuaExecutionContext, LuaExecutionResult } + +/** + * LuaEngine class wraps individual Lua execution lambdas + */ +export class LuaEngine { + private L: any + private logs: string[] = [] + + constructor() { + this.L = lauxlib.luaL_newstate() + lualib.luaL_openlibs(this.L) + setupContextAPI(this.L, this.logs) + } + + /** + * Execute Lua code with a context + * @param code - Lua code to execute + * @param context - Execution context + * @returns Execution result + */ + async execute(code: string, context: LuaExecutionContext = {}): Promise { + this.logs = [] + return executeLuaCode(this.L, code, context, this.logs) + } + + /** + * Destroy the Lua state + */ + destroy(): void { + if (this.L) { + lua.lua_close(this.L) + } + } +} + +/** + * Factory function to create a new LuaEngine instance + * @returns New LuaEngine instance + */ +export const createLuaEngine = (): LuaEngine => { + return new LuaEngine() +} + +// Re-export individual functions for direct imports +export { + setupContextAPI, + executeLuaCode +} from './functions' + +export { + pushToLua, + fromLua, + tableToJS +} from './functions/converters' diff --git a/frontends/nextjs/src/lib/lua/functions/converters/from-lua.ts b/frontends/nextjs/src/lib/lua/functions/converters/from-lua.ts new file mode 100644 index 000000000..e88dbc5c6 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/converters/from-lua.ts @@ -0,0 +1,34 @@ +/** + * From Lua + * Converts Lua stack values to JavaScript values + */ + +import * as fengari from 'fengari-web' +import { tableToJS } from './table-to-js' + +const lua = fengari.lua + +/** + * Convert a Lua stack value to JavaScript + * @param L - Lua state + * @param index - Stack index (default: -1 for top of stack) + * @returns JavaScript value + */ +export const fromLua = (L: any, index: number = -1): any => { + const type = lua.lua_type(L, index) + + switch (type) { + case lua.LUA_TNIL: + return null + case lua.LUA_TBOOLEAN: + return lua.lua_toboolean(L, index) + case lua.LUA_TNUMBER: + return lua.lua_tonumber(L, index) + case lua.LUA_TSTRING: + return lua.lua_tojsstring(L, index) + case lua.LUA_TTABLE: + return tableToJS(L, index) + default: + return null + } +} diff --git a/frontends/nextjs/src/lib/lua/functions/converters/index.ts b/frontends/nextjs/src/lib/lua/functions/converters/index.ts new file mode 100644 index 000000000..a282a138a --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/converters/index.ts @@ -0,0 +1,8 @@ +/** + * Converters Index + * Exports all Lua/JS conversion functions + */ + +export { pushToLua } from './push-to-lua' +export { fromLua } from './from-lua' +export { tableToJS } from './table-to-js' diff --git a/frontends/nextjs/src/lib/lua/functions/converters/push-to-lua.ts b/frontends/nextjs/src/lib/lua/functions/converters/push-to-lua.ts new file mode 100644 index 000000000..d03eb6b68 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/converters/push-to-lua.ts @@ -0,0 +1,41 @@ +/** + * Push to Lua + * Converts JavaScript values to Lua stack values + */ + +import * as fengari from 'fengari-web' + +const lua = fengari.lua + +/** + * Push a JavaScript value onto the Lua stack + * @param L - Lua state + * @param value - JavaScript value to push + */ +export const pushToLua = (L: any, value: any): void => { + if (value === null || value === undefined) { + lua.lua_pushnil(L) + } else if (typeof value === 'boolean') { + lua.lua_pushboolean(L, value) + } else if (typeof value === 'number') { + lua.lua_pushnumber(L, value) + } else if (typeof value === 'string') { + lua.lua_pushstring(L, fengari.to_luastring(value)) + } else if (Array.isArray(value)) { + lua.lua_createtable(L, value.length, 0) + value.forEach((item, index) => { + lua.lua_pushinteger(L, index + 1) + pushToLua(L, item) + lua.lua_settable(L, -3) + }) + } else if (typeof value === 'object') { + lua.lua_createtable(L, 0, Object.keys(value).length) + for (const [key, val] of Object.entries(value)) { + lua.lua_pushstring(L, fengari.to_luastring(key)) + pushToLua(L, val) + lua.lua_settable(L, -3) + } + } else { + lua.lua_pushnil(L) + } +} diff --git a/frontends/nextjs/src/lib/lua/functions/converters/table-to-js.ts b/frontends/nextjs/src/lib/lua/functions/converters/table-to-js.ts new file mode 100644 index 000000000..10c9b9498 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/converters/table-to-js.ts @@ -0,0 +1,70 @@ +/** + * Table to JS + * Converts Lua tables to JavaScript arrays/objects + */ + +import * as fengari from 'fengari-web' + +const lua = fengari.lua + +/** + * Convert a Lua table to JavaScript array or object + * @param L - Lua state + * @param index - Stack index of the table + * @returns JavaScript array or object + */ +export const tableToJS = (L: any, index: number): any => { + const result: any = {} + let isArray = true + let arrayIndex = 1 + + lua.lua_pushnil(L) + + while (lua.lua_next(L, index < 0 ? index - 1 : index) !== 0) { + const keyType = lua.lua_type(L, -2) + + if (keyType === lua.LUA_TNUMBER) { + const key = lua.lua_tonumber(L, -2) + if (key !== arrayIndex) { + isArray = false + } + arrayIndex++ + } else { + isArray = false + } + + const key = fromLuaValue(L, -2) + const value = fromLuaValue(L, -1) + result[key] = value + + lua.lua_pop(L, 1) + } + + if (isArray && arrayIndex > 1) { + return Object.values(result) + } + + return result +} + +/** + * Helper to convert single Lua value (avoids circular dependency) + */ +const fromLuaValue = (L: any, index: number): any => { + const type = lua.lua_type(L, index) + + switch (type) { + case lua.LUA_TNIL: + return null + case lua.LUA_TBOOLEAN: + return lua.lua_toboolean(L, index) + case lua.LUA_TNUMBER: + return lua.lua_tonumber(L, index) + case lua.LUA_TSTRING: + return lua.lua_tojsstring(L, index) + case lua.LUA_TTABLE: + return tableToJS(L, index) + default: + return null + } +} diff --git a/frontends/nextjs/src/lib/lua/functions/execution/execute-lua-code.ts b/frontends/nextjs/src/lib/lua/functions/execution/execute-lua-code.ts new file mode 100644 index 000000000..ae71e1ffe --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/execution/execute-lua-code.ts @@ -0,0 +1,108 @@ +/** + * Execute Lua Code + * Executes Lua code with a given context + */ + +import * as fengari from 'fengari-web' +import type { LuaExecutionContext, LuaExecutionResult } from '../types' +import { pushToLua } from '../converters/push-to-lua' +import { fromLua } from '../converters/from-lua' + +const lua = fengari.lua +const lauxlib = fengari.lauxlib + +/** + * Execute Lua code with a context + * @param L - Lua state + * @param code - Lua code to execute + * @param context - Execution context with data, user, kv + * @param logs - Array to collect log messages + * @returns Execution result + */ +export const executeLuaCode = async ( + L: any, + code: string, + context: LuaExecutionContext, + logs: string[] +): Promise => { + try { + // Create context table + lua.lua_createtable(L, 0, 3) + + if (context.data !== undefined) { + lua.lua_pushstring(L, fengari.to_luastring('data')) + pushToLua(L, context.data) + lua.lua_settable(L, -3) + } + + if (context.user !== undefined) { + lua.lua_pushstring(L, fengari.to_luastring('user')) + pushToLua(L, context.user) + lua.lua_settable(L, -3) + } + + const kvMethods: any = {} + if (context.kv) { + kvMethods.get = context.kv.get + kvMethods.set = context.kv.set + } + lua.lua_pushstring(L, fengari.to_luastring('kv')) + pushToLua(L, kvMethods) + lua.lua_settable(L, -3) + + lua.lua_setglobal(L, fengari.to_luastring('context')) + + // Load and execute code + const loadResult = lauxlib.luaL_loadstring(L, fengari.to_luastring(code)) + + if (loadResult !== lua.LUA_OK) { + const errorMsg = lua.lua_tojsstring(L, -1) + lua.lua_pop(L, 1) + return { + success: false, + error: `Syntax error: ${errorMsg}`, + logs, + } + } + + const execResult = lua.lua_pcall(L, 0, lua.LUA_MULTRET, 0) + + if (execResult !== lua.LUA_OK) { + const errorMsg = lua.lua_tojsstring(L, -1) + lua.lua_pop(L, 1) + return { + success: false, + error: `Runtime error: ${errorMsg}`, + logs, + } + } + + // Get results + const nresults = lua.lua_gettop(L) + let result: any = null + + if (nresults > 0) { + if (nresults === 1) { + result = fromLua(L, -1) + } else { + result = [] + for (let i = 1; i <= nresults; i++) { + result.push(fromLua(L, -nresults + i - 1)) + } + } + lua.lua_pop(L, nresults) + } + + return { + success: true, + result, + logs, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + logs, + } + } +} diff --git a/frontends/nextjs/src/lib/lua/functions/execution/index.ts b/frontends/nextjs/src/lib/lua/functions/execution/index.ts new file mode 100644 index 000000000..422a0e967 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/execution/index.ts @@ -0,0 +1,6 @@ +/** + * Execution Index + * Exports all execution functions + */ + +export { executeLuaCode } from './execute-lua-code' diff --git a/frontends/nextjs/src/lib/lua/functions/index.ts b/frontends/nextjs/src/lib/lua/functions/index.ts index 836b014a6..416f297e0 100644 --- a/frontends/nextjs/src/lib/lua/functions/index.ts +++ b/frontends/nextjs/src/lib/lua/functions/index.ts @@ -1,3 +1,23 @@ +/** + * Lua Functions Index + * Exports all Lua engine functions and snippet utilities + */ + +// Types +export type { LuaExecutionContext, LuaExecutionResult } from './types' + +// Converters +export { pushToLua } from './converters/push-to-lua' +export { fromLua } from './converters/from-lua' +export { tableToJS } from './converters/table-to-js' + +// Setup +export { setupContextAPI } from './setup/setup-context-api' + +// Execution +export { executeLuaCode } from './execution/execute-lua-code' + +// Snippet utilities export { getSnippetsByCategory } from './get-snippets-by-category' export { searchSnippets } from './search-snippets' export { getSnippetById } from './get-snippet-by-id' diff --git a/frontends/nextjs/src/lib/lua/functions/setup/index.ts b/frontends/nextjs/src/lib/lua/functions/setup/index.ts new file mode 100644 index 000000000..57a2bed4d --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/setup/index.ts @@ -0,0 +1,6 @@ +/** + * Setup Index + * Exports all setup functions + */ + +export { setupContextAPI } from './setup-context-api' diff --git a/frontends/nextjs/src/lib/lua/functions/setup/setup-context-api.ts b/frontends/nextjs/src/lib/lua/functions/setup/setup-context-api.ts new file mode 100644 index 000000000..f26048dbe --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/setup/setup-context-api.ts @@ -0,0 +1,63 @@ +/** + * Setup Context API + * Sets up the Lua execution context with log/print functions + */ + +import * as fengari from 'fengari-web' + +const lua = fengari.lua + +/** + * Setup the context API functions (log, print) in Lua state + * @param L - Lua state + * @param logs - Array to collect log messages + */ +export const setupContextAPI = (L: any, logs: string[]): void => { + // Create log function + const logFunction = function(LState: any) { + const nargs = lua.lua_gettop(LState) + const messages: string[] = [] + + for (let i = 1; i <= nargs; i++) { + if (lua.lua_isstring(LState, i)) { + messages.push(lua.lua_tojsstring(LState, i)) + } else if (lua.lua_isnumber(LState, i)) { + messages.push(String(lua.lua_tonumber(LState, i))) + } else if (lua.lua_isboolean(LState, i)) { + messages.push(String(lua.lua_toboolean(LState, i))) + } else { + messages.push(lua.lua_typename(LState, lua.lua_type(LState, i))) + } + } + + logs.push(messages.join(' ')) + return 0 + } + + lua.lua_pushcfunction(L, logFunction) + lua.lua_setglobal(L, fengari.to_luastring('log')) + + // Create print function (same behavior but tab-separated) + const printFunction = function(LState: any) { + const nargs = lua.lua_gettop(LState) + const messages: string[] = [] + + for (let i = 1; i <= nargs; i++) { + if (lua.lua_isstring(LState, i)) { + messages.push(lua.lua_tojsstring(LState, i)) + } else if (lua.lua_isnumber(LState, i)) { + messages.push(String(lua.lua_tonumber(LState, i))) + } else if (lua.lua_isboolean(LState, i)) { + messages.push(String(lua.lua_toboolean(LState, i))) + } else { + messages.push(lua.lua_typename(LState, lua.lua_type(LState, i))) + } + } + + logs.push(messages.join('\t')) + return 0 + } + + lua.lua_pushcfunction(L, printFunction) + lua.lua_setglobal(L, fengari.to_luastring('print')) +} diff --git a/frontends/nextjs/src/lib/lua/functions/types.ts b/frontends/nextjs/src/lib/lua/functions/types.ts new file mode 100644 index 000000000..a9bf1e7ef --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/types.ts @@ -0,0 +1,21 @@ +/** + * Lua Engine Types + * Shared type definitions for Lua execution + */ + +export interface LuaExecutionContext { + data?: any + user?: any + kv?: { + get: (key: string) => Promise + set: (key: string, value: any) => Promise + } + log?: (...args: any[]) => void +} + +export interface LuaExecutionResult { + success: boolean + result?: any + error?: string + logs: string[] +} diff --git a/frontends/nextjs/src/lib/security/SecurityScanner.ts b/frontends/nextjs/src/lib/security/SecurityScanner.ts new file mode 100644 index 000000000..65c1e202e --- /dev/null +++ b/frontends/nextjs/src/lib/security/SecurityScanner.ts @@ -0,0 +1,60 @@ +/** + * SecurityScanner - Class wrapper for security scanning operations + * + * This class serves as a container for lambda functions related to security scanning. + * Each method delegates to an individual function file in the functions/ directory. + * + * Pattern: "class is container for lambdas" + * - Each lambda is defined in its own file under functions/ + * - This class wraps them for convenient namespaced access + * - Can be used as SecurityScanner.methodName() or import individual functions + */ + +import type { SecurityScanResult, SecurityIssue, SecurityPattern } from './functions/types' + +// Import individual lambdas +import { scanJavaScript } from './functions/scanners/scan-javascript' +import { scanLua } from './functions/scanners/scan-lua' +import { scanJSON } from './functions/scanners/scan-json' +import { scanHTML } from './functions/scanners/scan-html' +import { sanitizeInput } from './functions/scanners/sanitize-input' +import { getSeverityColor } from './functions/helpers/get-severity-color' +import { getSeverityIcon } from './functions/helpers/get-severity-icon' + +// Re-export types for convenience +export type { SecurityScanResult, SecurityIssue, SecurityPattern } + +/** + * SecurityScanner class wraps individual scanner lambdas + */ +export class SecurityScanner { + /** Scan JavaScript code for security vulnerabilities */ + scanJavaScript = scanJavaScript + + /** Scan Lua code for security vulnerabilities */ + scanLua = scanLua + + /** Scan JSON for security vulnerabilities */ + scanJSON = scanJSON + + /** Scan HTML for security vulnerabilities */ + scanHTML = scanHTML + + /** Sanitize input based on content type */ + sanitizeInput = sanitizeInput +} + +// Export singleton instance for convenience +export const securityScanner = new SecurityScanner() + +// Export helper functions directly +export { getSeverityColor, getSeverityIcon } + +// Re-export all individual functions for direct imports +export { + scanJavaScript, + scanLua, + scanJSON, + scanHTML, + sanitizeInput +} diff --git a/frontends/nextjs/src/lib/security/secure-db/access-rules.ts b/frontends/nextjs/src/lib/security/secure-db/access-rules.ts new file mode 100644 index 000000000..5fa4bfbdc --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/access-rules.ts @@ -0,0 +1,43 @@ +import type { AccessRule } from './types' + +export const ACCESS_RULES: AccessRule[] = [ + { resource: 'user', operation: 'READ', allowedRoles: ['user', 'admin', 'god', 'supergod'] }, + { resource: 'user', operation: 'CREATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'user', operation: 'UPDATE', allowedRoles: ['admin', 'god', 'supergod'] }, + { resource: 'user', operation: 'DELETE', allowedRoles: ['god', 'supergod'] }, + + { resource: 'workflow', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] }, + { resource: 'workflow', operation: 'CREATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'workflow', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'workflow', operation: 'DELETE', allowedRoles: ['god', 'supergod'] }, + + { resource: 'luaScript', operation: 'READ', allowedRoles: ['god', 'supergod'] }, + { resource: 'luaScript', operation: 'CREATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'luaScript', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'luaScript', operation: 'DELETE', allowedRoles: ['god', 'supergod'] }, + + { resource: 'pageConfig', operation: 'READ', allowedRoles: ['user', 'admin', 'god', 'supergod'] }, + { resource: 'pageConfig', operation: 'CREATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'pageConfig', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'pageConfig', operation: 'DELETE', allowedRoles: ['god', 'supergod'] }, + + { resource: 'modelSchema', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] }, + { resource: 'modelSchema', operation: 'CREATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'modelSchema', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] }, + { resource: 'modelSchema', operation: 'DELETE', allowedRoles: ['god', 'supergod'] }, + + { resource: 'comment', operation: 'READ', allowedRoles: ['user', 'admin', 'god', 'supergod'] }, + { resource: 'comment', operation: 'CREATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] }, + { resource: 'comment', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] }, + { resource: 'comment', operation: 'DELETE', allowedRoles: ['admin', 'god', 'supergod'] }, + + { resource: 'smtpConfig', operation: 'READ', allowedRoles: ['god', 'supergod'] }, + { resource: 'smtpConfig', operation: 'UPDATE', allowedRoles: ['supergod'] }, + + { resource: 'credential', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] }, + + { resource: 'tenant', operation: 'READ', allowedRoles: ['god', 'supergod'] }, + { resource: 'tenant', operation: 'CREATE', allowedRoles: ['supergod'] }, + { resource: 'tenant', operation: 'UPDATE', allowedRoles: ['supergod'] }, + { resource: 'tenant', operation: 'DELETE', allowedRoles: ['supergod'] }, +] diff --git a/frontends/nextjs/src/lib/security/secure-db/check-access.ts b/frontends/nextjs/src/lib/security/secure-db/check-access.ts new file mode 100644 index 000000000..005fea32d --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/check-access.ts @@ -0,0 +1,30 @@ +import type { SecurityContext, OperationType, ResourceType } from './types' +import { ACCESS_RULES } from './access-rules' + +/** + * Check if user has access to perform operation on resource + */ +export async function checkAccess( + ctx: SecurityContext, + resource: ResourceType, + operation: OperationType, + resourceId?: string +): Promise { + const rule = ACCESS_RULES.find( + r => r.resource === resource && r.operation === operation + ) + + if (!rule) { + return false + } + + if (!rule.allowedRoles.includes(ctx.user.role)) { + return false + } + + if (rule.customCheck) { + return await rule.customCheck(ctx, resourceId) + } + + return true +} diff --git a/frontends/nextjs/src/lib/security/secure-db/check-rate-limit.ts b/frontends/nextjs/src/lib/security/secure-db/check-rate-limit.ts new file mode 100644 index 000000000..7be9c22bc --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/check-rate-limit.ts @@ -0,0 +1,38 @@ +const RATE_LIMIT_WINDOW = 60000 +const MAX_REQUESTS_PER_WINDOW = 100 + +const rateLimitMap = new Map() + +/** + * Check if user is within rate limits + */ +export function checkRateLimit(userId: string): boolean { + const now = Date.now() + const userRequests = rateLimitMap.get(userId) || [] + + const recentRequests = userRequests.filter( + timestamp => now - timestamp < RATE_LIMIT_WINDOW + ) + + if (recentRequests.length >= MAX_REQUESTS_PER_WINDOW) { + return false + } + + recentRequests.push(now) + rateLimitMap.set(userId, recentRequests) + return true +} + +/** + * Clear rate limit for a user (useful for testing) + */ +export function clearRateLimit(userId: string): void { + rateLimitMap.delete(userId) +} + +/** + * Clear all rate limits (useful for testing) + */ +export function clearAllRateLimits(): void { + rateLimitMap.clear() +} diff --git a/frontends/nextjs/src/lib/security/secure-db/sanitize-input.ts b/frontends/nextjs/src/lib/security/secure-db/sanitize-input.ts new file mode 100644 index 000000000..e5a57100a --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/sanitize-input.ts @@ -0,0 +1,19 @@ +/** + * Sanitize user input to prevent XSS and injection attacks + */ +export function sanitizeInput(input: T): T { + if (typeof input === 'string') { + return input.replace(/[<>'"]/g, '') as T + } + if (Array.isArray(input)) { + return input.map(item => sanitizeInput(item)) as T + } + if (typeof input === 'object' && input !== null) { + const sanitized: Record = {} + for (const key in input) { + sanitized[key] = sanitizeInput((input as Record)[key]) + } + return sanitized as T + } + return input +} diff --git a/frontends/nextjs/src/lib/security/secure-db/types.ts b/frontends/nextjs/src/lib/security/secure-db/types.ts new file mode 100644 index 000000000..287cd25ec --- /dev/null +++ b/frontends/nextjs/src/lib/security/secure-db/types.ts @@ -0,0 +1,33 @@ +import type { User } from '../../types/level-types' + +export type OperationType = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' +export type ResourceType = 'user' | 'workflow' | 'luaScript' | 'pageConfig' | + 'modelSchema' | 'comment' | 'componentNode' | 'componentConfig' | 'cssCategory' | + 'dropdownConfig' | 'tenant' | 'powerTransfer' | 'smtpConfig' | 'credential' + +export interface AuditLog { + id: string + timestamp: number + userId: string + username: string + operation: OperationType + resource: ResourceType + resourceId: string + success: boolean + errorMessage?: string + ipAddress?: string + metadata?: Record +} + +export interface SecurityContext { + user: User + ipAddress?: string + requestId?: string +} + +export interface AccessRule { + resource: ResourceType + operation: OperationType + allowedRoles: string[] + customCheck?: (ctx: SecurityContext, resourceId?: string) => Promise +} diff --git a/frontends/nextjs/src/lib/security/security-scanner.ts b/frontends/nextjs/src/lib/security/security-scanner.ts index 57d7eeda0..e012f40dc 100644 --- a/frontends/nextjs/src/lib/security/security-scanner.ts +++ b/frontends/nextjs/src/lib/security/security-scanner.ts @@ -1,565 +1,87 @@ -export interface SecurityScanResult { - safe: boolean - severity: 'safe' | 'low' | 'medium' | 'high' | 'critical' - issues: SecurityIssue[] - sanitizedCode?: string -} +/** + * Security Scanner + * + * Wraps individual scan functions into a unified class interface. + * Each scanning function is implemented as 1 lambda/function per file + * in the functions/ directory. + * + * Pattern: Lambda wrapper class - keeps this file short while maintaining + * a convenient class-based API. + */ -export interface SecurityIssue { - type: 'malicious' | 'suspicious' | 'dangerous' | 'warning' - severity: 'low' | 'medium' | 'high' | 'critical' - message: string - pattern: string - line?: number - recommendation?: string -} +// Types +export type { SecurityScanResult, SecurityIssue, SecurityPattern } from './functions/types' -const MALICIOUS_PATTERNS = [ - { - pattern: /eval\s*\(/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Use of eval() detected - can execute arbitrary code', - recommendation: 'Use safe alternatives like JSON.parse() or Function constructor with strict validation' - }, - { - pattern: /Function\s*\(/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'Dynamic Function constructor detected', - recommendation: 'Avoid dynamic code generation or use with extreme caution' - }, - { - pattern: /innerHTML\s*=/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'innerHTML assignment detected - XSS vulnerability risk', - recommendation: 'Use textContent, createElement, or React JSX instead' - }, - { - pattern: /dangerouslySetInnerHTML/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'dangerouslySetInnerHTML detected - XSS vulnerability risk', - recommendation: 'Sanitize HTML content or use safe alternatives' - }, - { - pattern: /document\.write\s*\(/gi, - type: 'dangerous' as const, - severity: 'medium' as const, - message: 'document.write() detected - can cause security issues', - recommendation: 'Use DOM manipulation methods instead' - }, - { - pattern: /\.call\s*\(\s*window/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Calling functions with window context', - recommendation: 'Be careful with context manipulation' - }, - { - pattern: /\.apply\s*\(\s*window/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Applying functions with window context', - recommendation: 'Be careful with context manipulation' - }, - { - pattern: /__proto__/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Prototype pollution attempt detected', - recommendation: 'Never manipulate __proto__ directly' - }, - { - pattern: /constructor\s*\[\s*['"]prototype['"]\s*\]/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Prototype manipulation detected', - recommendation: 'Use Object.create() or proper class syntax' - }, - { - pattern: /import\s+.*\s+from\s+['"]https?:/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Remote code import detected', - recommendation: 'Only import from trusted, local sources' - }, - { - pattern: /]*>/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Script tag injection detected', - recommendation: 'Never inject script tags dynamically' - }, - { - pattern: /on(click|load|error|mouseover|mouseout|focus|blur)\s*=/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Inline event handler detected', - recommendation: 'Use addEventListener or React event handlers' - }, - { - pattern: /javascript:\s*/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'javascript: protocol detected', - recommendation: 'Never use javascript: protocol in URLs' - }, - { - pattern: /data:\s*text\/html/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'Data URI with HTML detected', - recommendation: 'Avoid data URIs with executable content' - }, - { - pattern: /setTimeout\s*\(\s*['"`]/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'setTimeout with string argument detected', - recommendation: 'Use setTimeout with function reference instead' - }, - { - pattern: /setInterval\s*\(\s*['"`]/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'setInterval with string argument detected', - recommendation: 'Use setInterval with function reference instead' - }, - { - pattern: /localStorage|sessionStorage/gi, - type: 'warning' as const, - severity: 'low' as const, - message: 'Local/session storage usage detected', - recommendation: 'Use useKV hook for persistent data instead' - }, - { - pattern: /crypto\.subtle|atob|btoa/gi, - type: 'warning' as const, - severity: 'low' as const, - message: 'Cryptographic operation detected', - recommendation: 'Ensure proper key management and secure practices' - }, - { - pattern: /XMLHttpRequest|fetch\s*\(\s*['"`]http/gi, - type: 'warning' as const, - severity: 'medium' as const, - message: 'External HTTP request detected', - recommendation: 'Ensure CORS and security headers are properly configured' - }, - { - pattern: /window\.open/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'window.open detected', - recommendation: 'Be cautious with popup windows' - }, - { - pattern: /location\.href\s*=/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Direct location manipulation detected', - recommendation: 'Use React Router or validate URLs carefully' - }, - { - pattern: /require\s*\(\s*[^'"`]/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'Dynamic require() detected', - recommendation: 'Use static imports only' - }, - { - pattern: /\.exec\s*\(|child_process|spawn|fork|execFile/gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'System command execution attempt detected', - recommendation: 'This is not allowed in browser environment' - }, - { - pattern: /fs\.|path\.|os\./gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'Node.js system module usage detected', - recommendation: 'File system access not allowed in browser' - }, - { - pattern: /process\.env|process\.exit/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Process manipulation detected', - recommendation: 'Not applicable in browser environment' - } -] +// Import individual lambda functions +import { scanJavaScript } from './functions/scanners/scan-javascript' +import { scanLua } from './functions/scanners/scan-lua' +import { scanJSON } from './functions/scanners/scan-json' +import { scanHTML } from './functions/scanners/scan-html' +import { sanitizeInput } from './functions/scanners/sanitize-input' +import { getSeverityColor } from './functions/helpers/get-severity-color' +import { getSeverityIcon } from './functions/helpers/get-severity-icon' -const LUA_MALICIOUS_PATTERNS = [ - { - pattern: /os\.(execute|exit|remove|rename|tmpname)/gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'Lua OS module system call detected', - recommendation: 'OS module access is disabled for security' - }, - { - pattern: /io\.(popen|tmpfile|open|input|output|lines)/gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'Lua file I/O operation detected', - recommendation: 'File system access is disabled for security' - }, - { - pattern: /loadfile|dofile/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Lua file loading function detected', - recommendation: 'File loading is disabled for security' - }, - { - pattern: /package\.(loadlib|searchpath|cpath)/gi, - type: 'dangerous' as const, - severity: 'critical' as const, - message: 'Lua dynamic library loading detected', - recommendation: 'Dynamic library loading is disabled' - }, - { - pattern: /debug\.(getinfo|setmetatable|getfenv|setfenv)/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'Lua debug module advanced features detected', - recommendation: 'Limited debug functionality available' - }, - { - pattern: /loadstring\s*\(/gi, - type: 'dangerous' as const, - severity: 'high' as const, - message: 'Lua dynamic code execution detected', - recommendation: 'Use with extreme caution' - }, - { - pattern: /\.\.\s*[[\]]/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Potential Lua table manipulation', - recommendation: 'Ensure proper validation' - }, - { - pattern: /_G\s*\[/gi, - type: 'suspicious' as const, - severity: 'high' as const, - message: 'Global environment manipulation detected', - recommendation: 'Avoid modifying global environment' - }, - { - pattern: /getmetatable|setmetatable/gi, - type: 'suspicious' as const, - severity: 'medium' as const, - message: 'Metatable manipulation detected', - recommendation: 'Use carefully to avoid security issues' - }, - { - pattern: /while\s+true\s+do/gi, - type: 'warning' as const, - severity: 'medium' as const, - message: 'Infinite loop detected', - recommendation: 'Ensure proper break conditions exist' - }, - { - pattern: /function\s+(\w+)\s*\([^)]*\)\s*\{[^}]*\1\s*\(/gi, - type: 'warning' as const, - severity: 'low' as const, - message: 'Potential recursive function', - recommendation: 'Ensure recursion has proper termination' - } -] - -const SQL_INJECTION_PATTERNS = [ - { - pattern: /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+/gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'SQL injection attempt detected', - recommendation: 'Use parameterized queries' - }, - { - pattern: /UNION\s+SELECT/gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'SQL UNION injection attempt', - recommendation: 'Use parameterized queries' - }, - { - pattern: /'[\s]*OR[\s]*'1'[\s]*=[\s]*'1/gi, - type: 'malicious' as const, - severity: 'critical' as const, - message: 'SQL authentication bypass attempt', - recommendation: 'Never concatenate user input into SQL' - }, - { - pattern: /--[\s]*$/gm, - type: 'suspicious' as const, - severity: 'high' as const, - message: 'SQL comment pattern detected', - recommendation: 'May indicate SQL injection attempt' - } -] +import type { SecurityScanResult } from './functions/types' +/** + * SecurityScanner - Wrapper class for security scanning functions + * + * All methods delegate to individual lambda functions in functions/scanners/ + * This keeps the main class file small while providing a unified API. + */ export class SecurityScanner { - scanJavaScript(code: string): SecurityScanResult { - const issues: SecurityIssue[] = [] - const lines = code.split('\n') - - for (const pattern of MALICIOUS_PATTERNS) { - const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags)) - for (const match of matches) { - const lineNumber = this.getLineNumber(code, match.index || 0) - issues.push({ - type: pattern.type, - severity: pattern.severity, - message: pattern.message, - pattern: match[0], - line: lineNumber, - recommendation: pattern.recommendation - }) - } - } - - for (const pattern of SQL_INJECTION_PATTERNS) { - const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags)) - for (const match of matches) { - const lineNumber = this.getLineNumber(code, match.index || 0) - issues.push({ - type: pattern.type, - severity: pattern.severity, - message: pattern.message, - pattern: match[0], - line: lineNumber, - recommendation: pattern.recommendation - }) - } - } - - const severity = this.calculateOverallSeverity(issues) - const safe = severity === 'safe' || severity === 'low' - - return { - safe, - severity, - issues, - sanitizedCode: safe ? code : undefined - } - } - - scanLua(code: string): SecurityScanResult { - const issues: SecurityIssue[] = [] - - for (const pattern of LUA_MALICIOUS_PATTERNS) { - const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags)) - for (const match of matches) { - const lineNumber = this.getLineNumber(code, match.index || 0) - issues.push({ - type: pattern.type, - severity: pattern.severity, - message: pattern.message, - pattern: match[0], - line: lineNumber, - recommendation: pattern.recommendation - }) - } - } - - const severity = this.calculateOverallSeverity(issues) - const safe = severity === 'safe' || severity === 'low' - - return { - safe, - severity, - issues, - sanitizedCode: safe ? code : undefined - } - } - - scanJSON(jsonString: string): SecurityScanResult { - const issues: SecurityIssue[] = [] - - try { - JSON.parse(jsonString) - } catch (error) { - issues.push({ - type: 'warning', - severity: 'medium', - message: 'Invalid JSON format', - pattern: 'JSON parse error', - recommendation: 'Ensure JSON is properly formatted' - }) - } - - const protoPollution = /__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]/gi - if (protoPollution.test(jsonString)) { - issues.push({ - type: 'malicious', - severity: 'critical', - message: 'Prototype pollution attempt in JSON', - pattern: '__proto__', - recommendation: 'Remove prototype manipulation from JSON' - }) - } - - if (jsonString.includes('', - recommendation: 'Remove all HTML/script content from JSON' - }) - } - - const severity = this.calculateOverallSeverity(issues) - const safe = severity === 'safe' || severity === 'low' - - return { - safe, - severity, - issues, - sanitizedCode: safe ? jsonString : undefined - } - } - - scanHTML(html: string): SecurityScanResult { - const issues: SecurityIssue[] = [] - - const scriptTagPattern = /]*>.*?<\/script>/gis - const matches = html.matchAll(scriptTagPattern) - for (const match of matches) { - issues.push({ - type: 'dangerous', - severity: 'critical', - message: 'Script tag detected in HTML', - pattern: match[0].substring(0, 50) + '...', - recommendation: 'Remove script tags or use proper React components' - }) - } - - const inlineEventPattern = /on(click|load|error|mouseover|mouseout|focus|blur|submit)\s*=/gi - const inlineMatches = html.matchAll(inlineEventPattern) - for (const match of inlineMatches) { - issues.push({ - type: 'dangerous', - severity: 'high', - message: 'Inline event handler in HTML', - pattern: match[0], - recommendation: 'Use React event handlers instead' - }) - } - - const javascriptProtocol = /href\s*=\s*['"]javascript:/gi - if (javascriptProtocol.test(html)) { - issues.push({ - type: 'dangerous', - severity: 'critical', - message: 'javascript: protocol in href', - pattern: 'javascript:', - recommendation: 'Use proper URLs or event handlers' - }) - } - - const iframePattern = /]*>/gi - const iframeMatches = html.matchAll(iframePattern) - for (const match of iframeMatches) { - if (!match[0].includes('sandbox=')) { - issues.push({ - type: 'suspicious', - severity: 'medium', - message: 'Iframe without sandbox attribute', - pattern: match[0], - recommendation: 'Add sandbox attribute to iframes for security' - }) - } - } - - const severity = this.calculateOverallSeverity(issues) - const safe = severity === 'safe' || severity === 'low' - - return { - safe, - severity, - issues - } - } - - private getLineNumber(code: string, index: number): number { - return code.substring(0, index).split('\n').length - } - - private calculateOverallSeverity(issues: SecurityIssue[]): 'safe' | 'low' | 'medium' | 'high' | 'critical' { - if (issues.length === 0) return 'safe' - - const hasCritical = issues.some(i => i.severity === 'critical') - const hasHigh = issues.some(i => i.severity === 'high') - const hasMedium = issues.some(i => i.severity === 'medium') - const hasLow = issues.some(i => i.severity === 'low') - - if (hasCritical) return 'critical' - if (hasHigh) return 'high' - if (hasMedium) return 'medium' - if (hasLow) return 'low' - - return 'safe' - } - - sanitizeInput(input: string, type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text'): string { - let sanitized = input - - if (type === 'text') { - sanitized = sanitized.replace(/]*>.*?<\/script>/gis, '') - sanitized = sanitized.replace(/on\w+\s*=/gi, '') - sanitized = sanitized.replace(/javascript:/gi, '') - } - - if (type === 'html') { - sanitized = sanitized.replace(/]*>.*?<\/script>/gis, '') - sanitized = sanitized.replace(/on\w+\s*=/gi, '') - sanitized = sanitized.replace(/javascript:/gi, '') - sanitized = sanitized.replace(/data:\s*text\/html/gi, '') - } - - if (type === 'json') { - sanitized = sanitized.replace(/__proto__/gi, '_proto_') - sanitized = sanitized.replace(/constructor\s*\[\s*['"]prototype['"]\s*\]/gi, '') - } - - return sanitized - } + // Scanner methods - delegate to lambda functions + scanJavaScript = scanJavaScript + scanLua = scanLua + scanJSON = scanJSON + scanHTML = scanHTML + sanitizeInput = sanitizeInput } +// Default instance for convenience export const securityScanner = new SecurityScanner() -export function getSeverityColor(severity: string): string { - switch (severity) { - case 'critical': - return 'text-red-600 bg-red-50 border-red-200' - case 'high': - return 'text-orange-600 bg-orange-50 border-orange-200' - case 'medium': - return 'text-yellow-600 bg-yellow-50 border-yellow-200' - case 'low': - return 'text-blue-600 bg-blue-50 border-blue-200' - default: - return 'text-green-600 bg-green-50 border-green-200' - } +// Re-export helper functions +export { getSeverityColor, getSeverityIcon } + +// Re-export individual functions for direct use +export { + scanJavaScript, + scanLua, + scanJSON, + scanHTML, + sanitizeInput } -export function getSeverityIcon(severity: string): string { - switch (severity) { - case 'critical': - return '🚨' - case 'high': - return '⚠️' - case 'medium': - return '⚡' - case 'low': - return 'ℹ️' +/** + * Convenience function to scan code for vulnerabilities + * Automatically detects type based on content or explicit type parameter + */ +export function scanForVulnerabilities( + code: string, + type?: 'javascript' | 'lua' | 'json' | 'html' +): SecurityScanResult { + // Auto-detect type if not provided + if (!type) { + if (code.trim().startsWith('{') || code.trim().startsWith('[')) { + type = 'json' + } else if (code.includes('function') && code.includes('end')) { + type = 'lua' + } else if (code.includes('<') && code.includes('>')) { + type = 'html' + } else { + type = 'javascript' + } + } + + switch (type) { + case 'lua': + return scanLua(code) + case 'json': + return scanJSON(code) + case 'html': + return scanHTML(code) default: - return '✓' + return scanJavaScript(code) } }