This commit is contained in:
2025-12-25 17:16:29 +00:00
parent 07d803505a
commit f1ca57d9a0
107 changed files with 1896 additions and 533 deletions

View File

@@ -1,109 +1,13 @@
/**
* Authentication and Authorization Module
*
* Handles user authentication, permission checking, and role-based access control.
* Implements a 5-level hierarchical permission system where each level inherits
* permissions from lower levels.
* @deprecated Import from '@/lib/auth' instead
*/
import type { User, UserRole } from './level-types'
export {
canAccessLevel,
getRoleDisplayName,
getScrambledPassword,
DEFAULT_USERS,
DEFAULT_CREDENTIALS,
} from './auth/index'
const SCRAMBLE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
function generateDeterministicScrambledPassword(seed: string, length: number = 16): string {
// Deterministic output avoids server/client mismatches during hydration.
let hash = 2166136261
for (let i = 0; i < seed.length; i++) {
hash ^= seed.charCodeAt(i)
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
}
let password = ''
for (let i = 0; i < length; i++) {
hash = (hash * 1664525 + 1013904223) >>> 0
password += SCRAMBLE_CHARSET[hash % SCRAMBLE_CHARSET.length]
}
return password
}
// Pre-generated scrambled passwords for default user accounts
// Each role has a unique scrambled password for authentication
const SCRAMBLED_PASSWORDS = {
supergod: generateDeterministicScrambledPassword('supergod', 16),
god: generateDeterministicScrambledPassword('god', 16),
admin: generateDeterministicScrambledPassword('admin', 16),
demo: generateDeterministicScrambledPassword('demo', 16),
}
// Default users created during application initialization
// These provide initial access points for different permission levels
export const DEFAULT_USERS: 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(),
},
]
// Maps usernames to their scrambled passwords for authentication
export const DEFAULT_CREDENTIALS: Record<string, string> = SCRAMBLED_PASSWORDS
/**
* Gets the scrambled password for a given username
* @param username - The username to look up
* @returns The scrambled password, or empty string if not found
*/
export function getScrambledPassword(username: string): string {
return SCRAMBLED_PASSWORDS[username as keyof typeof SCRAMBLED_PASSWORDS] || ''
}
export function canAccessLevel(userRole: UserRole, level: number): boolean {
const roleHierarchy: Record<UserRole, number> = {
public: 1,
user: 2,
admin: 3,
god: 4,
supergod: 5,
}
return roleHierarchy[userRole] >= level
}
export function getRoleDisplayName(role: UserRole): string {
const names: Record<UserRole, string> = {
public: 'Public',
user: 'User',
admin: 'Administrator',
god: 'System Architect',
supergod: 'Supreme Administrator',
}
return names[role]
}

View File

@@ -0,0 +1,19 @@
import type { UserRole } from '../level-types'
/**
* Role hierarchy mapping roles to their numeric permission levels
*/
const roleHierarchy: Record<UserRole, number> = {
public: 1,
user: 2,
admin: 3,
god: 4,
supergod: 5,
}
/**
* Check if a user role has access to a given permission level
*/
export function canAccessLevel(userRole: UserRole, level: number): boolean {
return roleHierarchy[userRole] >= level
}

View File

@@ -0,0 +1,6 @@
import { SCRAMBLED_PASSWORDS } from './scrambled-passwords'
/**
* Maps usernames to their scrambled passwords for authentication
*/
export const DEFAULT_CREDENTIALS: Record<string, string> = SCRAMBLED_PASSWORDS

View File

@@ -0,0 +1,40 @@
import type { User } from '../level-types'
/**
* Default users created during application initialization
*/
export const DEFAULT_USERS: 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(),
},
]

View File

@@ -0,0 +1,19 @@
import type { UserRole } from '../level-types'
/**
* Human-readable display names for user roles
*/
const roleDisplayNames: Record<UserRole, string> = {
public: 'Public',
user: 'User',
admin: 'Administrator',
god: 'System Architect',
supergod: 'Supreme Administrator',
}
/**
* Get the display name for a user role
*/
export function getRoleDisplayName(role: UserRole): string {
return roleDisplayNames[role]
}

View File

@@ -0,0 +1,8 @@
import { SCRAMBLED_PASSWORDS } from './scrambled-passwords'
/**
* Gets the scrambled password for a given username
*/
export function getScrambledPassword(username: string): string {
return SCRAMBLED_PASSWORDS[username as keyof typeof SCRAMBLED_PASSWORDS] || ''
}

View File

@@ -0,0 +1,6 @@
export { canAccessLevel } from './can-access-level'
export { getRoleDisplayName } from './get-role-display-name'
export { getScrambledPassword } from './get-scrambled-password'
export { DEFAULT_USERS } from './default-users'
export { DEFAULT_CREDENTIALS } from './default-credentials'
export { SCRAMBLED_PASSWORDS } from './scrambled-passwords'

View File

@@ -0,0 +1,30 @@
const SCRAMBLE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
/**
* Generate a deterministic scrambled password from a seed
* Uses FNV-1a inspired hash for consistent output across server/client
*/
function generateDeterministicScrambledPassword(seed: string, length: number = 16): string {
let hash = 2166136261
for (let i = 0; i < seed.length; i++) {
hash ^= seed.charCodeAt(i)
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
}
let password = ''
for (let i = 0; i < length; i++) {
hash = (hash * 1664525 + 1013904223) >>> 0
password += SCRAMBLE_CHARSET[hash % SCRAMBLE_CHARSET.length]
}
return password
}
/**
* Pre-generated scrambled passwords for default user accounts
*/
export const SCRAMBLED_PASSWORDS = {
supergod: generateDeterministicScrambledPassword('supergod', 16),
god: generateDeterministicScrambledPassword('god', 16),
admin: generateDeterministicScrambledPassword('admin', 16),
demo: generateDeterministicScrambledPassword('demo', 16),
}

