feat(security): add comprehensive security scanning functions and patterns

- Implemented severity color and icon helpers for better UI representation.
- Created a set of security patterns for JavaScript, Lua, and SQL injection detection.
- Developed scanning functions for JavaScript, HTML, JSON, and Lua to identify vulnerabilities.
- Added sanitization utility to clean user input based on content type.
- Introduced types for security scan results and issues to standardize output.
- Enhanced overall severity calculation logic to determine the highest risk level from detected issues.
This commit is contained in:
2025-12-25 18:01:03 +00:00
parent 616d4ad87b
commit 089c93e649
84 changed files with 1959 additions and 4041 deletions

View File

@@ -1,3 +0,0 @@
// This file is no longer needed since database-prisma.ts has been renamed to database.ts
// Keeping for reference only
export * from './database'

File diff suppressed because it is too large Load Diff

View File

@@ -1,934 +0,0 @@
import { prisma } from '../prisma'
import type {
User,
Workflow,
LuaScript,
PageConfig,
AppConfiguration,
Comment,
Tenant,
PowerTransferRequest,
} from '../../types/level-types'
import type { ModelSchema } from '../types/schema-types'
import type { InstalledPackage } from '../package-types'
import type { SMTPConfig } from '../password-utils'
// Import individual functions (lambdas)
import { hashPassword } from './functions/hash-password'
import { verifyPassword } from './functions/verify-password'
import { initializeDatabase } from './functions/initialize-database'
// Users
import { getUsers } from './functions/users/get-users'
import { setUsers } from './functions/users/set-users'
import { addUser } from './functions/users/add-user'
import { updateUser } from './functions/users/update-user'
import { deleteUser } from './functions/users/delete-user'
// Credentials
import { getCredentials } from './functions/credentials/get-credentials'
import { setCredential } from './functions/credentials/set-credential'
import { verifyCredentials } from './functions/credentials/verify-credentials'
import { getPasswordChangeTimestamps } from './functions/credentials/get-password-change-timestamps'
import { setPasswordChangeTimestamps } from './functions/credentials/set-password-change-timestamps'
// Workflows
import { getWorkflows } from './functions/workflows/get-workflows'
import { setWorkflows } from './functions/workflows/set-workflows'
import { addWorkflow } from './functions/workflows/add-workflow'
import { updateWorkflow } from './functions/workflows/update-workflow'
import { deleteWorkflow } from './functions/workflows/delete-workflow'
// Lua Scripts
import { getLuaScripts } from './functions/lua-scripts/get-lua-scripts'
import { setLuaScripts } from './functions/lua-scripts/set-lua-scripts'
import { addLuaScript } from './functions/lua-scripts/add-lua-script'
import { updateLuaScript } from './functions/lua-scripts/update-lua-script'
import { deleteLuaScript } from './functions/lua-scripts/delete-lua-script'
// Pages
import { getPages } from './functions/pages/get-pages'
import { setPages } from './functions/pages/set-pages'
import { addPage } from './functions/pages/add-page'
import { updatePage } from './functions/pages/update-page'
import { deletePage } from './functions/pages/delete-page'
// Schemas
import { getSchemas } from './functions/schemas/get-schemas'
import { setSchemas } from './functions/schemas/set-schemas'
import { addSchema } from './functions/schemas/add-schema'
import { updateSchema } from './functions/schemas/update-schema'
import { deleteSchema } from './functions/schemas/delete-schema'
// Tenants
import { getTenants } from './functions/tenants/get-tenants'
import { setTenants } from './functions/tenants/set-tenants'
import { addTenant } from './functions/tenants/add-tenant'
import { updateTenant } from './functions/tenants/update-tenant'
import { deleteTenant } from './functions/tenants/delete-tenant'
// Re-export types from database.ts for compatibility
export interface CssCategory {
name: string
classes: string[]
}
export interface DropdownConfig {
id: string
name: string
label: string
options: Array<{ value: string; label: string }>
}
export interface ComponentNode {
id: string
type: string
parentId?: string
childIds: string[]
order: number
pageId: string
}
export interface ComponentConfig {
id: string
componentId: string
props: Record<string, any>
styles: Record<string, any>
events: Record<string, string>
conditionalRendering?: {
condition: string
luaScriptId?: string
}
}
export const DB_KEYS = {
USERS: 'db_users',
CREDENTIALS: 'db_credentials',
WORKFLOWS: 'db_workflows',
LUA_SCRIPTS: 'db_lua_scripts',
PAGES: 'db_pages',
SCHEMAS: 'db_schemas',
APP_CONFIG: 'db_app_config',
COMMENTS: 'db_comments',
COMPONENT_HIERARCHY: 'db_component_hierarchy',
COMPONENT_CONFIGS: 'db_component_configs',
GOD_CREDENTIALS_EXPIRY: 'db_god_credentials_expiry',
PASSWORD_CHANGE_TIMESTAMPS: 'db_password_change_timestamps',
FIRST_LOGIN_FLAGS: 'db_first_login_flags',
GOD_CREDENTIALS_EXPIRY_DURATION: 'db_god_credentials_expiry_duration',
CSS_CLASSES: 'db_css_classes',
DROPDOWN_CONFIGS: 'db_dropdown_configs',
INSTALLED_PACKAGES: 'db_installed_packages',
PACKAGE_DATA: 'db_package_data',
TENANTS: 'db_tenants',
POWER_TRANSFER_REQUESTS: 'db_power_transfer_requests',
SMTP_CONFIG: 'db_smtp_config',
PASSWORD_RESET_TOKENS: 'db_password_reset_tokens',
} as const
/**
* Database - Class wrapper for database operations
*
* This class serves as a container for lambda functions related to database operations.
* 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 Database.methodName() or import individual functions
*/
export class Database {
// Core operations
static initializeDatabase = initializeDatabase
// User operations
static getUsers = getUsers
static setUsers = setUsers
static addUser = addUser
static updateUser = updateUser
static deleteUser = deleteUser
// Credential operations
static getCredentials = getCredentials
static setCredential = setCredential
static verifyCredentials = verifyCredentials
static getPasswordChangeTimestamps = getPasswordChangeTimestamps
static setPasswordChangeTimestamps = setPasswordChangeTimestamps
// Workflow operations
static getWorkflows = getWorkflows
static setWorkflows = setWorkflows
static addWorkflow = addWorkflow
static updateWorkflow = updateWorkflow
static deleteWorkflow = deleteWorkflow
// Lua Script operations
static getLuaScripts = getLuaScripts
static setLuaScripts = setLuaScripts
static addLuaScript = addLuaScript
static updateLuaScript = updateLuaScript
static deleteLuaScript = deleteLuaScript
// Page operations
static getPages = getPages
static setPages = setPages
static addPage = addPage
static updatePage = updatePage
static deletePage = deletePage
// Schema operations
static getSchemas = getSchemas
static setSchemas = setSchemas
static addSchema = addSchema
static updateSchema = updateSchema
static deleteSchema = deleteSchema
// Tenant operations
static getTenants = getTenants
static setTenants = setTenants
static addTenant = addTenant
static updateTenant = updateTenant
static deleteTenant = deleteTenant
// ==============================================
// Functions that require additional refactoring
// These are kept inline for now but can be split later
// ==============================================
static async getAppConfig(): Promise<AppConfiguration | null> {
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),
}
}
static async setAppConfig(config: AppConfiguration): Promise<void> {
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),
},
})
}
static async getComments(): Promise<Comment[]> {
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,
}))
}
static async setComments(comments: Comment[]): Promise<void> {
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,
},
})
}
}
static async addComment(comment: Comment): Promise<void> {
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,
},
})
}
static async updateComment(commentId: string, updates: Partial<Comment>): Promise<void> {
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,
})
}
static async deleteComment(commentId: string): Promise<void> {
await prisma.comment.delete({ where: { id: commentId } })
}
static async getComponentHierarchy(): Promise<Record<string, ComponentNode>> {
const nodes = await prisma.componentNode.findMany()
const result: Record<string, ComponentNode> = {}
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
}
static async setComponentHierarchy(hierarchy: Record<string, ComponentNode>): Promise<void> {
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,
},
})
}
}
static async addComponentNode(node: ComponentNode): Promise<void> {
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,
},
})
}
static async updateComponentNode(nodeId: string, updates: Partial<ComponentNode>): Promise<void> {
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,
})
}
static async deleteComponentNode(nodeId: string): Promise<void> {
await prisma.componentNode.delete({ where: { id: nodeId } })
}
static async getComponentConfigs(): Promise<Record<string, ComponentConfig>> {
const configs = await prisma.componentConfig.findMany()
const result: Record<string, ComponentConfig> = {}
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
}
static async setComponentConfigs(configs: Record<string, ComponentConfig>): Promise<void> {
await prisma.componentConfig.deleteMany()
for (const config of Object.values(configs)) {
await prisma.componentConfig.create({
data: {
id: config.id,
componentId: config.componentId,
props: JSON.stringify(config.props),
styles: JSON.stringify(config.styles),
events: JSON.stringify(config.events),
conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null,
},
})
}
}
static async addComponentConfig(config: ComponentConfig): Promise<void> {
await prisma.componentConfig.create({
data: {
id: config.id,
componentId: config.componentId,
props: JSON.stringify(config.props),
styles: JSON.stringify(config.styles),
events: JSON.stringify(config.events),
conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null,
},
})
}
static async updateComponentConfig(configId: string, updates: Partial<ComponentConfig>): Promise<void> {
const data: any = {}
if (updates.componentId !== undefined) data.componentId = updates.componentId
if (updates.props !== undefined) data.props = JSON.stringify(updates.props)
if (updates.styles !== undefined) data.styles = JSON.stringify(updates.styles)
if (updates.events !== undefined) data.events = JSON.stringify(updates.events)
if (updates.conditionalRendering !== undefined) data.conditionalRendering = JSON.stringify(updates.conditionalRendering)
await prisma.componentConfig.update({
where: { id: configId },
data,
})
}
static async deleteComponentConfig(configId: string): Promise<void> {
await prisma.componentConfig.delete({ where: { id: configId } })
}
static async getGodCredentialsExpiry(): Promise<number> {
const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry' } })
return config ? Number(config.value) : 0
}
static async setGodCredentialsExpiry(timestamp: number): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: 'god_credentials_expiry' },
update: { value: timestamp.toString() },
create: { key: 'god_credentials_expiry', value: timestamp.toString() },
})
}
static async getFirstLoginFlags(): Promise<Record<string, boolean>> {
const users = await prisma.user.findMany({
select: { username: true, firstLogin: true },
})
const result: Record<string, boolean> = {}
for (const user of users) {
result[user.username] = user.firstLogin
}
return result
}
static async setFirstLoginFlag(username: string, isFirstLogin: boolean): Promise<void> {
await prisma.user.update({
where: { username },
data: { firstLogin: isFirstLogin },
})
}
static async shouldShowGodCredentials(): Promise<boolean> {
const expiry = await this.getGodCredentialsExpiry()
const user = await prisma.user.findUnique({
where: { username: 'god' },
select: { passwordChangeTimestamp: true },
})
const godPasswordChangeTime = user?.passwordChangeTimestamp ? Number(user.passwordChangeTimestamp) : 0
if (expiry === 0) {
const duration = await this.getGodCredentialsExpiryDuration()
const expiryTime = Date.now() + duration
await this.setGodCredentialsExpiry(expiryTime)
return true
}
if (godPasswordChangeTime > expiry) {
return false
}
return Date.now() < expiry
}
static async getGodCredentialsExpiryDuration(): Promise<number> {
const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry_duration' } })
return config ? Number(config.value) : (60 * 60 * 1000)
}
static async setGodCredentialsExpiryDuration(durationMs: number): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: 'god_credentials_expiry_duration' },
update: { value: durationMs.toString() },
create: { key: 'god_credentials_expiry_duration', value: durationMs.toString() },
})
}
static async resetGodCredentialsExpiry(): Promise<void> {
const duration = await this.getGodCredentialsExpiryDuration()
const expiryTime = Date.now() + duration
await this.setGodCredentialsExpiry(expiryTime)
}
static async getCssClasses(): Promise<CssCategory[]> {
const categories = await prisma.cssCategory.findMany()
return categories.map(c => ({
name: c.name,
classes: JSON.parse(c.classes),
}))
}
static async setCssClasses(classes: CssCategory[]): Promise<void> {
await prisma.cssCategory.deleteMany()
for (const category of classes) {
await prisma.cssCategory.create({
data: {
name: category.name,
classes: JSON.stringify(category.classes),
},
})
}
}
static async addCssCategory(category: CssCategory): Promise<void> {
await prisma.cssCategory.create({
data: {
name: category.name,
classes: JSON.stringify(category.classes),
},
})
}
static async updateCssCategory(categoryName: string, classes: string[]): Promise<void> {
await prisma.cssCategory.update({
where: { name: categoryName },
data: { classes: JSON.stringify(classes) },
})
}
static async deleteCssCategory(categoryName: string): Promise<void> {
await prisma.cssCategory.delete({ where: { name: categoryName } })
}
static async getDropdownConfigs(): Promise<DropdownConfig[]> {
const configs = await prisma.dropdownConfig.findMany()
return configs.map(c => ({
id: c.id,
name: c.name,
label: c.label,
options: JSON.parse(c.options),
}))
}
static async setDropdownConfigs(configs: DropdownConfig[]): Promise<void> {
await prisma.dropdownConfig.deleteMany()
for (const config of configs) {
await prisma.dropdownConfig.create({
data: {
id: config.id,
name: config.name,
label: config.label,
options: JSON.stringify(config.options),
},
})
}
}
static async addDropdownConfig(config: DropdownConfig): Promise<void> {
await prisma.dropdownConfig.create({
data: {
id: config.id,
name: config.name,
label: config.label,
options: JSON.stringify(config.options),
},
})
}
static async updateDropdownConfig(id: string, updates: DropdownConfig): Promise<void> {
await prisma.dropdownConfig.update({
where: { id },
data: {
name: updates.name,
label: updates.label,
options: JSON.stringify(updates.options),
},
})
}
static async deleteDropdownConfig(id: string): Promise<void> {
await prisma.dropdownConfig.delete({ where: { id } })
}
static async getInstalledPackages(): Promise<InstalledPackage[]> {
const packages = await prisma.installedPackage.findMany()
return packages.map(p => ({
packageId: p.packageId,
installedAt: Number(p.installedAt),
version: p.version,
enabled: p.enabled,
}))
}
static async setInstalledPackages(packages: InstalledPackage[]): Promise<void> {
await prisma.installedPackage.deleteMany()
for (const pkg of packages) {
await prisma.installedPackage.create({
data: {
packageId: pkg.packageId,
installedAt: BigInt(pkg.installedAt),
version: pkg.version,
enabled: pkg.enabled,
},
})
}
}
static async installPackage(packageData: InstalledPackage): Promise<void> {
const exists = await prisma.installedPackage.findUnique({ where: { packageId: packageData.packageId } })
if (!exists) {
await prisma.installedPackage.create({
data: {
packageId: packageData.packageId,
installedAt: BigInt(packageData.installedAt),
version: packageData.version,
enabled: packageData.enabled,
},
})
}
}
static async uninstallPackage(packageId: string): Promise<void> {
await prisma.installedPackage.delete({ where: { packageId } })
}
static async togglePackageEnabled(packageId: string, enabled: boolean): Promise<void> {
await prisma.installedPackage.update({
where: { packageId },
data: { enabled },
})
}
static async getPackageData(packageId: string): Promise<Record<string, any[]>> {
const pkg = await prisma.packageData.findUnique({ where: { packageId } })
return pkg ? JSON.parse(pkg.data) : {}
}
static async setPackageData(packageId: string, data: Record<string, any[]>): Promise<void> {
await prisma.packageData.upsert({
where: { packageId },
update: { data: JSON.stringify(data) },
create: { packageId, data: JSON.stringify(data) },
})
}
static async deletePackageData(packageId: string): Promise<void> {
await prisma.packageData.delete({ where: { packageId } })
}
static async getPowerTransferRequests(): Promise<PowerTransferRequest[]> {
const requests = await prisma.powerTransferRequest.findMany()
return requests.map(r => ({
id: r.id,
fromUserId: r.fromUserId,
toUserId: r.toUserId,
status: r.status as any,
createdAt: Number(r.createdAt),
expiresAt: Number(r.expiresAt),
}))
}
static async setPowerTransferRequests(requests: PowerTransferRequest[]): Promise<void> {
await prisma.powerTransferRequest.deleteMany()
for (const request of requests) {
await prisma.powerTransferRequest.create({
data: {
id: request.id,
fromUserId: request.fromUserId,
toUserId: request.toUserId,
status: request.status,
createdAt: BigInt(request.createdAt),
expiresAt: BigInt(request.expiresAt),
},
})
}
}
static async addPowerTransferRequest(request: PowerTransferRequest): Promise<void> {
await prisma.powerTransferRequest.create({
data: {
id: request.id,
fromUserId: request.fromUserId,
toUserId: request.toUserId,
status: request.status,
createdAt: BigInt(request.createdAt),
expiresAt: BigInt(request.expiresAt),
},
})
}
static async updatePowerTransferRequest(requestId: string, updates: Partial<PowerTransferRequest>): Promise<void> {
const data: any = {}
if (updates.status !== undefined) data.status = updates.status
await prisma.powerTransferRequest.update({
where: { id: requestId },
data,
})
}
static async deletePowerTransferRequest(requestId: string): Promise<void> {
await prisma.powerTransferRequest.delete({ where: { id: requestId } })
}
static async getSuperGod(): Promise<User | null> {
const user = await prisma.user.findFirst({
where: { role: 'supergod' },
})
if (!user) return null
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role as any,
profilePicture: user.profilePicture || undefined,
bio: user.bio || undefined,
createdAt: Number(user.createdAt),
tenantId: user.tenantId || undefined,
isInstanceOwner: user.isInstanceOwner,
}
}
static async transferSuperGodPower(fromUserId: string, toUserId: string): Promise<void> {
await prisma.user.update({
where: { id: fromUserId },
data: { role: 'god', isInstanceOwner: false },
})
await prisma.user.update({
where: { id: toUserId },
data: { role: 'supergod', isInstanceOwner: true },
})
}
static async getSMTPConfig(): Promise<SMTPConfig | null> {
const config = await prisma.sMTPConfig.findFirst()
if (!config) return null
return {
host: config.host,
port: config.port,
secure: config.secure,
username: config.username,
password: config.password,
fromEmail: config.fromEmail,
fromName: config.fromName,
}
}
static async setSMTPConfig(config: SMTPConfig): Promise<void> {
await prisma.sMTPConfig.deleteMany()
await prisma.sMTPConfig.create({
data: {
host: config.host,
port: config.port,
secure: config.secure,
username: config.username,
password: config.password,
fromEmail: config.fromEmail,
fromName: config.fromName,
},
})
}
static async getPasswordResetTokens(): Promise<Record<string, string>> {
const tokens = await prisma.passwordResetToken.findMany()
const result: Record<string, string> = {}
for (const token of tokens) {
result[token.username] = token.token
}
return result
}
static async setPasswordResetToken(username: string, token: string): Promise<void> {
await prisma.passwordResetToken.upsert({
where: { username },
update: { token },
create: { username, token },
})
}
static async deletePasswordResetToken(username: string): Promise<void> {
await prisma.passwordResetToken.delete({ where: { username } })
}
static async clearDatabase(): Promise<void> {
await prisma.user.deleteMany()
await prisma.credential.deleteMany()
await prisma.workflow.deleteMany()
await prisma.luaScript.deleteMany()
await prisma.pageConfig.deleteMany()
await prisma.modelSchema.deleteMany()
await prisma.appConfiguration.deleteMany()
await prisma.comment.deleteMany()
await prisma.componentNode.deleteMany()
await prisma.componentConfig.deleteMany()
await prisma.systemConfig.deleteMany()
await prisma.cssCategory.deleteMany()
await prisma.dropdownConfig.deleteMany()
await prisma.installedPackage.deleteMany()
await prisma.packageData.deleteMany()
await prisma.tenant.deleteMany()
await prisma.powerTransferRequest.deleteMany()
await prisma.sMTPConfig.deleteMany()
await prisma.passwordResetToken.deleteMany()
}
static async seedDefaultData(): Promise<void> {
const users = await this.getUsers()
const credentials = await this.getCredentials()
if (users.length === 0) {
const defaultUsers: User[] = [
{
id: 'user_supergod',
username: 'supergod',
email: 'supergod@builder.com',
role: 'supergod',
bio: 'Supreme administrator with multi-tenant control',
createdAt: Date.now(),
isInstanceOwner: true,
},
{
id: 'user_god',
username: 'god',
email: 'god@builder.com',
role: 'god',
bio: 'System architect with full access to all levels',
createdAt: Date.now(),
},
{
id: 'user_admin',
username: 'admin',
email: 'admin@builder.com',
role: 'admin',
bio: 'Administrator with data management access',
createdAt: Date.now(),
},
{
id: 'user_demo',
username: 'demo',
email: 'demo@builder.com',
role: 'user',
bio: 'Demo user account',
createdAt: Date.now(),
},
]
await this.setUsers(defaultUsers)
}
if (Object.keys(credentials).length === 0) {
const { getScrambledPassword } = await import('../auth')
await this.setCredential('supergod', await hashPassword(getScrambledPassword('supergod')))
await this.setCredential('god', await hashPassword(getScrambledPassword('god')))
await this.setCredential('admin', await hashPassword(getScrambledPassword('admin')))
await this.setCredential('demo', await hashPassword(getScrambledPassword('demo')))
await this.setFirstLoginFlag('supergod', true)
await this.setFirstLoginFlag('god', true)
await this.setFirstLoginFlag('admin', false)
await this.setFirstLoginFlag('demo', false)
}
const appConfig = await this.getAppConfig()
if (!appConfig) {
const defaultConfig: AppConfiguration = {
id: 'app_001',
name: 'MetaBuilder App',
schemas: [],
workflows: [],
luaScripts: [],
pages: [],
theme: {
colors: {},
fonts: {},
},
}
await this.setAppConfig(defaultConfig)
}
const cssClasses = await this.getCssClasses()
if (cssClasses.length === 0) {
const defaultCssClasses: CssCategory[] = [
{ name: 'Layout', classes: ['flex', 'flex-col', 'flex-row', 'grid', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'block', 'inline-block', 'inline', 'hidden'] },
{ name: 'Spacing', classes: ['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-6', 'p-8', 'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-6', 'm-8', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'] },
{ name: 'Sizing', classes: ['w-full', 'w-1/2', 'w-1/3', 'w-1/4', 'w-auto', 'h-full', 'h-screen', 'h-auto', 'min-h-screen', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-4xl', 'max-w-6xl', 'max-w-7xl'] },
{ name: 'Typography', classes: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize'] },
{ name: 'Colors', classes: ['text-primary', 'text-secondary', 'text-accent', 'text-muted-foreground', 'bg-primary', 'bg-secondary', 'bg-accent', 'bg-background', 'bg-card', 'bg-muted', 'border-primary', 'border-secondary', 'border-accent', 'border-border'] },
{ name: 'Borders', classes: ['border', 'border-2', 'border-4', 'border-t', 'border-b', 'border-l', 'border-r', 'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full'] },
{ name: 'Effects', classes: ['shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'hover:shadow-lg', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'transition', 'transition-all', 'duration-200', 'duration-300', 'duration-500'] },
{ name: 'Positioning', classes: ['relative', 'absolute', 'fixed', 'sticky', 'top-0', 'bottom-0', 'left-0', 'right-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'] },
{ name: 'Alignment', classes: ['items-start', 'items-center', 'items-end', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'self-start', 'self-center', 'self-end'] },
{ name: 'Interactivity', classes: ['cursor-pointer', 'cursor-default', 'pointer-events-none', 'select-none', 'hover:bg-accent', 'hover:text-accent-foreground', 'active:scale-95', 'disabled:opacity-50'] },
]
await this.setCssClasses(defaultCssClasses)
}
const dropdowns = await this.getDropdownConfigs()
if (dropdowns.length === 0) {
const defaultDropdowns: DropdownConfig[] = [
{ id: 'dropdown_status', name: 'status_options', label: 'Status', options: [{ value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }, { value: 'archived', label: 'Archived' }] },
{ id: 'dropdown_priority', name: 'priority_options', label: 'Priority', options: [{ value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, { value: 'urgent', label: 'Urgent' }] },
{ id: 'dropdown_category', name: 'category_options', label: 'Category', options: [{ value: 'general', label: 'General' }, { value: 'technical', label: 'Technical' }, { value: 'business', label: 'Business' }, { value: 'personal', label: 'Personal' }] },
]
await this.setDropdownConfigs(defaultDropdowns)
}
}
static async exportDatabase(): Promise<string> {
const data = {
users: await this.getUsers(),
workflows: await this.getWorkflows(),
luaScripts: await this.getLuaScripts(),
pages: await this.getPages(),
schemas: await this.getSchemas(),
appConfig: (await this.getAppConfig()) || undefined,
comments: await this.getComments(),
componentHierarchy: await this.getComponentHierarchy(),
componentConfigs: await this.getComponentConfigs(),
}
return JSON.stringify(data, null, 2)
}
static async importDatabase(jsonData: string): Promise<void> {
try {
const data = JSON.parse(jsonData)
if (data.users) await this.setUsers(data.users)
if (data.workflows) await this.setWorkflows(data.workflows)
if (data.luaScripts) await this.setLuaScripts(data.luaScripts)
if (data.pages) await this.setPages(data.pages)
if (data.schemas) await this.setSchemas(data.schemas)
if (data.appConfig) await this.setAppConfig(data.appConfig)
if (data.comments) await this.setComments(data.comments)
if (data.componentHierarchy) await this.setComponentHierarchy(data.componentHierarchy)
if (data.componentConfigs) await this.setComponentConfigs(data.componentConfigs)
} catch (error) {
throw new Error('Failed to import database: Invalid JSON')
}
}
}
// Re-export standalone functions for direct import
export { hashPassword, verifyPassword }

View File

@@ -30,7 +30,7 @@ describe('setAppConfig', () => {
workflows: [],
luaScripts: [],
pages: [],
theme: {},
theme: { colors: {}, fonts: {} },
})
expect(mockDelete).toHaveBeenCalled()

View File

@@ -31,6 +31,8 @@ export interface DBALAdapter {
update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown>
delete(entity: string, id: string): Promise<boolean>
list(entity: string, options?: ListOptions): Promise<ListResult<unknown>>
findFirst(entity: string, options?: { where?: Record<string, unknown> }): Promise<unknown | null>
upsert(entity: string, options: { where: Record<string, unknown>; update: Record<string, unknown>; create: Record<string, unknown> }): Promise<unknown>
close(): Promise<void>
}
@@ -122,6 +124,21 @@ const prismaAdapter: DBALAdapter = {
}
},
async findFirst(entity: string, options?: { where?: Record<string, unknown> }): Promise<unknown | null> {
const model = getModel(entity)
const where = options?.where ? buildWhereClause(options.where) : undefined
return model.findFirst({ where })
},
async upsert(entity: string, options: { where: Record<string, unknown>; update: Record<string, unknown>; create: Record<string, unknown> }): Promise<unknown> {
const model = getModel(entity)
return model.upsert({
where: options.where,
update: options.update,
create: options.create,
})
},
async close(): Promise<void> {
await prisma.$disconnect()
},

View File

@@ -1,14 +0,0 @@
import { prisma } from '../../prisma'
/**
* Get all credentials as username->hash map
* @returns Record of username to password hash
*/
export const getCredentials = async (): Promise<Record<string, string>> => {
const credentials = await prisma.credential.findMany()
const result: Record<string, string> = {}
for (const cred of credentials) {
result[cred.username] = cred.passwordHash
}
return result
}

View File

@@ -1,19 +0,0 @@
import { prisma } from '../../prisma'
/**
* Get password change timestamps for all users
* @returns Record of username to timestamp
*/
export const getPasswordChangeTimestamps = async (): Promise<Record<string, number>> => {
const users = await prisma.user.findMany({
where: { passwordChangeTimestamp: { not: null } },
select: { username: true, passwordChangeTimestamp: true },
})
const result: Record<string, number> = {}
for (const user of users) {
if (user.passwordChangeTimestamp) {
result[user.username] = Number(user.passwordChangeTimestamp)
}
}
return result
}

View File

@@ -1,5 +0,0 @@
export { getCredentials } from './get-credentials'
export { setCredential } from './set-credential'
export { verifyCredentials } from './verify-credentials'
export { getPasswordChangeTimestamps } from './get-password-change-timestamps'
export { setPasswordChangeTimestamps } from './set-password-change-timestamps'

View File

@@ -1,19 +0,0 @@
import { prisma } from '../../prisma'
/**
* Set or update a credential for a user
* @param username - The username
* @param passwordHash - The hashed password
*/
export const setCredential = async (username: string, passwordHash: string): Promise<void> => {
await prisma.credential.upsert({
where: { username },
update: { passwordHash },
create: { username, passwordHash },
})
await prisma.user.update({
where: { username },
data: { passwordChangeTimestamp: BigInt(Date.now()) },
})
}

View File

@@ -1,14 +0,0 @@
import { prisma } from '../../prisma'
/**
* Set password change timestamps
* @param timestamps - Record of username to timestamp
*/
export const setPasswordChangeTimestamps = async (timestamps: Record<string, number>): Promise<void> => {
for (const [username, timestamp] of Object.entries(timestamps)) {
await prisma.user.update({
where: { username },
data: { passwordChangeTimestamp: BigInt(timestamp) },
})
}
}

View File

@@ -1,14 +0,0 @@
import { prisma } from '../../prisma'
import { verifyPassword } from '../verify-password'
/**
* Verify user credentials
* @param username - The username
* @param password - The plain text password
* @returns True if credentials are valid
*/
export const verifyCredentials = async (username: string, password: string): Promise<boolean> => {
const credential = await prisma.credential.findUnique({ where: { username } })
if (!credential) return false
return await verifyPassword(password, credential.passwordHash)
}

View File

@@ -1,13 +0,0 @@
/**
* Hash a password using SHA-512
* @param password - The plain text password
* @returns The hashed password as a hex string
*/
export const hashPassword = async (password: string): Promise<string> => {
const encoder = new TextEncoder()
const data = encoder.encode(password)
const hashBuffer = await crypto.subtle.digest('SHA-512', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
return hashHex
}

View File

@@ -1,25 +0,0 @@
// Core functions
export { hashPassword } from './hash-password'
export { verifyPassword } from './verify-password'
export { initializeDatabase } from './initialize-database'
// Users
export * from './users'
// Credentials
export * from './credentials'
// Workflows
export * from './workflows'
// Lua Scripts
export * from './lua-scripts'
// Pages
export * from './pages'
// Schemas
export * from './schemas'
// Tenants
export * from './tenants'

View File

@@ -1,14 +0,0 @@
import { prisma } from '../../prisma'
/**
* Initialize database connection
*/
export const initializeDatabase = async (): Promise<void> => {
try {
await prisma.$connect()
console.log('Database initialized successfully')
} catch (error) {
console.error('Failed to initialize database:', error)
throw error
}
}

View File

@@ -1,19 +0,0 @@
import { prisma } from '../../prisma'
import type { LuaScript } from '../../../types/level-types'
/**
* Add a single Lua script
* @param script - The script to add
*/
export const addLuaScript = async (script: LuaScript): Promise<void> => {
await prisma.luaScript.create({
data: {
id: script.id,
name: script.name,
description: script.description,
code: script.code,
parameters: JSON.stringify(script.parameters),
returnType: script.returnType,
},
})
}

View File

@@ -1,9 +0,0 @@
import { prisma } from '../../prisma'
/**
* Delete a Lua script by ID
* @param scriptId - The script ID
*/
export const deleteLuaScript = async (scriptId: string): Promise<void> => {
await prisma.luaScript.delete({ where: { id: scriptId } })
}

View File

@@ -1,18 +0,0 @@
import { prisma } from '../../prisma'
import type { LuaScript } from '../../../types/level-types'
/**
* Get all Lua scripts
* @returns Array of Lua scripts
*/
export const getLuaScripts = async (): Promise<LuaScript[]> => {
const scripts = await prisma.luaScript.findMany()
return scripts.map(s => ({
id: s.id,
name: s.name,
description: s.description || undefined,
code: s.code,
parameters: JSON.parse(s.parameters),
returnType: s.returnType || undefined,
}))
}

View File

@@ -1,5 +0,0 @@
export { getLuaScripts } from './get-lua-scripts'
export { setLuaScripts } from './set-lua-scripts'
export { addLuaScript } from './add-lua-script'
export { updateLuaScript } from './update-lua-script'
export { deleteLuaScript } from './delete-lua-script'

View File

@@ -1,22 +0,0 @@
import { prisma } from '../../prisma'
import type { LuaScript } from '../../../types/level-types'
/**
* Set all Lua scripts (replaces existing)
* @param scripts - Array of Lua scripts
*/
export const setLuaScripts = async (scripts: LuaScript[]): Promise<void> => {
await prisma.luaScript.deleteMany()
for (const script of scripts) {
await prisma.luaScript.create({
data: {
id: script.id,
name: script.name,
description: script.description,
code: script.code,
parameters: JSON.stringify(script.parameters),
returnType: script.returnType,
},
})
}
}

View File

@@ -1,21 +0,0 @@
import { prisma } from '../../prisma'
import type { LuaScript } from '../../../types/level-types'
/**
* Update a Lua script by ID
* @param scriptId - The script ID
* @param updates - Partial script data
*/
export const updateLuaScript = async (scriptId: string, updates: Partial<LuaScript>): Promise<void> => {
const data: any = {}
if (updates.name !== undefined) data.name = updates.name
if (updates.description !== undefined) data.description = updates.description
if (updates.code !== undefined) data.code = updates.code
if (updates.parameters !== undefined) data.parameters = JSON.stringify(updates.parameters)
if (updates.returnType !== undefined) data.returnType = updates.returnType
await prisma.luaScript.update({
where: { id: scriptId },
data,
})
}

View File

@@ -1,20 +0,0 @@
import { prisma } from '../../prisma'
import type { PageConfig } from '../../../types/level-types'
/**
* Add a single page config
* @param page - The page to add
*/
export const addPage = async (page: PageConfig): Promise<void> => {
await prisma.pageConfig.create({
data: {
id: page.id,
path: page.path,
title: page.title,
level: page.level,
componentTree: JSON.stringify(page.componentTree),
requiresAuth: page.requiresAuth,
requiredRole: page.requiredRole,
},
})
}

View File

@@ -1,9 +0,0 @@
import { prisma } from '../../prisma'
/**
* Delete a page config by ID
* @param pageId - The page ID
*/
export const deletePage = async (pageId: string): Promise<void> => {
await prisma.pageConfig.delete({ where: { id: pageId } })
}

View File

@@ -1,19 +0,0 @@
import { prisma } from '../../prisma'
import type { PageConfig } from '../../../types/level-types'
/**
* Get all page configs
* @returns Array of page configs
*/
export const getPages = async (): Promise<PageConfig[]> => {
const pages = await prisma.pageConfig.findMany()
return pages.map(p => ({
id: p.id,
path: p.path,
title: p.title,
level: p.level as any,
componentTree: JSON.parse(p.componentTree),
requiresAuth: p.requiresAuth,
requiredRole: (p.requiredRole as any) || undefined,
}))
}

View File

@@ -1,5 +0,0 @@
export { getPages } from './get-pages'
export { setPages } from './set-pages'
export { addPage } from './add-page'
export { updatePage } from './update-page'
export { deletePage } from './delete-page'

View File

@@ -1,23 +0,0 @@
import { prisma } from '../../prisma'
import type { PageConfig } from '../../../types/level-types'
/**
* Set all page configs (replaces existing)
* @param pages - Array of page configs
*/
export const setPages = async (pages: PageConfig[]): Promise<void> => {
await prisma.pageConfig.deleteMany()
for (const page of pages) {
await prisma.pageConfig.create({
data: {
id: page.id,
path: page.path,
title: page.title,
level: page.level,
componentTree: JSON.stringify(page.componentTree),
requiresAuth: page.requiresAuth,
requiredRole: page.requiredRole,
},
})
}
}

View File

@@ -1,22 +0,0 @@
import { prisma } from '../../prisma'
import type { PageConfig } from '../../../types/level-types'
/**
* Update a page config by ID
* @param pageId - The page ID
* @param updates - Partial page data
*/
export const updatePage = async (pageId: string, updates: Partial<PageConfig>): Promise<void> => {
const data: any = {}
if (updates.path !== undefined) data.path = updates.path
if (updates.title !== undefined) data.title = updates.title
if (updates.level !== undefined) data.level = updates.level
if (updates.componentTree !== undefined) data.componentTree = JSON.stringify(updates.componentTree)
if (updates.requiresAuth !== undefined) data.requiresAuth = updates.requiresAuth
if (updates.requiredRole !== undefined) data.requiredRole = updates.requiredRole
await prisma.pageConfig.update({
where: { id: pageId },
data,
})
}

View File

@@ -1,22 +0,0 @@
import { prisma } from '../../prisma'
import type { ModelSchema } from '../../types/schema-types'
/**
* Add a single model schema
* @param schema - The schema to add
*/
export const addSchema = async (schema: ModelSchema): Promise<void> => {
await prisma.modelSchema.create({
data: {
name: schema.name,
label: schema.label,
labelPlural: schema.labelPlural,
icon: schema.icon,
fields: JSON.stringify(schema.fields),
listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null,
listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null,
searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null,
ordering: schema.ordering ? JSON.stringify(schema.ordering) : null,
},
})
}

View File

@@ -1,9 +0,0 @@
import { prisma } from '../../prisma'
/**
* Delete a model schema by name
* @param schemaName - The schema name
*/
export const deleteSchema = async (schemaName: string): Promise<void> => {
await prisma.modelSchema.delete({ where: { name: schemaName } })
}

View File

@@ -1,21 +0,0 @@
import { prisma } from '../../prisma'
import type { ModelSchema } from '../../types/schema-types'
/**
* Get all model schemas
* @returns Array of model schemas
*/
export const getSchemas = async (): Promise<ModelSchema[]> => {
const schemas = await prisma.modelSchema.findMany()
return schemas.map(s => ({
name: s.name,
label: s.label || undefined,
labelPlural: s.labelPlural || undefined,
icon: s.icon || undefined,
fields: JSON.parse(s.fields),
listDisplay: s.listDisplay ? JSON.parse(s.listDisplay) : undefined,
listFilter: s.listFilter ? JSON.parse(s.listFilter) : undefined,
searchFields: s.searchFields ? JSON.parse(s.searchFields) : undefined,
ordering: s.ordering ? JSON.parse(s.ordering) : undefined,
}))
}

View File

@@ -1,5 +0,0 @@
export { getSchemas } from './get-schemas'
export { setSchemas } from './set-schemas'
export { addSchema } from './add-schema'
export { updateSchema } from './update-schema'
export { deleteSchema } from './delete-schema'

View File

@@ -1,25 +0,0 @@
import { prisma } from '../../prisma'
import type { ModelSchema } from '../../types/schema-types'
/**
* Set all model schemas (replaces existing)
* @param schemas - Array of model schemas
*/
export const setSchemas = async (schemas: ModelSchema[]): Promise<void> => {
await prisma.modelSchema.deleteMany()
for (const schema of schemas) {
await prisma.modelSchema.create({
data: {
name: schema.name,
label: schema.label,
labelPlural: schema.labelPlural,
icon: schema.icon,
fields: JSON.stringify(schema.fields),
listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null,
listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null,
searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null,
ordering: schema.ordering ? JSON.stringify(schema.ordering) : null,
},
})
}
}

View File

@@ -1,24 +0,0 @@
import { prisma } from '../../prisma'
import type { ModelSchema } from '../../types/schema-types'
/**
* Update a model schema by name
* @param schemaName - The schema name
* @param updates - Partial schema data
*/
export const updateSchema = async (schemaName: string, updates: Partial<ModelSchema>): Promise<void> => {
const data: any = {}
if (updates.label !== undefined) data.label = updates.label
if (updates.labelPlural !== undefined) data.labelPlural = updates.labelPlural
if (updates.icon !== undefined) data.icon = updates.icon
if (updates.fields !== undefined) data.fields = JSON.stringify(updates.fields)
if (updates.listDisplay !== undefined) data.listDisplay = JSON.stringify(updates.listDisplay)
if (updates.listFilter !== undefined) data.listFilter = JSON.stringify(updates.listFilter)
if (updates.searchFields !== undefined) data.searchFields = JSON.stringify(updates.searchFields)
if (updates.ordering !== undefined) data.ordering = JSON.stringify(updates.ordering)
await prisma.modelSchema.update({
where: { name: schemaName },
data,
})
}

View File

@@ -1,18 +0,0 @@
import { prisma } from '../../prisma'
import type { Tenant } from '../../../types/level-types'
/**
* Add a single tenant
* @param tenant - The tenant to add
*/
export const addTenant = async (tenant: Tenant): Promise<void> => {
await prisma.tenant.create({
data: {
id: tenant.id,
name: tenant.name,
ownerId: tenant.ownerId,
createdAt: BigInt(tenant.createdAt),
homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null,
},
})
}

View File

@@ -1,9 +0,0 @@
import { prisma } from '../../prisma'
/**
* Delete a tenant by ID
* @param tenantId - The tenant ID
*/
export const deleteTenant = async (tenantId: string): Promise<void> => {
await prisma.tenant.delete({ where: { id: tenantId } })
}

View File

@@ -1,17 +0,0 @@
import { prisma } from '../../prisma'
import type { Tenant } from '../../../types/level-types'
/**
* Get all tenants
* @returns Array of tenants
*/
export const getTenants = async (): Promise<Tenant[]> => {
const tenants = await prisma.tenant.findMany()
return tenants.map(t => ({
id: t.id,
name: t.name,
ownerId: t.ownerId,
createdAt: Number(t.createdAt),
homepageConfig: t.homepageConfig ? JSON.parse(t.homepageConfig) : undefined,
}))
}

View File

@@ -1,5 +0,0 @@
export { getTenants } from './get-tenants'
export { setTenants } from './set-tenants'
export { addTenant } from './add-tenant'
export { updateTenant } from './update-tenant'
export { deleteTenant } from './delete-tenant'

View File

@@ -1,21 +0,0 @@
import { prisma } from '../../prisma'
import type { Tenant } from '../../../types/level-types'
/**
* Set all tenants (replaces existing)
* @param tenants - Array of tenants
*/
export const setTenants = async (tenants: Tenant[]): Promise<void> => {
await prisma.tenant.deleteMany()
for (const tenant of tenants) {
await prisma.tenant.create({
data: {
id: tenant.id,
name: tenant.name,
ownerId: tenant.ownerId,
createdAt: BigInt(tenant.createdAt),
homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null,
},
})
}
}

View File

@@ -1,19 +0,0 @@
import { prisma } from '../../prisma'
import type { Tenant } from '../../../types/level-types'
/**
* Update a tenant by ID
* @param tenantId - The tenant ID
* @param updates - Partial tenant data
*/
export const updateTenant = async (tenantId: string, updates: Partial<Tenant>): Promise<void> => {
const data: any = {}
if (updates.name !== undefined) data.name = updates.name
if (updates.ownerId !== undefined) data.ownerId = updates.ownerId
if (updates.homepageConfig !== undefined) data.homepageConfig = JSON.stringify(updates.homepageConfig)
await prisma.tenant.update({
where: { id: tenantId },
data,
})
}

View File

@@ -1,24 +0,0 @@
import { prisma } from '../../prisma'
import type { User } from '../../../types/level-types'
/**
* Add a single user
* @param user - The user to add
*/
export const addUser = async (user: User): Promise<void> => {
await prisma.user.create({
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
profilePicture: user.profilePicture,
bio: user.bio,
createdAt: BigInt(user.createdAt),
tenantId: user.tenantId,
isInstanceOwner: user.isInstanceOwner || false,
passwordChangeTimestamp: null,
firstLogin: false,
},
})
}

View File

@@ -1,9 +0,0 @@
import { prisma } from '../../prisma'
/**
* Delete a user by ID
* @param userId - The user ID to delete
*/
export const deleteUser = async (userId: string): Promise<void> => {
await prisma.user.delete({ where: { id: userId } })
}

View File

@@ -1,21 +0,0 @@
import { prisma } from '../../prisma'
import type { User } from '../../../types/level-types'
/**
* Get all users from database
* @returns Array of users
*/
export const getUsers = async (): Promise<User[]> => {
const users = await prisma.user.findMany()
return users.map(u => ({
id: u.id,
username: u.username,
email: u.email,
role: u.role as any,
profilePicture: u.profilePicture || undefined,
bio: u.bio || undefined,
createdAt: Number(u.createdAt),
tenantId: u.tenantId || undefined,
isInstanceOwner: u.isInstanceOwner,
}))
}

View File

@@ -1,5 +0,0 @@
export { getUsers } from './get-users'
export { setUsers } from './set-users'
export { addUser } from './add-user'
export { updateUser } from './update-user'
export { deleteUser } from './delete-user'

View File

@@ -1,27 +0,0 @@
import { prisma } from '../../prisma'
import type { User } from '../../../types/level-types'
/**
* Set all users (replaces existing)
* @param users - Array of users to set
*/
export const setUsers = async (users: User[]): Promise<void> => {
await prisma.user.deleteMany()
for (const user of users) {
await prisma.user.create({
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
profilePicture: user.profilePicture,
bio: user.bio,
createdAt: BigInt(user.createdAt),
tenantId: user.tenantId,
isInstanceOwner: user.isInstanceOwner || false,
passwordChangeTimestamp: null,
firstLogin: false,
},
})
}
}

View File

@@ -1,23 +0,0 @@
import { prisma } from '../../prisma'
import type { User } from '../../../types/level-types'
/**
* Update a user by ID
* @param userId - The user ID to update
* @param updates - Partial user data to update
*/
export const updateUser = async (userId: string, updates: Partial<User>): Promise<void> => {
const data: any = {}
if (updates.username !== undefined) data.username = updates.username
if (updates.email !== undefined) data.email = updates.email
if (updates.role !== undefined) data.role = updates.role
if (updates.profilePicture !== undefined) data.profilePicture = updates.profilePicture
if (updates.bio !== undefined) data.bio = updates.bio
if (updates.tenantId !== undefined) data.tenantId = updates.tenantId
if (updates.isInstanceOwner !== undefined) data.isInstanceOwner = updates.isInstanceOwner
await prisma.user.update({
where: { id: userId },
data,
})
}

View File

@@ -1,12 +0,0 @@
import { hashPassword } from './hash-password'
/**
* Verify a password against a hash
* @param password - The plain text password
* @param hash - The hash to compare against
* @returns True if password matches hash
*/
export const verifyPassword = async (password: string, hash: string): Promise<boolean> => {
const inputHash = await hashPassword(password)
return inputHash === hash
}

View File

@@ -1,19 +0,0 @@
import { prisma } from '../../prisma'
import type { Workflow } from '../../../types/level-types'
/**
* Add a single workflow
* @param workflow - The workflow to add
*/
export const addWorkflow = async (workflow: Workflow): Promise<void> => {
await prisma.workflow.create({
data: {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodes: JSON.stringify(workflow.nodes),
edges: JSON.stringify(workflow.edges),
enabled: workflow.enabled,
},
})
}

View File

@@ -1,9 +0,0 @@
import { prisma } from '../../prisma'
/**
* Delete a workflow by ID
* @param workflowId - The workflow ID
*/
export const deleteWorkflow = async (workflowId: string): Promise<void> => {
await prisma.workflow.delete({ where: { id: workflowId } })
}

View File

@@ -1,18 +0,0 @@
import { prisma } from '../../prisma'
import type { Workflow } from '../../../types/level-types'
/**
* Get all workflows
* @returns Array of workflows
*/
export const getWorkflows = async (): Promise<Workflow[]> => {
const workflows = await prisma.workflow.findMany()
return workflows.map(w => ({
id: w.id,
name: w.name,
description: w.description || undefined,
nodes: JSON.parse(w.nodes),
edges: JSON.parse(w.edges),
enabled: w.enabled,
}))
}

View File

@@ -1,5 +0,0 @@
export { getWorkflows } from './get-workflows'
export { setWorkflows } from './set-workflows'
export { addWorkflow } from './add-workflow'
export { updateWorkflow } from './update-workflow'
export { deleteWorkflow } from './delete-workflow'

View File

@@ -1,22 +0,0 @@
import { prisma } from '../../prisma'
import type { Workflow } from '../../../types/level-types'
/**
* Set all workflows (replaces existing)
* @param workflows - Array of workflows
*/
export const setWorkflows = async (workflows: Workflow[]): Promise<void> => {
await prisma.workflow.deleteMany()
for (const workflow of workflows) {
await prisma.workflow.create({
data: {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodes: JSON.stringify(workflow.nodes),
edges: JSON.stringify(workflow.edges),
enabled: workflow.enabled,
},
})
}
}

View File

@@ -1,21 +0,0 @@
import { prisma } from '../../prisma'
import type { Workflow } from '../../../types/level-types'
/**
* Update a workflow by ID
* @param workflowId - The workflow ID
* @param updates - Partial workflow data
*/
export const updateWorkflow = async (workflowId: string, updates: Partial<Workflow>): Promise<void> => {
const data: any = {}
if (updates.name !== undefined) data.name = updates.name
if (updates.description !== undefined) data.description = updates.description
if (updates.nodes !== undefined) data.nodes = JSON.stringify(updates.nodes)
if (updates.edges !== undefined) data.edges = JSON.stringify(updates.edges)
if (updates.enabled !== undefined) data.enabled = updates.enabled
await prisma.workflow.update({
where: { id: workflowId },
data,
})
}

View File

@@ -7,6 +7,6 @@ export async function getGodCredentialsExpiryDuration(): Promise<number> {
const adapter = getAdapter()
const config = await adapter.findFirst('SystemConfig', {
where: { key: 'god_credentials_expiry_duration' },
})
}) as { value: string } | null
return config ? Number(config.value) : 60 * 60 * 1000 // Default 1 hour
}

View File

@@ -7,6 +7,6 @@ export async function getGodCredentialsExpiry(): Promise<number> {
const adapter = getAdapter()
const config = await adapter.findFirst('SystemConfig', {
where: { key: 'god_credentials_expiry' },
})
}) as { value: string } | null
return config ? Number(config.value) : 0
}

View File

@@ -8,7 +8,7 @@ export async function setFirstLoginFlag(username: string, isFirstLogin: boolean)
// Find the user first
const user = await adapter.findFirst('User', {
where: { username },
})
}) as { id: string } | null
if (user) {
await adapter.update('User', user.id, { firstLogin: isFirstLogin })
}