View File

@@ -0,0 +1,16 @@
import { prisma } from '../prisma'
import type { AppConfiguration } from '../../level-types'
export async function 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),
}
}

View File

@@ -0,0 +1,2 @@
export { getAppConfig } from './get-app-config'
export { setAppConfig } from './set-app-config'

View File

@@ -0,0 +1,17 @@
import { prisma } from '../prisma'
import type { AppConfiguration } from '../../level-types'
export async function 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),
},
})
}

View File

@@ -0,0 +1,15 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
export async function 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,
},
})
}

View File

@@ -0,0 +1,5 @@
import { prisma } from '../prisma'
export async function deleteComment(commentId: string): Promise<void> {
await prisma.comment.delete({ where: { id: commentId } })
}

View File

@@ -0,0 +1,14 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
export async function 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,
}))
}

View File

@@ -0,0 +1,5 @@
export { getComments } from './get-comments'
export { setComments } from './set-comments'
export { addComment } from './add-comment'
export { updateComment } from './update-comment'
export { deleteComment } from './delete-comment'

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
export async function 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,
},
})
}
}

View File

@@ -0,0 +1,9 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
export async function 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 })
}

View File

@@ -0,0 +1,15 @@
import { prisma } from '../prisma'
import type { ComponentConfig } from '../types'
export async function 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,
},
})
}

View File

@@ -0,0 +1,15 @@
import { prisma } from '../prisma'
import type { ComponentNode } from '../types'
export async function 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,
},
})
}

View File

@@ -0,0 +1,5 @@
import { prisma } from '../prisma'
export async function deleteComponentConfig(configId: string): Promise<void> {
await prisma.componentConfig.delete({ where: { id: configId } })
}

View File

@@ -0,0 +1,5 @@
import { prisma } from '../prisma'
export async function deleteComponentNode(nodeId: string): Promise<void> {
await prisma.componentNode.delete({ where: { id: nodeId } })
}

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { ComponentConfig } from '../types'
export async function 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
}

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { ComponentNode } from '../types'
export async function 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
}

View File

@@ -0,0 +1,10 @@
export { getComponentHierarchy } from './get-component-hierarchy'
export { setComponentHierarchy } from './set-component-hierarchy'
export { addComponentNode } from './add-component-node'
export { updateComponentNode } from './update-component-node'
export { deleteComponentNode } from './delete-component-node'
export { getComponentConfigs } from './get-component-configs'
export { setComponentConfigs } from './set-component-configs'
export { addComponentConfig } from './add-component-config'
export { updateComponentConfig } from './update-component-config'
export { deleteComponentConfig } from './delete-component-config'

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { ComponentConfig } from '../types'
export async function 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,
},
})
}
}

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { ComponentNode } from '../types'
export async function 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,
},
})
}
}

View File

@@ -0,0 +1,14 @@
import { prisma } from '../prisma'
import type { ComponentConfig } from '../types'
export async function 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 = updates.conditionalRendering ? JSON.stringify(updates.conditionalRendering) : null
}
await prisma.componentConfig.update({ where: { id: configId }, data })
}

View File

@@ -0,0 +1,12 @@
import { prisma } from '../prisma'
import type { ComponentNode } from '../types'
export async function 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 })
}

View File

@@ -0,0 +1,15 @@
import { prisma } from '../prisma'
import { getPasswordResetTokens } from './get-password-reset-tokens'
/**
* Delete a password reset token
*/
export async function deletePasswordResetToken(username: string): Promise<void> {
const tokens = await getPasswordResetTokens()
delete tokens[username]
await prisma.keyValue.upsert({
where: { key: 'db_password_reset_tokens' },
update: { value: JSON.stringify(tokens) },
create: { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) },
})
}

View File

@@ -0,0 +1,13 @@
import { prisma } from '../prisma'
/**
* Get all credentials as a username->passwordHash map
*/
export async function getCredentials(): 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

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
/**
* Get password change timestamps for all users
*/
export async function getPasswordChangeTimestamps(): 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

@@ -0,0 +1,9 @@
import { prisma } from '../prisma'
/**
* Get password reset tokens
*/
export async function getPasswordResetTokens(): Promise<Record<string, string>> {
const kv = await prisma.keyValue.findUnique({ where: { key: 'db_password_reset_tokens' } })
return kv ? JSON.parse(kv.value) : {}
}

View File

@@ -0,0 +1,8 @@
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'
export { getPasswordResetTokens } from './get-password-reset-tokens'
export { setPasswordResetToken } from './set-password-reset-token'
export { deletePasswordResetToken } from './delete-password-reset-token'

View File