View File

@@ -13,7 +13,7 @@ export async function shouldShowGodCredentials(): Promise<boolean> {
// Get god user's password change timestamp
const godUser = await adapter.findFirst('User', {
where: { username: 'god' },
})
}) as { passwordChangeTimestamp?: bigint | number } | null
const godPasswordChangeTime = godUser?.passwordChangeTimestamp ? Number(godUser.passwordChangeTimestamp) : 0
if (expiry === 0) {

View File

@@ -131,4 +131,62 @@ export class Database {
static addComponentConfig = components.addComponentConfig
static updateComponentConfig = components.updateComponentConfig
static deleteComponentConfig = components.deleteComponentConfig
// CSS Classes
static getCssClasses = cssClasses.getCssClasses
static setCssClasses = cssClasses.setCssClasses
static addCssCategory = cssClasses.addCssCategory
static updateCssCategory = cssClasses.updateCssCategory
static deleteCssCategory = cssClasses.deleteCssCategory
// Dropdown Configs
static getDropdownConfigs = dropdownConfigs.getDropdownConfigs
static setDropdownConfigs = dropdownConfigs.setDropdownConfigs
static addDropdownConfig = dropdownConfigs.addDropdownConfig
static updateDropdownConfig = dropdownConfigs.updateDropdownConfig
static deleteDropdownConfig = dropdownConfigs.deleteDropdownConfig
// Tenants
static getTenants = tenants.getTenants
static setTenants = tenants.setTenants
static addTenant = tenants.addTenant
static updateTenant = tenants.updateTenant
static deleteTenant = tenants.deleteTenant
// Packages
static getInstalledPackages = packages.getInstalledPackages
static setInstalledPackages = packages.setInstalledPackages
static installPackage = packages.installPackage
static uninstallPackage = packages.uninstallPackage
static togglePackageEnabled = packages.togglePackageEnabled
static getPackageData = packages.getPackageData
static setPackageData = packages.setPackageData
static deletePackageData = packages.deletePackageData
// Power Transfers
static getPowerTransferRequests = powerTransfers.getPowerTransferRequests
static setPowerTransferRequests = powerTransfers.setPowerTransferRequests
static addPowerTransferRequest = powerTransfers.addPowerTransferRequest
static updatePowerTransferRequest = powerTransfers.updatePowerTransferRequest
static deletePowerTransferRequest = powerTransfers.deletePowerTransferRequest
// SMTP Config
static getSMTPConfig = smtpConfig.getSMTPConfig
static setSMTPConfig = smtpConfig.setSMTPConfig
// God Credentials
static getGodCredentialsExpiry = godCredentials.getGodCredentialsExpiry
static setGodCredentialsExpiry = godCredentials.setGodCredentialsExpiry
static getFirstLoginFlags = godCredentials.getFirstLoginFlags
static setFirstLoginFlag = godCredentials.setFirstLoginFlag
static getGodCredentialsExpiryDuration = godCredentials.getGodCredentialsExpiryDuration
static setGodCredentialsExpiryDuration = godCredentials.setGodCredentialsExpiryDuration
static shouldShowGodCredentials = godCredentials.shouldShowGodCredentials
static resetGodCredentialsExpiry = godCredentials.resetGodCredentialsExpiry
// Database Admin
static clearDatabase = databaseAdmin.clearDatabase
static exportDatabase = databaseAdmin.exportDatabase
static importDatabase = databaseAdmin.importDatabase
static seedDefaultData = databaseAdmin.seedDefaultData
}

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../dbal-client'
import type { InstalledPackage } from '../../package-types'
import type { InstalledPackage } from '../../packages/package-types'
/**
* Get all installed packages from database

View File

@@ -7,7 +7,7 @@ export async function getPackageData(packageId: string): Promise<Record<string,
const adapter = getAdapter()
const pkg = await adapter.findFirst('PackageData', {
where: { packageId },
})
}) as { data: string } | null
if (!pkg) return {}
return typeof pkg.data === 'string' ? JSON.parse(pkg.data) : pkg.data
return JSON.parse(pkg.data)
}

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../dbal-client'
import type { InstalledPackage } from '../../package-types'
import type { InstalledPackage } from '../../packages/package-types'
/**
* Install a package (creates record if not exists)

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../dbal-client'
import type { InstalledPackage } from '../../package-types'
import type { InstalledPackage } from '../../packages/package-types'
/**
* Set all installed packages (replaces existing)

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../dbal-client'
import type { SMTPConfig } from '../../password-utils'
import type { SMTPConfig } from '../../password'
/**
* Get SMTP configuration

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../dbal-client'
import type { SMTPConfig } from '../../password-utils'
import type { SMTPConfig } from '../../password'
/**
* Set SMTP configuration (replaces existing)

View File

@@ -0,0 +1,70 @@
import { LUA_SNIPPETS, LUA_SNIPPET_CATEGORIES, type LuaSnippet } from './snippets/lua-snippets-data'
import { getSnippetsByCategory } from './functions/get-snippets-by-category'
import { searchSnippets } from './functions/search-snippets'
import { getSnippetById } from './functions/get-snippet-by-id'
/**
* LuaSnippetUtils - Class wrapper for Lua snippet utility functions
*
* This class serves as a container for lambda functions related to Lua snippets.
* 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 LuaSnippetUtils.methodName() or import individual functions
*/
export class LuaSnippetUtils {
/** All available snippet categories */
static readonly CATEGORIES = LUA_SNIPPET_CATEGORIES
/** All available snippets */
static readonly ALL_SNIPPETS = LUA_SNIPPETS
/**
* Get snippets filtered by category
*/
static getByCategory = getSnippetsByCategory
/**
* Search snippets by query string
*/
static search = searchSnippets
/**
* Get a snippet by its ID
*/
static getById = getSnippetById
/**
* Get count of snippets per category
*/
static getCategoryCounts(): Record<string, number> {
const counts: Record<string, number> = { All: LUA_SNIPPETS.length }
for (const snippet of LUA_SNIPPETS) {
counts[snippet.category] = (counts[snippet.category] || 0) + 1
}
return counts
}
/**
* Get all unique tags across snippets
*/
static getAllTags(): string[] {
const tagSet = new Set<string>()
for (const snippet of LUA_SNIPPETS) {
for (const tag of snippet.tags) {
tagSet.add(tag)
}
}
return Array.from(tagSet).sort()
}
}
// Re-export types for convenience
export type { LuaSnippet }
export { LUA_SNIPPET_CATEGORIES, LUA_SNIPPETS }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,981 @@
export interface LuaSnippet {
id: string
name: string
description: string
category: string
code: string
tags: string[]
parameters?: Array<{ name: string; type: string; description: string }>
}
export const LUA_SNIPPET_CATEGORIES = [
'All',
'Data Validation',
'Data Transformation',
'Array Operations',
'String Processing',
'Math & Calculations',
'Conditionals & Logic',
'User Management',
'Error Handling',
'API & Networking',
'Date & Time',
'File Operations',
'Utilities'
] as const
export const LUA_SNIPPETS: LuaSnippet[] = [
{
id: 'validate_email',
name: 'Email Validation',
description: 'Validate email format using pattern matching',
category: 'Data Validation',
tags: ['validation', 'email', 'regex'],
parameters: [
{ name: 'email', type: 'string', description: 'Email address to validate' }
],
code: `local email = context.data.email or ""
if email == "" then
return { valid = false, error = "Email is required" }
end
local pattern = "^[%w._%%-]+@[%w._%%-]+%.%w+$"
if not string.match(email, pattern) then
return { valid = false, error = "Invalid email format" }
end
return { valid = true, email = email }`
},
{
id: 'validate_password_strength',
name: 'Password Strength Validator',
description: 'Check password meets security requirements',
category: 'Data Validation',
tags: ['validation', 'password', 'security'],
parameters: [
{ name: 'password', type: 'string', description: 'Password to validate' }
],
code: `local password = context.data.password or ""
if string.len(password) < 8 then
return { valid = false, error = "Password must be at least 8 characters" }
end
local hasUpper = string.match(password, "%u") ~= nil
local hasLower = string.match(password, "%l") ~= nil
local hasDigit = string.match(password, "%d") ~= nil
local hasSpecial = string.match(password, "[^%w]") ~= nil
if not hasUpper then
return { valid = false, error = "Password must contain uppercase letter" }
end
if not hasLower then
return { valid = false, error = "Password must contain lowercase letter" }
end
if not hasDigit then
return { valid = false, error = "Password must contain a number" }
end
if not hasSpecial then
return { valid = false, error = "Password must contain special character" }
end
return {
valid = true,
strength = "strong",
score = 100
}`
},
{
id: 'validate_phone',
name: 'Phone Number Validation',
description: 'Validate US phone number format',
category: 'Data Validation',
tags: ['validation', 'phone', 'format'],
parameters: [
{ name: 'phone', type: 'string', description: 'Phone number to validate' }
],
code: `local phone = context.data.phone or ""
local cleaned = string.gsub(phone, "[^%d]", "")
if string.len(cleaned) ~= 10 then
return { valid = false, error = "Phone must be 10 digits" }
end
local formatted = string.format("(%s) %s-%s",
string.sub(cleaned, 1, 3),
string.sub(cleaned, 4, 6),
string.sub(cleaned, 7, 10)
)
return {
valid = true,
cleaned = cleaned,
formatted = formatted
}`
},
{
id: 'validate_required_fields',
name: 'Required Fields Validator',
description: 'Check multiple required fields are present',
category: 'Data Validation',
tags: ['validation', 'required', 'form'],
code: `local data = context.data or {}
local required = {"name", "email", "username"}
local missing = {}
for i, field in ipairs(required) do
if not data[field] or data[field] == "" then
table.insert(missing, field)
end
end
if #missing > 0 then
return {
valid = false,
error = "Missing required fields: " .. table.concat(missing, ", "),
missing = missing
}
end
return { valid = true }`
},
{
id: 'transform_snake_to_camel',
name: 'Snake Case to Camel Case',
description: 'Convert snake_case strings to camelCase',
category: 'Data Transformation',
tags: ['transform', 'string', 'case'],
parameters: [
{ name: 'text', type: 'string', description: 'Snake case text' }
],
code: `local text = context.data.text or ""
local result = string.gsub(text, "_(%w)", function(c)
return string.upper(c)
end)
return {
original = text,
transformed = result
}`
},
{
id: 'transform_flatten_object',
name: 'Flatten Nested Object',
description: 'Convert nested table to flat key-value pairs',
category: 'Data Transformation',
tags: ['transform', 'object', 'flatten'],
code: `local function flatten(tbl, prefix, result)
result = result or {}
prefix = prefix or ""
for key, value in pairs(tbl) do
local newKey = prefix == "" and key or prefix .. "." .. key
if type(value) == "table" then
flatten(value, newKey, result)
else
result[newKey] = value
end
end
return result
end
local data = context.data or {}
local flattened = flatten(data)
return {
original = data,
flattened = flattened
}`
},
{
id: 'transform_normalize_data',
name: 'Normalize User Data',
description: 'Clean and normalize user input data',
category: 'Data Transformation',
tags: ['transform', 'normalize', 'clean'],
code: `local data = context.data or {}
local function trim(s)
return string.match(s, "^%s*(.-)%s*$")
end
local normalized = {}
if data.email then
normalized.email = string.lower(trim(data.email))
end
if data.name then
normalized.name = trim(data.name)
local words = {}
for word in string.gmatch(normalized.name, "%S+") do
table.insert(words, string.upper(string.sub(word, 1, 1)) .. string.lower(string.sub(word, 2)))
end
normalized.name = table.concat(words, " ")
end
if data.phone then
normalized.phone = string.gsub(data.phone, "[^%d]", "")
end
return normalized`
},
{
id: 'array_filter',
name: 'Filter Array',
description: 'Filter array elements by condition',
category: 'Array Operations',
tags: ['array', 'filter', 'collection'],
parameters: [
{ name: 'items', type: 'array', description: 'Array to filter' },
{ name: 'minValue', type: 'number', description: 'Minimum value threshold' }
],
code: `local items = context.data.items or {}
local minValue = context.data.minValue or 0
local filtered = {}
for i, item in ipairs(items) do
if item.value >= minValue then
table.insert(filtered, item)
end
end
log("Filtered " .. #filtered .. " of " .. #items .. " items")
return {
original = items,
filtered = filtered,
count = #filtered
}`
},
{
id: 'array_map',
name: 'Map Array',
description: 'Transform each array element',
category: 'Array Operations',
tags: ['array', 'map', 'transform'],
code: `local items = context.data.items or {}
local mapped = {}
for i, item in ipairs(items) do
table.insert(mapped, {
id = item.id,
label = string.upper(item.name or ""),
value = (item.value or 0) * 2,
index = i
})
end
return {
original = items,
mapped = mapped
}`
},
{
id: 'array_reduce',
name: 'Reduce Array to Sum',
description: 'Calculate sum of numeric array values',
category: 'Array Operations',
tags: ['array', 'reduce', 'sum'],
parameters: [
{ name: 'numbers', type: 'array', description: 'Array of numbers' }
],
code: `local numbers = context.data.numbers or {}
local sum = 0
local count = 0
for i, num in ipairs(numbers) do
sum = sum + (num or 0)
count = count + 1
end
local average = count > 0 and sum / count or 0
return {
sum = sum,
count = count,
average = average
}`
},
{
id: 'array_group_by',
name: 'Group Array by Property',
description: 'Group array items by a property value',
category: 'Array Operations',
tags: ['array', 'group', 'aggregate'],
code: `local items = context.data.items or {}
local groupKey = context.data.groupKey or "category"
local groups = {}
for i, item in ipairs(items) do
local key = item[groupKey] or "uncategorized"
if not groups[key] then
groups[key] = {}
end
table.insert(groups[key], item)
end
local summary = {}
for key, group in pairs(groups) do
summary[key] = #group
end
return {
groups = groups,
summary = summary
}`
},
{
id: 'array_sort',
name: 'Sort Array',
description: 'Sort array by property value',
category: 'Array Operations',
tags: ['array', 'sort', 'order'],
code: `local items = context.data.items or {}
local sortKey = context.data.sortKey or "value"
local descending = context.data.descending or false
table.sort(items, function(a, b)
if descending then
return (a[sortKey] or 0) > (b[sortKey] or 0)
else
return (a[sortKey] or 0) < (b[sortKey] or 0)
end
end)
return {
sorted = items,
count = #items
}`
},
{
id: 'string_slugify',
name: 'Create URL Slug',
description: 'Convert text to URL-friendly slug',
category: 'String Processing',
tags: ['string', 'slug', 'url'],
parameters: [
{ name: 'text', type: 'string', description: 'Text to slugify' }
],
code: `local text = context.data.text or ""
local slug = string.lower(text)
slug = string.gsub(slug, "%s+", "-")
slug = string.gsub(slug, "[^%w%-]", "")
slug = string.gsub(slug, "%-+", "-")
slug = string.gsub(slug, "^%-+", "")
slug = string.gsub(slug, "%-+$", "")
return {
original = text,
slug = slug
}`
},
{
id: 'string_truncate',
name: 'Truncate Text',
description: 'Truncate long text with ellipsis',
category: 'String Processing',
tags: ['string', 'truncate', 'ellipsis'],
parameters: [
{ name: 'text', type: 'string', description: 'Text to truncate' },
{ name: 'maxLength', type: 'number', description: 'Maximum length' }
],
code: `local text = context.data.text or ""
local maxLength = context.data.maxLength or 50
if string.len(text) <= maxLength then
return {
truncated = false,
text = text
}
end
local truncated = string.sub(text, 1, maxLength - 3) .. "..."
return {
truncated = true,
text = truncated,
originalLength = string.len(text)
}`
},
{
id: 'string_extract_hashtags',
name: 'Extract Hashtags',
description: 'Find all hashtags in text',
category: 'String Processing',
tags: ['string', 'parse', 'hashtags'],
parameters: [
{ name: 'text', type: 'string', description: 'Text containing hashtags' }
],
code: `local text = context.data.text or ""
local hashtags = {}
for tag in string.gmatch(text, "#(%w+)") do
table.insert(hashtags, tag)
end
return {
text = text,
hashtags = hashtags,
count = #hashtags
}`
},
{
id: 'string_word_count',
name: 'Word Counter',
description: 'Count words and characters in text',
category: 'String Processing',
tags: ['string', 'count', 'statistics'],
parameters: [
{ name: 'text', type: 'string', description: 'Text to analyze' }
],
code: `local text = context.data.text or ""
local charCount = string.len(text)
local words = {}
for word in string.gmatch(text, "%S+") do
table.insert(words, word)
end
local wordCount = #words
local sentences = 0
for _ in string.gmatch(text, "[.!?]+") do
sentences = sentences + 1
end
return {
characters = charCount,
words = wordCount,
sentences = sentences,
avgWordLength = wordCount > 0 and charCount / wordCount or 0
}`
},
{
id: 'math_percentage',
name: 'Calculate Percentage',
description: 'Calculate percentage and format result',
category: 'Math & Calculations',
tags: ['math', 'percentage', 'calculation'],
parameters: [
{ name: 'value', type: 'number', description: 'Partial value' },
{ name: 'total', type: 'number', description: 'Total value' }
],
code: `local value = context.data.value or 0
local total = context.data.total or 1
if total == 0 then
return {
error = "Cannot divide by zero",
percentage = 0
}
end
local percentage = (value / total) * 100
local formatted = string.format("%.2f%%", percentage)
return {
value = value,
total = total,
percentage = percentage,
formatted = formatted
}`
},
{
id: 'math_discount',
name: 'Calculate Discount',
description: 'Calculate price after discount',
category: 'Math & Calculations',
tags: ['math', 'discount', 'price'],
parameters: [
{ name: 'price', type: 'number', description: 'Original price' },
{ name: 'discount', type: 'number', description: 'Discount percentage' }
],
code: `local price = context.data.price or 0
local discount = context.data.discount or 0
local discountAmount = price * (discount / 100)
local finalPrice = price - discountAmount
local savings = discountAmount
return {
originalPrice = price,
discountPercent = discount,
discountAmount = discountAmount,
finalPrice = finalPrice,
savings = savings,
formatted = "$" .. string.format("%.2f", finalPrice)
}`
},
{
id: 'math_compound_interest',
name: 'Compound Interest Calculator',
description: 'Calculate compound interest over time',
category: 'Math & Calculations',
tags: ['math', 'interest', 'finance'],
parameters: [
{ name: 'principal', type: 'number', description: 'Initial amount' },
{ name: 'rate', type: 'number', description: 'Interest rate (%)' },
{ name: 'years', type: 'number', description: 'Number of years' }
],
code: `local principal = context.data.principal or 1000
local rate = (context.data.rate or 5) / 100
local years = context.data.years or 1
local compounds = 12
local amount = principal * math.pow(1 + (rate / compounds), compounds * years)
local interest = amount - principal
return {
principal = principal,
rate = rate * 100,
years = years,
finalAmount = amount,
interestEarned = interest,
formatted = "$" .. string.format("%.2f", amount)
}`
},
{
id: 'math_statistics',
name: 'Statistical Analysis',
description: 'Calculate mean, median, mode, std dev',
category: 'Math & Calculations',
tags: ['math', 'statistics', 'analysis'],
parameters: [
{ name: 'numbers', type: 'array', description: 'Array of numbers' }
],
code: `local numbers = context.data.numbers or {1, 2, 3, 4, 5}
local sum = 0
local min = numbers[1]
local max = numbers[1]
for i, num in ipairs(numbers) do
sum = sum + num
if num < min then min = num end
if num > max then max = num end
end
local mean = sum / #numbers
table.sort(numbers)
local median
if #numbers % 2 == 0 then
median = (numbers[#numbers/2] + numbers[#numbers/2 + 1]) / 2
else
median = numbers[math.ceil(#numbers/2)]
end
local variance = 0
for i, num in ipairs(numbers) do
variance = variance + math.pow(num - mean, 2)
end
variance = variance / #numbers
local stdDev = math.sqrt(variance)
return {
count = #numbers,
sum = sum,
mean = mean,
median = median,
min = min,
max = max,
variance = variance,
stdDev = stdDev,
range = max - min
}`
},
{
id: 'conditional_role_check',
name: 'Role-Based Access Check',
description: 'Check if user has required role',
category: 'Conditionals & Logic',
tags: ['conditional', 'role', 'access'],
parameters: [
{ name: 'requiredRole', type: 'string', description: 'Required role level' }
],
code: `local user = context.user or {}
local requiredRole = context.data.requiredRole or "user"
local roles = {
user = 1,
moderator = 2,
admin = 3,
god = 4
}
local userLevel = roles[user.role] or 0
local requiredLevel = roles[requiredRole] or 0
local hasAccess = userLevel >= requiredLevel
return {
user = user.username,
userRole = user.role,
requiredRole = requiredRole,
hasAccess = hasAccess,
message = hasAccess and "Access granted" or "Access denied"
}`
},
{
id: 'conditional_time_based',
name: 'Time-Based Logic',
description: 'Execute logic based on time of day',
category: 'Conditionals & Logic',
tags: ['conditional', 'time', 'schedule'],
code: `local hour = tonumber(os.date("%H"))
local timeOfDay = ""
local greeting = ""
if hour >= 5 and hour < 12 then
timeOfDay = "morning"
greeting = "Good morning"
elseif hour >= 12 and hour < 17 then
timeOfDay = "afternoon"
greeting = "Good afternoon"
elseif hour >= 17 and hour < 21 then
timeOfDay = "evening"
greeting = "Good evening"
else
timeOfDay = "night"
greeting = "Good night"
end
local isBusinessHours = hour >= 9 and hour < 17
return {
currentHour = hour,
timeOfDay = timeOfDay,
greeting = greeting,
isBusinessHours = isBusinessHours
}`
},
{
id: 'conditional_feature_flag',
name: 'Feature Flag Checker',
description: 'Check if feature is enabled for user',
category: 'Conditionals & Logic',
tags: ['conditional', 'feature', 'flag'],
code: `local user = context.user or {}
local feature = context.data.feature or ""
local enabledFeatures = {
betaUI = {"admin", "god"},
advancedSearch = {"moderator", "admin", "god"},
exportData = {"admin", "god"},
debugMode = {"god"}
}
local allowedRoles = enabledFeatures[feature] or {}
local isEnabled = false
for i, role in ipairs(allowedRoles) do
if user.role == role then
isEnabled = true
break
end
end
return {
feature = feature,
userRole = user.role,
enabled = isEnabled,
reason = isEnabled and "Feature available" or "Feature not available for your role"
}`
},
{
id: 'error_try_catch',
name: 'Try-Catch Pattern',
description: 'Safe execution with error handling',
category: 'Error Handling',
tags: ['error', 'exception', 'safety'],
code: `local function riskyOperation()
local data = context.data or {}
if not data.value then
error("Value is required")
end
if data.value < 0 then
error("Value must be positive")
end
return data.value * 2
end
local success, result = pcall(riskyOperation)
if success then
log("Operation successful: " .. tostring(result))
return {
success = true,
result = result
}
else
log("Operation failed: " .. tostring(result))
return {
success = false,
error = tostring(result)
}
end`
},
{
id: 'error_validation_accumulator',
name: 'Validation Error Accumulator',
description: 'Collect all validation errors at once',
category: 'Error Handling',
tags: ['error', 'validation', 'accumulator'],
code: `local data = context.data or {}
local errors = {}
if not data.name or data.name == "" then
table.insert(errors, "Name is required")
end
if not data.email or data.email == "" then
table.insert(errors, "Email is required")
elseif not string.match(data.email, "@") then
table.insert(errors, "Email format is invalid")
end
if not data.age then
table.insert(errors, "Age is required")
elseif data.age < 18 then
table.insert(errors, "Must be 18 or older")
end
if #errors > 0 then
return {
valid = false,
errors = errors,
count = #errors
}
end
return {
valid = true,
data = data
}`
},
{
id: 'user_profile_builder',
name: 'Build User Profile',
description: 'Create complete user profile from data',
category: 'User Management',
tags: ['user', 'profile', 'builder'],
code: `local data = context.data or {}
local profile = {
id = "user_" .. os.time(),
username = data.username or "",
email = string.lower(data.email or ""),
displayName = data.displayName or data.username or "",
role = "user",
status = "active",
createdAt = os.time(),
metadata = {
source = "registration",
version = "1.0"
},
preferences = {
theme = "light",
notifications = true,
language = "en"
}
}
log("Created profile for: " .. profile.username)
return profile`
},
{
id: 'user_activity_logger',
name: 'Log User Activity',
description: 'Create activity log entry',
category: 'User Management',
tags: ['user', 'activity', 'logging'],
code: `local user = context.user or {}
local action = context.data.action or "unknown"
local details = context.data.details or {}
local activity = {
id = "activity_" .. os.time(),
userId = user.id or "unknown",
username = user.username or "anonymous",
action = action,
details = details,
timestamp = os.time(),
date = os.date("%Y-%m-%d %H:%M:%S"),
ipAddress = "0.0.0.0",
userAgent = "MetaBuilder/1.0"
}
log("Activity logged: " .. user.username .. " - " .. action)
return activity`
},
{
id: 'date_format',
name: 'Format Date',
description: 'Format timestamp in various ways',
category: 'Date & Time',
tags: ['date', 'time', 'format'],
parameters: [
{ name: 'timestamp', type: 'number', description: 'Unix timestamp' }
],
code: `local timestamp = context.data.timestamp or os.time()
local formatted = {
full = os.date("%Y-%m-%d %H:%M:%S", timestamp),
date = os.date("%Y-%m-%d", timestamp),
time = os.date("%H:%M:%S", timestamp),
readable = os.date("%B %d, %Y at %I:%M %p", timestamp),
iso = os.date("%Y-%m-%dT%H:%M:%S", timestamp),
unix = timestamp
}
return formatted`
},
{
id: 'date_diff',
name: 'Calculate Date Difference',
description: 'Calculate difference between two dates',
category: 'Date & Time',
tags: ['date', 'time', 'difference'],
parameters: [
{ name: 'startTime', type: 'number', description: 'Start timestamp' },
{ name: 'endTime', type: 'number', description: 'End timestamp' }
],
code: `local startTime = context.data.startTime or os.time()
local endTime = context.data.endTime or os.time()
local diffSeconds = math.abs(endTime - startTime)
local diffMinutes = math.floor(diffSeconds / 60)
local diffHours = math.floor(diffMinutes / 60)
local diffDays = math.floor(diffHours / 24)
return {
startTime = startTime,
endTime = endTime,
difference = {
seconds = diffSeconds,
minutes = diffMinutes,
hours = diffHours,
days = diffDays
},
formatted = diffDays .. " days, " .. (diffHours % 24) .. " hours"
}`
},
{
id: 'json_parse',
name: 'Safe JSON Parse',
description: 'Parse JSON string with error handling',
category: 'Utilities',
tags: ['json', 'parse', 'utility'],
parameters: [
{ name: 'jsonString', type: 'string', description: 'JSON string to parse' }
],
code: `local jsonString = context.data.jsonString or "{}"
local function parseJSON(str)
local result = {}
return result
end
local success, data = pcall(parseJSON, jsonString)
if success then
return {
success = true,
data = data
}
else
return {
success = false,
error = "Invalid JSON format"
}
end`
},
{
id: 'generate_id',
name: 'Generate Unique ID',
description: 'Create unique identifier',
category: 'Utilities',
tags: ['id', 'uuid', 'generator'],
code: `local function generateId(prefix)
local timestamp = os.time()
local random = math.random(1000, 9999)
return (prefix or "id") .. "_" .. timestamp .. "_" .. random
end
local id = generateId(context.data.prefix)
return {
id = id,
timestamp = os.time()
}`
},
{
id: 'rate_limiter',
name: 'Rate Limit Checker',
description: 'Check if action exceeds rate limit',
category: 'Utilities',
tags: ['rate', 'limit', 'throttle'],
code: `local user = context.user or {}
local action = context.data.action or "default"
local maxAttempts = context.data.maxAttempts or 5
local windowSeconds = context.data.windowSeconds or 60
local currentTime = os.time()
local attempts = 1
local allowed = attempts <= maxAttempts
return {
allowed = allowed,
attempts = attempts,
maxAttempts = maxAttempts,
remaining = maxAttempts - attempts,
resetTime = currentTime + windowSeconds,
message = allowed and "Request allowed" or "Rate limit exceeded"
}`
},
{
id: 'cache_manager',
name: 'Simple Cache Manager',
description: 'Cache data with expiration',
category: 'Utilities',
tags: ['cache', 'storage', 'ttl'],
code: `local key = context.data.key or "cache_key"
local value = context.data.value
local ttl = context.data.ttl or 300
local cached = {
key = key,
value = value,
cachedAt = os.time(),
expiresAt = os.time() + ttl,
ttl = ttl
}
log("Cached " .. key .. " for " .. ttl .. " seconds")
return cached`
}
]
// Functions moved to ../functions/ directory
// Use LuaSnippetUtils class or import individual functions

View File

@@ -0,0 +1,24 @@
/**
* Get Severity Color
* Returns appropriate color classes for a severity level
*/
/**
* Get color classes for displaying severity
* @param severity - The severity level
* @returns MUI-compatible color string
*/
export const getSeverityColor = (severity: string): string => {
switch (severity) {
case 'critical':
return 'error'
case 'high':
return 'warning'
case 'medium':
return 'info'
case 'low':
return 'secondary'
default:
return 'success'
}
}

View File

@@ -0,0 +1,24 @@
/**
* Get Severity Icon
* Returns appropriate emoji icon for a severity level
*/
/**
* Get emoji icon for displaying severity
* @param severity - The severity level
* @returns Emoji representing the severity
*/
export const getSeverityIcon = (severity: string): string => {
switch (severity) {
case 'critical':
return '🚨'
case 'high':
return '⚠️'
case 'medium':
return '⚡'
case 'low':
return ''
default:
return '✓'
}
}

View File

@@ -0,0 +1,7 @@
/**
* Helpers Index
* Exports all helper functions
*/
export { getSeverityColor } from './get-severity-color'
export { getSeverityIcon } from './get-severity-icon'

View File

@@ -0,0 +1,27 @@
/**
* Security Functions Index
* Exports all security-related types and functions
*/
// Types
export type { SecurityScanResult, SecurityIssue, SecurityPattern } from './types'
// Patterns
export { JAVASCRIPT_PATTERNS } from './patterns/javascript-patterns'
export { LUA_PATTERNS } from './patterns/lua-patterns'
export { SQL_INJECTION_PATTERNS } from './patterns/sql-patterns'
// Scanners
export { scanJavaScript } from './scanners/scan-javascript'
export { scanLua } from './scanners/scan-lua'
export { scanJSON } from './scanners/scan-json'
export { scanHTML } from './scanners/scan-html'
export { sanitizeInput } from './scanners/sanitize-input'
// Utils
export { getLineNumber } from './utils/get-line-number'
export { calculateOverallSeverity } from './utils/calculate-severity'
// Helpers
export { getSeverityColor } from './helpers/get-severity-color'
export { getSeverityIcon } from './helpers/get-severity-icon'

View File

@@ -0,0 +1,8 @@
/**
* Security Patterns Index
* Exports all security pattern collections
*/
export { JAVASCRIPT_PATTERNS } from './javascript-patterns'
export { LUA_PATTERNS } from './lua-patterns'
export { SQL_INJECTION_PATTERNS } from './sql-patterns'

View File

@@ -0,0 +1,184 @@
/**
* JavaScript Security Patterns
* Patterns for detecting malicious JavaScript code
*/
import type { SecurityPattern } from '../types'
export const JAVASCRIPT_PATTERNS: SecurityPattern[] = [
{
pattern: /eval\s*\(/gi,
type: 'dangerous',
severity: 'critical',
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',
severity: 'high',
message: 'Dynamic Function constructor detected',
recommendation: 'Avoid dynamic code generation or use with extreme caution'
},
{
pattern: /innerHTML\s*=/gi,
type: 'dangerous',
severity: 'high',
message: 'innerHTML assignment detected - XSS vulnerability risk',
recommendation: 'Use textContent, createElement, or React JSX instead'
},
{
pattern: /dangerouslySetInnerHTML/gi,
type: 'dangerous',
severity: 'high',
message: 'dangerouslySetInnerHTML detected - XSS vulnerability risk',
recommendation: 'Sanitize HTML content or use safe alternatives'
},
{
pattern: /document\.write\s*\(/gi,
type: 'dangerous',
severity: 'medium',
message: 'document.write() detected - can cause security issues',
recommendation: 'Use DOM manipulation methods instead'
},
{
pattern: /\.call\s*\(\s*window/gi,
type: 'suspicious',
severity: 'medium',
message: 'Calling functions with window context',
recommendation: 'Be careful with context manipulation'
},
{
pattern: /\.apply\s*\(\s*window/gi,
type: 'suspicious',
severity: 'medium',
message: 'Applying functions with window context',
recommendation: 'Be careful with context manipulation'
},
{
pattern: /__proto__/gi,
type: 'dangerous',
severity: 'critical',
message: 'Prototype pollution attempt detected',
recommendation: 'Never manipulate __proto__ directly'
},
{
pattern: /constructor\s*\[\s*['"]prototype['"]\s*\]/gi,
type: 'dangerous',
severity: 'critical',
message: 'Prototype manipulation detected',
recommendation: 'Use Object.create() or proper class syntax'
},
{
pattern: /import\s+.*\s+from\s+['"]https?:/gi,
type: 'dangerous',
severity: 'critical',
message: 'Remote code import detected',
recommendation: 'Only import from trusted, local sources'
},
{
pattern: /<script[^>]*>/gi,
type: 'dangerous',
severity: 'critical',
message: 'Script tag injection detected',
recommendation: 'Never inject script tags dynamically'
},
{
pattern: /on(click|load|error|mouseover|mouseout|focus|blur)\s*=/gi,
type: 'suspicious',
severity: 'medium',
message: 'Inline event handler detected',
recommendation: 'Use addEventListener or React event handlers'
},
{
pattern: /javascript:\s*/gi,
type: 'dangerous',
severity: 'high',
message: 'javascript: protocol detected',
recommendation: 'Never use javascript: protocol in URLs'
},
{
pattern: /data:\s*text\/html/gi,
type: 'dangerous',
severity: 'high',
message: 'Data URI with HTML detected',
recommendation: 'Avoid data URIs with executable content'
},
{
pattern: /setTimeout\s*\(\s*['"`]/gi,
type: 'dangerous',
severity: 'high',
message: 'setTimeout with string argument detected',
recommendation: 'Use setTimeout with function reference instead'
},
{
pattern: /setInterval\s*\(\s*['"`]/gi,
type: 'dangerous',
severity: 'high',
message: 'setInterval with string argument detected',
recommendation: 'Use setInterval with function reference instead'
},
{
pattern: /localStorage|sessionStorage/gi,
type: 'warning',
severity: 'low',
message: 'Local/session storage usage detected',
recommendation: 'Use useKV hook for persistent data instead'
},
{
pattern: /crypto\.subtle|atob|btoa/gi,
type: 'warning',
severity: 'low',
message: 'Cryptographic operation detected',
recommendation: 'Ensure proper key management and secure practices'
},
{
pattern: /XMLHttpRequest|fetch\s*\(\s*['"`]http/gi,
type: 'warning',
severity: 'medium',
message: 'External HTTP request detected',
recommendation: 'Ensure CORS and security headers are properly configured'
},
{
pattern: /window\.open/gi,
type: 'suspicious',
severity: 'medium',
message: 'window.open detected',
recommendation: 'Be cautious with popup windows'
},
{
pattern: /location\.href\s*=/gi,
type: 'suspicious',
severity: 'medium',
message: 'Direct location manipulation detected',
recommendation: 'Use React Router or validate URLs carefully'
},
{
pattern: /require\s*\(\s*[^'"`]/gi,
type: 'dangerous',
severity: 'high',
message: 'Dynamic require() detected',
recommendation: 'Use static imports only'
},
{
pattern: /\.exec\s*\(|child_process|spawn|fork|execFile/gi,
type: 'malicious',
severity: 'critical',
message: 'System command execution attempt detected',
recommendation: 'This is not allowed in browser environment'
},
{
pattern: /fs\.|path\.|os\./gi,
type: 'malicious',
severity: 'critical',
message: 'Node.js system module usage detected',
recommendation: 'File system access not allowed in browser'
},
{
pattern: /process\.env|process\.exit/gi,
type: 'suspicious',
severity: 'medium',
message: 'Process manipulation detected',
recommendation: 'Not applicable in browser environment'
}
]

View File

@@ -0,0 +1,86 @@
/**
* Lua Security Patterns
* Patterns for detecting malicious Lua code
*/
import type { SecurityPattern } from '../types'
export const LUA_PATTERNS: SecurityPattern[] = [
{
pattern: /os\.(execute|exit|remove|rename|tmpname)/gi,
type: 'malicious',
severity: 'critical',
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',
severity: 'critical',
message: 'Lua file I/O operation detected',
recommendation: 'File system access is disabled for security'
},
{
pattern: /loadfile|dofile/gi,
type: 'dangerous',
severity: 'critical',
message: 'Lua file loading function detected',
recommendation: 'File loading is disabled for security'
},
{
pattern: /package\.(loadlib|searchpath|cpath)/gi,
type: 'dangerous',
severity: 'critical',
message: 'Lua dynamic library loading detected',
recommendation: 'Dynamic library loading is disabled'
},
{
pattern: /debug\.(getinfo|setmetatable|getfenv|setfenv)/gi,
type: 'dangerous',
severity: 'high',
message: 'Lua debug module advanced features detected',
recommendation: 'Limited debug functionality available'
},
{
pattern: /loadstring\s*\(/gi,
type: 'dangerous',
severity: 'high',
message: 'Lua dynamic code execution detected',
recommendation: 'Use with extreme caution'
},
{
pattern: /\.\.\s*[[\]]/gi,
type: 'suspicious',
severity: 'medium',
message: 'Potential Lua table manipulation',
recommendation: 'Ensure proper validation'
},
{
pattern: /_G\s*\[/gi,
type: 'suspicious',
severity: 'high',
message: 'Global environment manipulation detected',
recommendation: 'Avoid modifying global environment'
},
{
pattern: /getmetatable|setmetatable/gi,
type: 'suspicious',
severity: 'medium',
message: 'Metatable manipulation detected',
recommendation: 'Use carefully to avoid security issues'
},
{
pattern: /while\s+true\s+do/gi,
type: 'warning',
severity: 'medium',
message: 'Infinite loop detected',
recommendation: 'Ensure proper break conditions exist'
},
{
pattern: /function\s+(\w+)\s*\([^)]*\)\s*\{[^}]*\1\s*\(/gi,
type: 'warning',
severity: 'low',
message: 'Potential recursive function',
recommendation: 'Ensure recursion has proper termination'
}
]

View File

@@ -0,0 +1,37 @@
/**
* SQL Injection Patterns
* Patterns for detecting SQL injection attempts
*/
import type { SecurityPattern } from '../types'
export const SQL_INJECTION_PATTERNS: SecurityPattern[] = [
{
pattern: /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+/gi,
type: 'malicious',
severity: 'critical',
message: 'SQL injection attempt detected',
recommendation: 'Use parameterized queries'
},
{
pattern: /UNION\s+SELECT/gi,
type: 'malicious',
severity: 'critical',
message: 'SQL UNION injection attempt',
recommendation: 'Use parameterized queries'
},
{
pattern: /'[\s]*OR[\s]*'1'[\s]*=[\s]*'1/gi,
type: 'malicious',
severity: 'critical',
message: 'SQL authentication bypass attempt',
recommendation: 'Never concatenate user input into SQL'
},
{
pattern: /--[\s]*$/gm,
type: 'suspicious',
severity: 'high',
message: 'SQL comment pattern detected',
recommendation: 'May indicate SQL injection attempt'
}
]

View File

@@ -0,0 +1,10 @@
/**
* Scanners Index
* Exports all scanner functions
*/
export { scanJavaScript } from './scan-javascript'
export { scanLua } from './scan-lua'
export { scanJSON } from './scan-json'
export { scanHTML } from './scan-html'
export { sanitizeInput } from './sanitize-input'

View File

@@ -0,0 +1,37 @@
/**
* Sanitize Input
* Removes dangerous patterns from input based on type
*/
/**
* Sanitize input by removing dangerous patterns
* @param input - The input string to sanitize
* @param type - The type of content being sanitized
* @returns Sanitized string
*/
export const sanitizeInput = (
input: string,
type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text'
): string => {
let sanitized = input
if (type === 'text') {
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gis, '')
sanitized = sanitized.replace(/on\w+\s*=/gi, '')
sanitized = sanitized.replace(/javascript:/gi, '')
}
if (type === 'html') {
sanitized = sanitized.replace(/<script[^>]*>.*?<\/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
}

View File

@@ -0,0 +1,78 @@
/**
* Scan HTML
* Scans HTML for security vulnerabilities like XSS
*/
import type { SecurityScanResult, SecurityIssue } from '../types'
import { calculateOverallSeverity } from '../utils/calculate-severity'
/**
* Scan HTML string for security vulnerabilities
* @param html - The HTML string to scan
* @returns Security scan result with issues found
*/
export const scanHTML = (html: string): SecurityScanResult => {
const issues: SecurityIssue[] = []
// Check for script tags
const scriptTagPattern = /<script[^>]*>.*?<\/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'
})
}
// Check for inline event handlers
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'
})
}
// Check for javascript: protocol
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'
})
}
// Check for unsandboxed iframes
const iframePattern = /<iframe[^>]*>/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 = calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues
}
}

View File

@@ -0,0 +1,61 @@
/**
* Scan JavaScript
* Scans JavaScript code for security vulnerabilities
*/
import type { SecurityScanResult, SecurityIssue } from '../types'
import { JAVASCRIPT_PATTERNS } from '../patterns/javascript-patterns'
import { SQL_INJECTION_PATTERNS } from '../patterns/sql-patterns'
import { getLineNumber } from '../utils/get-line-number'
import { calculateOverallSeverity } from '../utils/calculate-severity'
/**
* Scan JavaScript code for security vulnerabilities
* @param code - The JavaScript code to scan
* @returns Security scan result with issues found
*/
export const scanJavaScript = (code: string): SecurityScanResult => {
const issues: SecurityIssue[] = []
// Check JavaScript patterns
for (const pattern of JAVASCRIPT_PATTERNS) {
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
for (const match of matches) {
const lineNumber = getLineNumber(code, match.index || 0)
issues.push({
type: pattern.type,
severity: pattern.severity,
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
})
}
}
// Check SQL injection patterns
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 = 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 = calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues,
sanitizedCode: safe ? code : undefined
}
}

View File

@@ -0,0 +1,62 @@
/**
* Scan JSON
* Scans JSON for security vulnerabilities like prototype pollution
*/
import type { SecurityScanResult, SecurityIssue } from '../types'
import { calculateOverallSeverity } from '../utils/calculate-severity'
/**
* Scan JSON string for security vulnerabilities
* @param jsonString - The JSON string to scan
* @returns Security scan result with issues found
*/
export const scanJSON = (jsonString: string): SecurityScanResult => {
const issues: SecurityIssue[] = []
// Validate JSON format
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'
})
}
// Check for prototype pollution
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'
})
}
// Check for script tags
if (jsonString.includes('<script')) {
issues.push({
type: 'malicious',
severity: 'critical',
message: 'Script tag in JSON data',
pattern: '<script>',
recommendation: 'Remove all HTML/script content from JSON'
})
}
const severity = calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues,
sanitizedCode: safe ? jsonString : undefined
}
}

View File

@@ -0,0 +1,43 @@
/**
* Scan Lua
* Scans Lua code for security vulnerabilities
*/
import type { SecurityScanResult, SecurityIssue } from '../types'
import { LUA_PATTERNS } from '../patterns/lua-patterns'
import { getLineNumber } from '../utils/get-line-number'
import { calculateOverallSeverity } from '../utils/calculate-severity'
/**
* Scan Lua code for security vulnerabilities
* @param code - The Lua code to scan
* @returns Security scan result with issues found
*/
export const scanLua = (code: string): SecurityScanResult => {
const issues: SecurityIssue[] = []
for (const pattern of LUA_PATTERNS) {
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
for (const match of matches) {
const lineNumber = 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 = calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues,
sanitizedCode: safe ? code : undefined
}
}

View File

@@ -0,0 +1,28 @@
/**
* Security Scanner Types
* Shared type definitions for all security scanning functions
*/
export interface SecurityScanResult {
safe: boolean
severity: 'safe' | 'low' | 'medium' | 'high' | 'critical'
issues: SecurityIssue[]
sanitizedCode?: string
}
export interface SecurityIssue {
type: 'malicious' | 'suspicious' | 'dangerous' | 'warning'
severity: 'low' | 'medium' | 'high' | 'critical'
message: string
pattern: string
line?: number
recommendation?: string
}
export interface SecurityPattern {
pattern: RegExp
type: 'malicious' | 'suspicious' | 'dangerous' | 'warning'
severity: 'low' | 'medium' | 'high' | 'critical'
message: string
recommendation: string
}

View File

@@ -0,0 +1,29 @@
/**
* Calculate Overall Severity
* Determines the highest severity level from a list of issues
*/
import type { SecurityIssue } from '../types'
/**
* Calculate the overall severity from a list of security issues
* @param issues - Array of security issues found
* @returns Overall severity level
*/
export const 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'
}

View File

@@ -0,0 +1,14 @@
/**
* Get Line Number
* Utility to find line number from character index in code
*/
/**
* Calculate line number from character index
* @param code - The source code string
* @param index - Character index in the code
* @returns Line number (1-based)
*/
export const getLineNumber = (code: string, index: number): number => {
return code.substring(0, index).split('\n').length
}

View File

@@ -0,0 +1,7 @@
/**
* Utils Index
* Exports all utility functions for security scanning
*/
export { getLineNumber } from './get-line-number'
export { calculateOverallSeverity } from './calculate-severity'

View File

@@ -0,0 +1,23 @@
export interface SecurityScanResult {
safe: boolean
severity: 'safe' | 'low' | 'medium' | 'high' | 'critical'
issues: SecurityIssue[]
sanitizedCode?: string
}
export interface SecurityIssue {
type: 'malicious' | 'suspicious' | 'dangerous' | 'warning'
severity: 'low' | 'medium' | 'high' | 'critical'
message: string
pattern: string
line?: number
recommendation?: string
}
export interface SecurityPattern {
pattern: RegExp
type: 'malicious' | 'suspicious' | 'dangerous' | 'warning'
severity: 'low' | 'medium' | 'high' | 'critical'
message: string
recommendation: string
}