@@ -0,0 +1,17 @@
import { prisma } from '../prisma'
/**
* Set or update a user's credential
*/
export async function setCredential(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

@@ -0,0 +1,13 @@
import { prisma } from '../prisma'
/**
* Set password change timestamps for users
*/
export async function setPasswordChangeTimestamps(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

@@ -0,0 +1,15 @@
import { prisma } from '../prisma'
import { getPasswordResetTokens } from './get-password-reset-tokens'
/**
* Set a password reset token for a user
*/
export async function setPasswordResetToken(username: string, token: string): Promise<void> {
const tokens = await getPasswordResetTokens()
tokens[username] = token
await prisma.keyValue.upsert({
where: { key: 'db_password_reset_tokens' },
update: { value: JSON.stringify(tokens) },
create: { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) },
})
}

View File

@@ -0,0 +1,11 @@
import { prisma } from '../prisma'
import { verifyPassword } from '../hash-password'
/**
* Verify username/password combination
*/
export async function verifyCredentials(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

@@ -0,0 +1,11 @@
/**
* Hash a password using SHA-512
*/
export async function hashPassword(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

@@ -0,0 +1,84 @@
// Types
export type { CssCategory, DropdownConfig, DatabaseSchema, ComponentNode, ComponentConfig } from './types'
// Core
export { hashPassword } from './hash-password'
export { verifyPassword } from './verify-password'
export { initializeDatabase } from './initialize-database'
// Domain re-exports
export * from './users'
export * from './credentials'
export * from './workflows'
export * from './lua-scripts'
export * from './pages'
export * from './schemas'
// Import all for namespace class
import { initializeDatabase } from './initialize-database'
import { hashPassword } from './hash-password'
import { verifyPassword } from './verify-password'
import * as users from './users'
import * as credentials from './credentials'
import * as workflows from './workflows'
import * as luaScripts from './lua-scripts'
import * as pages from './pages'
import * as schemas from './schemas'
/**
* Database namespace class - groups all DB operations as static methods
* No instance state - pure function container for backward compatibility
*/
export class Database {
// Core
static initializeDatabase = initializeDatabase
static hashPassword = hashPassword
static verifyPassword = verifyPassword
// Users
static getUsers = users.getUsers
static setUsers = users.setUsers
static addUser = users.addUser
static updateUser = users.updateUser
static deleteUser = users.deleteUser
static getSuperGod = users.getSuperGod
static transferSuperGodPower = users.transferSuperGodPower
// Credentials
static getCredentials = credentials.getCredentials
static setCredential = credentials.setCredential
static verifyCredentials = credentials.verifyCredentials
static getPasswordChangeTimestamps = credentials.getPasswordChangeTimestamps
static setPasswordChangeTimestamps = credentials.setPasswordChangeTimestamps
static getPasswordResetTokens = credentials.getPasswordResetTokens
static setPasswordResetToken = credentials.setPasswordResetToken
static deletePasswordResetToken = credentials.deletePasswordResetToken
// Workflows
static getWorkflows = workflows.getWorkflows
static setWorkflows = workflows.setWorkflows
static addWorkflow = workflows.addWorkflow
static updateWorkflow = workflows.updateWorkflow
static deleteWorkflow = workflows.deleteWorkflow
// Lua Scripts
static getLuaScripts = luaScripts.getLuaScripts
static setLuaScripts = luaScripts.setLuaScripts
static addLuaScript = luaScripts.addLuaScript
static updateLuaScript = luaScripts.updateLuaScript
static deleteLuaScript = luaScripts.deleteLuaScript
// Pages
static getPages = pages.getPages
static setPages = pages.setPages
static addPage = pages.addPage
static updatePage = pages.updatePage
static deletePage = pages.deletePage
// Schemas
static getSchemas = schemas.getSchemas
static setSchemas = schemas.setSchemas
static addSchema = schemas.addSchema
static updateSchema = schemas.updateSchema
static deleteSchema = schemas.deleteSchema
}

View File

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

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
/**
* Add a Lua script
*/
export async function addLuaScript(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

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

View File

@@ -0,0 +1,17 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
/**
* Get all Lua scripts
*/
export async function getLuaScripts(): 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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,21 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
/**
* Set all Lua scripts (replaces existing)
*/
export async function setLuaScripts(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

@@ -0,0 +1,19 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
/**
* Update a Lua script by ID
*/
export async function updateLuaScript(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

@@ -0,0 +1,19 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
/**
* Add a page
*/
export async function addPage(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

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

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
/**
* Get all pages
*/
export async function getPages(): 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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,22 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
/**
* Set all pages (replaces existing)
*/
export async function setPages(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

@@ -0,0 +1,20 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
/**
* Update a page by ID
*/
export async function updatePage(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

@@ -0,0 +1,4 @@
/**
* Symlink to actual prisma client for db folder imports
*/
export { prisma } from '../prisma'

View File

@@ -0,0 +1,21 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
/**
* Add a schema
*/
export async function addSchema(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

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

View File

@@ -0,0 +1,20 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
/**
* Get all schemas
*/
export async function getSchemas(): 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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,24 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
/**
* Set all schemas (replaces existing)
*/
export async function setSchemas(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

@@ -0,0 +1,22 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
/**
* Update a schema by name
*/
export async function updateSchema(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

@@ -0,0 +1,98 @@
/**
* CSS category configuration
*/
export interface CssCategory {
name: string
classes: string[]
}
/**
* Dropdown configuration
*/
export interface DropdownConfig {
id: string
name: string
label: string
options: Array<{ value: string; label: string }>
}
/**
* Component node in hierarchy
*/
export interface ComponentNode {
id: string
type: string
parentId?: string
childIds: string[]
order: number
pageId: string
}
/**
* Component configuration
*/
export interface ComponentConfig {
id: string
componentId: string
props: Record<string, any>
styles: Record<string, any>
events: Record<string, string>
conditionalRendering?: {
condition: string
luaScriptId?: string
}
}
/**
* Full database schema type
*/
export interface DatabaseSchema {
users: import('../level-types').User[]
credentials: Record<string, string>
workflows: import('../level-types').Workflow[]
luaScripts: import('../level-types').LuaScript[]
pages: import('../level-types').PageConfig[]
schemas: import('../schema-types').ModelSchema[]
appConfig: import('../level-types').AppConfiguration
comments: import('../level-types').Comment[]
componentHierarchy: Record<string, ComponentNode>
componentConfigs: Record<string, ComponentConfig>
godCredentialsExpiry: number
passwordChangeTimestamps: Record<string, number>
firstLoginFlags: Record<string, boolean>
godCredentialsExpiryDuration: number
cssClasses: CssCategory[]
dropdownConfigs: DropdownConfig[]
tenants: import('../level-types').Tenant[]
powerTransferRequests: import('../level-types').PowerTransferRequest[]
smtpConfig: import('../password').SMTPConfig
passwordResetTokens: Record<string, string>
}
/**
* Database keys enum
*/
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

View File

@@ -0,0 +1,21 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
/**
* Add a single user
*/
export async function addUser(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,
},
})
}

View File

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

View File

@@ -0,0 +1,25 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
/**
* Get the SuperGod user (instance owner)
*/
export async function getSuperGod(): Promise<User | null> {
const user = await prisma.user.findFirst({
where: { isInstanceOwner: true },
})
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,
}
}

View File

@@ -0,0 +1,20 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
/**
* Get all users from database
*/
export async function getUsers(): 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

@@ -0,0 +1,7 @@
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'
export { getSuperGod } from './get-super-god'
export { transferSuperGodPower } from './transfer-super-god-power'

View File

@@ -0,0 +1,26 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
/**
* Set all users (replaces existing)
*/
export async function setUsers(users: User[]): Promise<void> {
await prisma.$transaction(async (tx) => {
await tx.user.deleteMany()
for (const user of users) {
await tx.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,
},
})
}
})
}

View File

@@ -0,0 +1,17 @@
import { prisma } from '../prisma'
/**
* Transfer SuperGod power from one user to another
*/
export async function transferSuperGodPower(fromUserId: string, toUserId: string): Promise<void> {
await prisma.$transaction([
prisma.user.update({
where: { id: fromUserId },
data: { isInstanceOwner: false, role: 'god' },
}),
prisma.user.update({
where: { id: toUserId },
data: { isInstanceOwner: true, role: 'supergod' },
}),
])
}

View File

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

View File

@@ -0,0 +1,9 @@
import { hashPassword } from './hash-password'
/**
* Verify a password against a hash
*/
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
const inputHash = await hashPassword(password)
return inputHash === hash
}

View File

@@ -0,0 +1,18 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
/**
* Add a workflow
*/
export async function addWorkflow(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

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

View File

@@ -0,0 +1,17 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
/**
* Get all workflows
*/
export async function getWorkflows(): 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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,21 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
/**
* Set all workflows (replaces existing)
*/
export async function setWorkflows(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

@@ -0,0 +1,19 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
/**
* Update a workflow by ID
*/
export async function updateWorkflow(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

@@ -1,123 +1,15 @@
/**
* Package Loader Module
*
* Handles the initialization and loading of the modular package system.
* Discovers packages from the /packages directory, builds a registry,
* and exports seed data for components, scripts, and metadata.
*
* Supports both modular packages (new) and legacy packages from catalog.
* @deprecated Import from '@/lib/package-loader' folder instead
*/
import { PACKAGE_CATALOG } from './package-catalog'
import { loadPackageComponents } from './declarative-component-renderer'
import { buildPackageRegistry, exportAllPackagesForSeed, type PackageRegistry } from './package-glue'
// Track initialization state to prevent duplicate loading
let isInitialized = false
// Cache the package registry after first load
let packageRegistry: PackageRegistry | null = null
type ModularPackageSeedData = {
components: any[]
scripts: any[]
packages: any[]
}
const emptyModularPackageSeedData: ModularPackageSeedData = {
components: [],
scripts: [],
packages: []
}
let modularPackageSeedData: ModularPackageSeedData | null = null
/**
* Initializes the package system by loading all available packages
* This function is idempotent - calling multiple times is safe
*
* Steps:
* 1. Check if already initialized (return early if so)
* 2. Build package registry from /packages directory
* 3. Extract and export seed data
* 4. Load package components into renderer
* 5. Load legacy packages from catalog
*
* @async
* @returns {Promise<void>}
*/
export async function initializePackageSystem() {
if (isInitialized) return
// Load modular packages from /packages folder structure
try {
// Build registry with all packages found in /packages
packageRegistry = await buildPackageRegistry()
// Extract seed data from modular packages (components, scripts, metadata)
const seedData = exportAllPackagesForSeed(packageRegistry)
modularPackageSeedData = seedData
// TODO: Replace with proper persistent storage (currently no-op)
// Modular package data would be stored in database or KV store
// await Database.setModularPackageComponents(seedData.components)
// await Database.setModularPackageScripts(seedData.scripts)
// await Database.setModularPackageMetadata(seedData.packages)
console.log('Loaded modular package data:', {
components: seedData.components?.length || 0,
scripts: seedData.scripts?.length || 0,
packages: seedData.packages?.length || 0
})
console.log(`✅ Loaded ${seedData.packages.length} modular packages:`,
seedData.packages.map(p => p.name).join(', '))
} catch (error) {
console.warn('⚠️ Could not load modular packages:', error)
}
// Load legacy packages from catalog for backward compatibility
Object.values(PACKAGE_CATALOG).forEach(pkg => {
if (pkg.content) {
loadPackageComponents(pkg.content)
}
})
isInitialized = true
}
export function getInstalledPackageIds(): string[] {
return Object.keys(PACKAGE_CATALOG)
}
export function getPackageContent(packageId: string) {
const pkg = PACKAGE_CATALOG[packageId]
return pkg ? pkg.content : null
}
export function getPackageManifest(packageId: string) {
const pkg = PACKAGE_CATALOG[packageId]
return pkg ? pkg.manifest : null
}
export function getPackageRegistry(): PackageRegistry | null {
return packageRegistry
}
export async function getModularPackageComponents() {
// TODO: Replace with proper database query
// return await Database.getModularPackageComponents() || []
await initializePackageSystem()
return (modularPackageSeedData ?? emptyModularPackageSeedData).components
}
export async function getModularPackageScripts() {
// TODO: Replace with proper database query
// return await Database.getModularPackageScripts() || []
await initializePackageSystem()
return (modularPackageSeedData ?? emptyModularPackageSeedData).scripts
}
export async function getModularPackageMetadata() {
// TODO: Replace with proper database query
// return await Database.getModularPackageMetadata() || []
await initializePackageSystem()
return (modularPackageSeedData ?? emptyModularPackageSeedData).packages
}
export {
initializePackageSystem,
getInstalledPackageIds,
getPackageContent,
getPackageManifest,
getPackageRegistry,
getModularPackageComponents,
getModularPackageScripts,
getModularPackageMetadata,
} from './package-loader/index'

View File

@@ -0,0 +1,8 @@
import { PACKAGE_CATALOG } from '../package-catalog'
/**
* Get list of all installed package IDs from the catalog
*/
export function getInstalledPackageIds(): string[] {
return Object.keys(PACKAGE_CATALOG)
}

View File

@@ -0,0 +1,9 @@
import { initializePackageSystem, getModularSeedData } from './initialize-package-system'
/**
* Get modular package components
*/
export async function getModularPackageComponents(): Promise<any[]> {
await initializePackageSystem()
return getModularSeedData().components
}

View File

@@ -0,0 +1,9 @@
import { initializePackageSystem, getModularSeedData } from './initialize-package-system'
/**
* Get modular package metadata
*/
export async function getModularPackageMetadata(): Promise<any[]> {
await initializePackageSystem()
return getModularSeedData().packages
}

View File

@@ -0,0 +1,9 @@
import { initializePackageSystem, getModularSeedData } from './initialize-package-system'
/**
* Get modular package scripts
*/
export async function getModularPackageScripts(): Promise<any[]> {
await initializePackageSystem()
return getModularSeedData().scripts
}

View File

@@ -0,0 +1,9 @@
import { PACKAGE_CATALOG } from '../package-catalog'
/**
* Get the content of a package by its ID
*/
export function getPackageContent(packageId: string) {
const pkg = PACKAGE_CATALOG[packageId]
return pkg ? pkg.content : null
}

View File

@@ -0,0 +1,9 @@
import { PACKAGE_CATALOG } from '../package-catalog'
/**
* Get the manifest of a package by its ID
*/
export function getPackageManifest(packageId: string) {
const pkg = PACKAGE_CATALOG[packageId]
return pkg ? pkg.manifest : null
}

View File

@@ -0,0 +1,20 @@
import type { PackageRegistry } from '../package-glue'
/**
* Cached package registry singleton
*/
let packageRegistry: PackageRegistry | null = null
/**
* Get the current package registry
*/
export function getPackageRegistry(): PackageRegistry | null {
return packageRegistry
}
/**
* Set the package registry (called during initialization)
*/
export function setPackageRegistry(registry: PackageRegistry | null): void {
packageRegistry = registry
}

View File

@@ -0,0 +1,9 @@
export { initializePackageSystem } from './initialize-package-system'
export { getInstalledPackageIds } from './get-installed-package-ids'
export { getPackageContent } from './get-package-content'
export { getPackageManifest } from './get-package-manifest'
export { getPackageRegistry } from './get-package-registry'
export { getModularPackageComponents } from './get-modular-package-components'
export { getModularPackageScripts } from './get-modular-package-scripts'
export { getModularPackageMetadata } from './get-modular-package-metadata'
export type { ModularPackageSeedData } from './modular-package-seed-data'

View File

@@ -0,0 +1,54 @@
import { PACKAGE_CATALOG } from '../package-catalog'
import { loadPackageComponents } from '../declarative-component-renderer'
import { buildPackageRegistry, exportAllPackagesForSeed } from '../package-glue'
import { setPackageRegistry } from './get-package-registry'
import { emptyModularPackageSeedData, type ModularPackageSeedData } from './modular-package-seed-data'
let isInitialized = false
let modularPackageSeedData: ModularPackageSeedData | null = null
/**
* Get cached modular package seed data
*/
export function getModularSeedData(): ModularPackageSeedData {
return modularPackageSeedData ?? emptyModularPackageSeedData
}
/**
* Initializes the package system by loading all available packages
* This function is idempotent - calling multiple times is safe
*/
export async function initializePackageSystem(): Promise<void> {
if (isInitialized) return
// Load modular packages from /packages folder structure
try {
const packageRegistry = await buildPackageRegistry()
setPackageRegistry(packageRegistry)
const seedData = exportAllPackagesForSeed(packageRegistry)
modularPackageSeedData = seedData
console.log('Loaded modular package data:', {
components: seedData.components?.length || 0,
scripts: seedData.scripts?.length || 0,
packages: seedData.packages?.length || 0,
})
console.log(
`✅ Loaded ${seedData.packages.length} modular packages:`,
seedData.packages.map((p) => p.name).join(', ')
)
} catch (error) {
console.warn('⚠️ Could not load modular packages:', error)
}
// Load legacy packages from catalog for backward compatibility
Object.values(PACKAGE_CATALOG).forEach((pkg) => {
if (pkg.content) {
loadPackageComponents(pkg.content)
}
})
isInitialized = true
}

View File

@@ -0,0 +1,17 @@
/**
* Type for modular package seed data
*/
export type ModularPackageSeedData = {
components: any[]
scripts: any[]
packages: any[]
}
/**
* Empty seed data default
*/
export const emptyModularPackageSeedData: ModularPackageSeedData = {
components: [],
scripts: [],
packages: [],
}

View File

@@ -1,78 +1,10 @@
export function generateScrambledPassword(length: number = 16): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
const array = new Uint8Array(length)
crypto.getRandomValues(array)
let password = ''
for (let i = 0; i < length; i++) {
password += charset[array[i] % charset.length]
}
return password
}
export function generateDeterministicScrambledPassword(seed: string, length: number = 16): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
let hash = 0x811c9dc5
for (let i = 0; i < seed.length; i++) {
hash ^= seed.charCodeAt(i)
hash = Math.imul(hash, 0x01000193) >>> 0
}
let state = hash || 1
let password = ''
for (let i = 0; i < length; i++) {
state = (state + 0x6d2b79f5) >>> 0
let t = state
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
const rand = ((t ^ (t >>> 14)) >>> 0) / 4294967296
password += charset[Math.floor(rand * charset.length)]
}
return password
}
export async function simulateEmailSend(
to: string,
subject: string,
body: string,
smtpConfig?: SMTPConfig
): Promise<{ success: boolean; message: string }> {
console.log('=== EMAIL SIMULATION ===')
console.log(`To: ${to}`)
console.log(`Subject: ${subject}`)
console.log(`Body:\n${body}`)
if (smtpConfig) {
console.log(`SMTP Host: ${smtpConfig.host}:${smtpConfig.port}`)
console.log(`From: ${smtpConfig.fromEmail}`)
}
console.log('========================')
return {
success: true,
message: 'Email simulated successfully (check console)'
}
}
export interface SMTPConfig {
host: string
port: number
secure: boolean
username: string
password: string
fromEmail: string
fromName: string
}
export const DEFAULT_SMTP_CONFIG: SMTPConfig = {
host: 'smtp.example.com',
port: 587,
secure: false,
username: '',
password: '',
fromEmail: 'noreply@metabuilder.com',
fromName: 'MetaBuilder System',
}
/**
* @deprecated Import from '@/lib/password' instead
*/
export {
generateScrambledPassword,
generateDeterministicScrambledPassword,
simulateEmailSend,
DEFAULT_SMTP_CONFIG,
} from './password'
export type { SMTPConfig } from './password'

View File

@@ -0,0 +1,14 @@
import type { SMTPConfig } from './smtp-config'
/**
* Default SMTP configuration
*/
export const DEFAULT_SMTP_CONFIG: SMTPConfig = {
host: 'smtp.example.com',
port: 587,
secure: false,
username: '',
password: '',
fromEmail: 'noreply@metabuilder.com',
fromName: 'MetaBuilder System',
}

View File

@@ -0,0 +1,26 @@
/**
* Generate a deterministic password from a seed string using FNV-1a hash
*/
export function generateDeterministicScrambledPassword(seed: string, length: number = 16): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
let hash = 0x811c9dc5
for (let i = 0; i < seed.length; i++) {
hash ^= seed.charCodeAt(i)
hash = Math.imul(hash, 0x01000193) >>> 0
}
let state = hash || 1
let password = ''
for (let i = 0; i < length; i++) {
state = (state + 0x6d2b79f5) >>> 0
let t = state
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
const rand = ((t ^ (t >>> 14)) >>> 0) / 4294967296
password += charset[Math.floor(rand * charset.length)]
}
return password
}

View File

@@ -0,0 +1,15 @@
/**
* Generate a cryptographically secure random password
*/
export function generateScrambledPassword(length: number = 16): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
const array = new Uint8Array(length)
crypto.getRandomValues(array)
let password = ''
for (let i = 0; i < length; i++) {
password += charset[array[i] % charset.length]
}
return password
}

View File

@@ -0,0 +1,5 @@
export { generateScrambledPassword } from './generate-scrambled-password'
export { generateDeterministicScrambledPassword } from './generate-deterministic-password'
export type { SMTPConfig } from './smtp-config'
export { DEFAULT_SMTP_CONFIG } from './default-smtp-config'
export { simulateEmailSend } from './simulate-email-send'

View File

@@ -0,0 +1,26 @@
import type { SMTPConfig } from './smtp-config'
/**
* Simulate sending an email (logs to console)
*/
export async function simulateEmailSend(
to: string,
subject: string,
body: string,
smtpConfig?: SMTPConfig
): Promise<{ success: boolean; message: string }> {
console.log('=== EMAIL SIMULATION ===')
console.log(`To: ${to}`)
console.log(`Subject: ${subject}`)
console.log(`Body:\n${body}`)
if (smtpConfig) {
console.log(`SMTP Host: ${smtpConfig.host}:${smtpConfig.port}`)
console.log(`From: ${smtpConfig.fromEmail}`)
}
console.log('========================')
return {
success: true,
message: 'Email simulated successfully (check console)',
}
}

View File

@@ -0,0 +1,12 @@
/**
* SMTP configuration interface
*/
export interface SMTPConfig {
host: string
port: number
secure: boolean
username: string
password: string
fromEmail: string
fromName: string
}

View File

@@ -1,234 +1,19 @@
import { createSandboxedLuaEngine, type SandboxedLuaResult } from './sandboxed-lua-engine'
import type { Workflow, WorkflowNode, LuaScript } from './level-types'
/**
* Workflow Engine Module
* @deprecated Import from '@/lib/workflow' instead
*/
export interface WorkflowExecutionContext {
data: any
user?: any
scripts?: LuaScript[]
}
export {
executeWorkflow,
executeNode,
createWorkflowState,
logToWorkflow,
WorkflowEngine,
createWorkflowEngine,
} from './workflow'
export interface WorkflowExecutionResult {
success: boolean
outputs: Record<string, any>
logs: string[]
error?: string
securityWarnings?: string[]
}
export class WorkflowEngine {
// Unit tests for workflow execution paths are in workflow-engine.test.ts
// Coverage includes: node errors, condition short-circuit, Lua security warnings
private logs: string[] = []
private securityWarnings: string[] = []
async executeWorkflow(
workflow: Workflow,
context: WorkflowExecutionContext
): Promise<WorkflowExecutionResult> {
this.logs = []
this.securityWarnings = []
const outputs: Record<string, any> = {}
let currentData = context.data
try {
this.log(`Starting workflow: ${workflow.name}`)
for (let i = 0; i < workflow.nodes.length; i++) {
const node = workflow.nodes[i]
this.log(`Executing node ${i + 1}: ${node.label} (${node.type})`)
const nodeResult = await this.executeNode(node, currentData, context)
if (!nodeResult.success) {
return {
success: false,
outputs,
logs: this.logs,
error: `Node "${node.label}" failed: ${nodeResult.error}`,
securityWarnings: this.securityWarnings,
}
}
outputs[node.id] = nodeResult.output
currentData = nodeResult.output
if (node.type === 'condition' && nodeResult.output === false) {
this.log(`Condition node returned false, stopping workflow`)
break
}
}
this.log(`Workflow completed successfully`)
return {
success: true,
outputs,
logs: this.logs,
securityWarnings: this.securityWarnings,
}
} catch (error) {
return {
success: false,
outputs,
logs: this.logs,
error: error instanceof Error ? error.message : String(error),
securityWarnings: this.securityWarnings,
}
}
}
private async executeNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext
): Promise<{ success: boolean; output?: any; error?: string }> {
try {
switch (node.type) {
case 'trigger':
return { success: true, output: data }
case 'action':
return await this.executeActionNode(node, data, context)
case 'condition':
return await this.executeConditionNode(node, data, context)
case 'lua':
return await this.executeLuaNode(node, data, context)
case 'transform':
return await this.executeTransformNode(node, data, context)
default:
return {
success: false,
error: `Unknown node type: ${node.type}`,
}
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
}
}
}
private async executeActionNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext
): Promise<{ success: boolean; output?: any; error?: string }> {
this.log(`Action: ${node.config.action || 'default'}`)
return { success: true, output: data }
}
private async executeConditionNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext
): Promise<{ success: boolean; output?: any; error?: string }> {
const condition = node.config.condition || 'true'
try {
const result = new Function('data', 'context', `return ${condition}`)(
data,
context
)
this.log(`Condition evaluated to: ${result}`)
return { success: true, output: result }
} catch (error) {
return {
success: false,
error: `Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`,
}
}
}
private async executeLuaNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext
): Promise<{ success: boolean; output?: any; error?: string }> {
const scriptId = node.config.scriptId
if (!scriptId || !context.scripts) {
const luaCode = node.config.code || 'return context.data'
return await this.executeLuaCode(luaCode, data, context)
}
const script = context.scripts.find((s) => s.id === scriptId)
if (!script) {
return {
success: false,
error: `Script not found: ${scriptId}`,
}
}
return await this.executeLuaCode(script.code, data, context)
}
private async executeLuaCode(
code: string,
data: any,
context: WorkflowExecutionContext
): Promise<{ success: boolean; output?: any; error?: string }> {
const engine = createSandboxedLuaEngine()
try {
const luaContext = {
data,
user: context.user,
log: (...args: any[]) => this.log(...args),
}
const result: SandboxedLuaResult = await engine.executeWithSandbox(code, luaContext)
if (result.security.severity === 'critical' || result.security.severity === 'high') {
this.securityWarnings.push(`Security issues detected: ${result.security.issues.map(i => i.message).join(', ')}`)
}
result.execution.logs.forEach((log) => this.log(`[Lua] ${log}`))
if (!result.execution.success) {
return {
success: false,
error: result.execution.error,
}
}
return { success: true, output: result.execution.result }
} finally {
engine.destroy()
}
}
private async executeTransformNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext
): Promise<{ success: boolean; output?: any; error?: string }> {
const transform = node.config.transform || 'data'
try {
const result = new Function('data', 'context', `return ${transform}`)(
data,
context
)
this.log(`Transform result: ${JSON.stringify(result)}`)
return { success: true, output: result }
} catch (error) {
return {
success: false,
error: `Transform failed: ${error instanceof Error ? error.message : String(error)}`,
}
}
}
private log(...args: any[]) {
this.logs.push(args.map((arg) => String(arg)).join(' '))
}
}
export function createWorkflowEngine(): WorkflowEngine {
return new WorkflowEngine()
}
export type {
WorkflowExecutionContext,
WorkflowExecutionResult,
WorkflowState,
} from './workflow'

View File

@@ -0,0 +1,17 @@
import type { WorkflowNode } from '../level-types'
import type { WorkflowExecutionContext } from './workflow-execution-context'
import type { WorkflowState } from './workflow-state'
import { logToWorkflow } from './log-to-workflow'
/**
* Execute an action node
*/
export async function executeActionNode(
node: WorkflowNode,
data: any,
_context: WorkflowExecutionContext,
state: WorkflowState
): Promise<{ success: boolean; output?: any; error?: string }> {
logToWorkflow(state, `Action: ${node.config.action || 'default'}`)
return { success: true, output: data }
}

View File

@@ -0,0 +1,27 @@
import type { WorkflowNode } from '../level-types'
import type { WorkflowExecutionContext } from './workflow-execution-context'
import type { WorkflowState } from './workflow-state'
import { logToWorkflow } from './log-to-workflow'
/**
* Execute a condition node
*/
export async function executeConditionNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext,
state: WorkflowState
): Promise<{ success: boolean; output?: any; error?: string }> {
const condition = node.config.condition || 'true'
try {
const result = new Function('data', 'context', `return ${condition}`)(data, context)
logToWorkflow(state, `Condition evaluated to: ${result}`)
return { success: true, output: result }
} catch (error) {
return {
success: false,
error: `Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`,
}
}
}

View File

@@ -0,0 +1,45 @@
import { createSandboxedLuaEngine, type SandboxedLuaResult } from '../sandboxed-lua-engine'
import type { WorkflowExecutionContext } from './workflow-execution-context'
import type { WorkflowState } from './workflow-state'
import { logToWorkflow } from './log-to-workflow'
/**
* Execute Lua code in sandbox
*/
export async function executeLuaCode(
code: string,
data: any,
context: WorkflowExecutionContext,
state: WorkflowState
): Promise<{ success: boolean; output?: any; error?: string }> {
const engine = createSandboxedLuaEngine()
try {
const luaContext = {
data,
user: context.user,
log: (...args: any[]) => logToWorkflow(state, ...args),
}
const result: SandboxedLuaResult = await engine.executeWithSandbox(code, luaContext)
if (result.security.severity === 'critical' || result.security.severity === 'high') {
state.securityWarnings.push(
`Security issues detected: ${result.security.issues.map((i) => i.message).join(', ')}`
)
}
result.execution.logs.forEach((log) => logToWorkflow(state, `[Lua] ${log}`))
if (!result.execution.success) {
return {
success: false,
error: result.execution.error,
}
}
return { success: true, output: result.execution.result }
} finally {
engine.destroy()
}
}

View File

@@ -0,0 +1,31 @@
import type { WorkflowNode } from '../level-types'
import type { WorkflowExecutionContext } from './workflow-execution-context'
import type { WorkflowState } from './workflow-state'
import { executeLuaCode } from './execute-lua-code'
/**
* Execute a Lua script node
*/
export async function executeLuaNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext,
state: WorkflowState
): Promise<{ success: boolean; output?: any; error?: string }> {
const scriptId = node.config.scriptId
if (!scriptId || !context.scripts) {
const luaCode = node.config.code || 'return context.data'
return await executeLuaCode(luaCode, data, context, state)
}
const script = context.scripts.find((s) => s.id === scriptId)
if (!script) {
return {
success: false,
error: `Script not found: ${scriptId}`,
}
}
return await executeLuaCode(script.code, data, context, state)
}

View File

@@ -0,0 +1,47 @@
import type { WorkflowNode } from '../level-types'
import type { WorkflowExecutionContext } from './workflow-execution-context'
import type { WorkflowState } from './workflow-state'
import { executeActionNode } from './execute-action-node'
import { executeConditionNode } from './execute-condition-node'
import { executeLuaNode } from './execute-lua-node'
import { executeTransformNode } from './execute-transform-node'
/**
* Execute a single workflow node
*/
export async function executeNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext,
state: WorkflowState
): Promise<{ success: boolean; output?: any; error?: string }> {
try {
switch (node.type) {
case 'trigger':
return { success: true, output: data }
case 'action':
return await executeActionNode(node, data, context, state)
case 'condition':
return await executeConditionNode(node, data, context, state)
case 'lua':
return await executeLuaNode(node, data, context, state)
case 'transform':
return await executeTransformNode(node, data, context, state)
default:
return {
success: false,
error: `Unknown node type: ${node.type}`,
}
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
}
}
}

View File

@@ -0,0 +1,27 @@
import type { WorkflowNode } from '../level-types'
import type { WorkflowExecutionContext } from './workflow-execution-context'
import type { WorkflowState } from './workflow-state'
import { logToWorkflow } from './log-to-workflow'
/**
* Execute a transform node
*/
export async function executeTransformNode(
node: WorkflowNode,
data: any,
context: WorkflowExecutionContext,
state: WorkflowState
): Promise<{ success: boolean; output?: any; error?: string }> {
const transform = node.config.transform || 'data'
try {
const result = new Function('data', 'context', `return ${transform}`)(data, context)
logToWorkflow(state, `Transform result: ${JSON.stringify(result)}`)
return { success: true, output: result }
} catch (error) {
return {
success: false,
error: `Transform failed: ${error instanceof Error ? error.message : String(error)}`,
}
}
}

Some files were not shown because too many files have changed in this diff Show More