mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Merge branch 'main' into codex/create-and-organize-test-files
This commit is contained in:
8
dbal/development/src/core/client.ts
Normal file
8
dbal/development/src/core/client.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { DBALConfig } from '../runtime/config'
|
||||
import { DBALClient } from './client/client'
|
||||
export { buildAdapter, buildEntityOperations } from './client/builders'
|
||||
export { normalizeClientConfig, validateClientConfig } from './client/mappers'
|
||||
|
||||
export const createDBALClient = (config: DBALConfig) => new DBALClient(config)
|
||||
|
||||
export { DBALClient }
|
||||
24
dbal/development/src/core/client/builders.ts
Normal file
24
dbal/development/src/core/client/builders.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { DBALAdapter } from '../../adapters/adapter'
|
||||
import type { DBALConfig } from '../../runtime/config'
|
||||
import { createAdapter } from './adapter-factory'
|
||||
import {
|
||||
createComponentOperations,
|
||||
createLuaScriptOperations,
|
||||
createPackageOperations,
|
||||
createPageOperations,
|
||||
createSessionOperations,
|
||||
createUserOperations,
|
||||
createWorkflowOperations
|
||||
} from '../entities'
|
||||
|
||||
export const buildAdapter = (config: DBALConfig): DBALAdapter => createAdapter(config)
|
||||
|
||||
export const buildEntityOperations = (adapter: DBALAdapter) => ({
|
||||
users: createUserOperations(adapter),
|
||||
pages: createPageOperations(adapter),
|
||||
components: createComponentOperations(adapter),
|
||||
workflows: createWorkflowOperations(adapter),
|
||||
luaScripts: createLuaScriptOperations(adapter),
|
||||
packages: createPackageOperations(adapter),
|
||||
sessions: createSessionOperations(adapter)
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file client.ts
|
||||
* @description DBAL Client - Main interface for database operations
|
||||
*
|
||||
*
|
||||
* Provides CRUD operations for all entities through modular operation handlers.
|
||||
* Each entity type has its own dedicated operations module following the
|
||||
* single-responsibility pattern.
|
||||
@@ -9,82 +9,67 @@
|
||||
|
||||
import type { DBALConfig } from '../../runtime/config'
|
||||
import type { DBALAdapter } from '../../adapters/adapter'
|
||||
import { createAdapter } from './adapter-factory'
|
||||
import {
|
||||
createUserOperations,
|
||||
createPageOperations,
|
||||
createComponentOperations,
|
||||
createWorkflowOperations,
|
||||
createLuaScriptOperations,
|
||||
createPackageOperations,
|
||||
createSessionOperations,
|
||||
} from '../entities'
|
||||
import { buildAdapter, buildEntityOperations } from './builders'
|
||||
import { normalizeClientConfig, validateClientConfig } from './mappers'
|
||||
|
||||
export class DBALClient {
|
||||
private adapter: DBALAdapter
|
||||
private config: DBALConfig
|
||||
private operations: ReturnType<typeof buildEntityOperations>
|
||||
|
||||
constructor(config: DBALConfig) {
|
||||
this.config = config
|
||||
|
||||
// Validate configuration
|
||||
if (!config.adapter) {
|
||||
throw new Error('Adapter type must be specified')
|
||||
}
|
||||
if (config.mode !== 'production' && !config.database?.url) {
|
||||
throw new Error('Database URL must be specified for non-production mode')
|
||||
}
|
||||
|
||||
this.adapter = createAdapter(config)
|
||||
this.config = normalizeClientConfig(validateClientConfig(config))
|
||||
this.adapter = buildAdapter(this.config)
|
||||
this.operations = buildEntityOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* User entity operations
|
||||
*/
|
||||
get users() {
|
||||
return createUserOperations(this.adapter)
|
||||
return this.operations.users
|
||||
}
|
||||
|
||||
/**
|
||||
* Page entity operations
|
||||
*/
|
||||
get pages() {
|
||||
return createPageOperations(this.adapter)
|
||||
return this.operations.pages
|
||||
}
|
||||
|
||||
/**
|
||||
* Component hierarchy entity operations
|
||||
*/
|
||||
get components() {
|
||||
return createComponentOperations(this.adapter)
|
||||
return this.operations.components
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow entity operations
|
||||
*/
|
||||
get workflows() {
|
||||
return createWorkflowOperations(this.adapter)
|
||||
return this.operations.workflows
|
||||
}
|
||||
|
||||
/**
|
||||
* Lua script entity operations
|
||||
*/
|
||||
get luaScripts() {
|
||||
return createLuaScriptOperations(this.adapter)
|
||||
return this.operations.luaScripts
|
||||
}
|
||||
|
||||
/**
|
||||
* Package entity operations
|
||||
*/
|
||||
get packages() {
|
||||
return createPackageOperations(this.adapter)
|
||||
return this.operations.packages
|
||||
}
|
||||
|
||||
/**
|
||||
* Session entity operations
|
||||
*/
|
||||
get sessions() {
|
||||
return createSessionOperations(this.adapter)
|
||||
return this.operations.sessions
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
25
dbal/development/src/core/client/mappers.ts
Normal file
25
dbal/development/src/core/client/mappers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { DBALConfig } from '../../runtime/config'
|
||||
import { DBALError } from '../foundation/errors'
|
||||
|
||||
export const validateClientConfig = (config: DBALConfig): DBALConfig => {
|
||||
if (!config.adapter) {
|
||||
throw DBALError.validationError('Adapter type must be specified', [])
|
||||
}
|
||||
|
||||
if (config.mode !== 'production' && !config.database?.url) {
|
||||
throw DBALError.validationError('Database URL must be specified for non-production mode', [])
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export const normalizeClientConfig = (config: DBALConfig): DBALConfig => ({
|
||||
...config,
|
||||
security: {
|
||||
sandbox: config.security?.sandbox ?? 'strict',
|
||||
enableAuditLog: config.security?.enableAuditLog ?? true
|
||||
},
|
||||
performance: {
|
||||
...config.performance
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
export { DBALClient } from './core/client/client'
|
||||
export { DBALClient, createDBALClient } from './core/client'
|
||||
export type { DBALConfig } from './runtime/config'
|
||||
export type * from './core/foundation/types'
|
||||
export { DBALError, DBALErrorCode } from './core/foundation/errors'
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { summarizeWorkflowRuns } from './analyze-workflow-runs'
|
||||
import {
|
||||
analyzeWorkflowRuns,
|
||||
parseWorkflowRuns,
|
||||
summarizeWorkflowRuns,
|
||||
} from './analyze-workflow-runs'
|
||||
|
||||
describe('parseWorkflowRuns', () => {
|
||||
it('normalizes unknown entries and ignores items without numeric IDs', () => {
|
||||
const runs = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Build',
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:10:00Z',
|
||||
head_branch: 'main',
|
||||
event: 'push',
|
||||
},
|
||||
{ id: 'not-a-number' },
|
||||
{
|
||||
id: 2,
|
||||
name: '',
|
||||
status: '',
|
||||
conclusion: 'failure',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
head_branch: '',
|
||||
event: '',
|
||||
},
|
||||
]
|
||||
|
||||
const parsed = parseWorkflowRuns(runs)
|
||||
|
||||
expect(parsed).toHaveLength(2)
|
||||
expect(parsed[0].name).toBe('Build')
|
||||
expect(parsed[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'Unknown workflow',
|
||||
status: 'unknown',
|
||||
conclusion: 'failure',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
head_branch: 'unknown',
|
||||
event: 'unknown',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('summarizeWorkflowRuns', () => {
|
||||
it('summarizes totals, success rate, and failure hotspots', () => {
|
||||
@@ -60,3 +107,24 @@ describe('summarizeWorkflowRuns', () => {
|
||||
expect(summary.mostRecent).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('analyzeWorkflowRuns', () => {
|
||||
it('returns parsed summary and formatted output', () => {
|
||||
const result = analyzeWorkflowRuns([
|
||||
{
|
||||
id: 7,
|
||||
name: 'Deploy',
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
created_at: '2024-02-01T00:00:00Z',
|
||||
updated_at: '2024-02-01T00:05:00Z',
|
||||
head_branch: 'main',
|
||||
event: 'workflow_dispatch',
|
||||
},
|
||||
])
|
||||
|
||||
expect(result.summary.total).toBe(1)
|
||||
expect(result.formatted).toContain('Workflow Run Analysis')
|
||||
expect(result.formatted).toContain('Deploy')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,164 +1,18 @@
|
||||
export type WorkflowRunLike = {
|
||||
id: number
|
||||
name: string
|
||||
status: string
|
||||
conclusion: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
head_branch: string
|
||||
event: string
|
||||
}
|
||||
import { parseWorkflowRuns, WorkflowRunLike } from './parser'
|
||||
import { formatWorkflowRunAnalysis, summarizeWorkflowRuns, WorkflowRunSummary } from './stats'
|
||||
|
||||
export type WorkflowRunSummary = {
|
||||
total: number
|
||||
completed: number
|
||||
successful: number
|
||||
failed: number
|
||||
cancelled: number
|
||||
inProgress: number
|
||||
successRate: number
|
||||
mostRecent: WorkflowRunLike | null
|
||||
recentRuns: WorkflowRunLike[]
|
||||
topFailingWorkflows: Array<{ name: string; failures: number }>
|
||||
failingBranches: Array<{ branch: string; failures: number }>
|
||||
failingEvents: Array<{ event: string; failures: number }>
|
||||
}
|
||||
export type { WorkflowRunLike, WorkflowRunSummary }
|
||||
export { parseWorkflowRuns, summarizeWorkflowRuns, formatWorkflowRunAnalysis }
|
||||
|
||||
const DEFAULT_RECENT_COUNT = 5
|
||||
const DEFAULT_TOP_COUNT = 3
|
||||
|
||||
function toTopCounts(
|
||||
values: string[],
|
||||
topCount: number
|
||||
): Array<{ key: string; count: number }> {
|
||||
const counts = new Map<string, number>()
|
||||
values.forEach((value) => {
|
||||
counts.set(value, (counts.get(value) || 0) + 1)
|
||||
})
|
||||
|
||||
return Array.from(counts.entries())
|
||||
.map(([key, count]) => ({ key, count }))
|
||||
.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
|
||||
.slice(0, topCount)
|
||||
}
|
||||
|
||||
export function summarizeWorkflowRuns(
|
||||
runs: WorkflowRunLike[],
|
||||
export function analyzeWorkflowRuns(
|
||||
runs: unknown[],
|
||||
options?: { recentCount?: number; topCount?: number }
|
||||
): WorkflowRunSummary {
|
||||
const recentCount = options?.recentCount ?? DEFAULT_RECENT_COUNT
|
||||
const topCount = options?.topCount ?? DEFAULT_TOP_COUNT
|
||||
const total = runs.length
|
||||
|
||||
const completedRuns = runs.filter((run) => run.status === 'completed')
|
||||
const successful = completedRuns.filter((run) => run.conclusion === 'success').length
|
||||
const failed = completedRuns.filter((run) => run.conclusion === 'failure').length
|
||||
const cancelled = completedRuns.filter((run) => run.conclusion === 'cancelled').length
|
||||
const inProgress = total - completedRuns.length
|
||||
const successRate = completedRuns.length
|
||||
? Math.round((successful / completedRuns.length) * 100)
|
||||
: 0
|
||||
|
||||
const sortedByUpdated = [...runs].sort(
|
||||
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
)
|
||||
const mostRecent = sortedByUpdated[0] ?? null
|
||||
const recentRuns = sortedByUpdated.slice(0, recentCount)
|
||||
|
||||
const failureRuns = completedRuns.filter((run) => run.conclusion === 'failure')
|
||||
const topFailingWorkflows = toTopCounts(
|
||||
failureRuns.map((run) => run.name),
|
||||
topCount
|
||||
).map((entry) => ({ name: entry.key, failures: entry.count }))
|
||||
|
||||
const failingBranches = toTopCounts(
|
||||
failureRuns.map((run) => run.head_branch),
|
||||
topCount
|
||||
).map((entry) => ({ branch: entry.key, failures: entry.count }))
|
||||
|
||||
const failingEvents = toTopCounts(
|
||||
failureRuns.map((run) => run.event),
|
||||
topCount
|
||||
).map((entry) => ({ event: entry.key, failures: entry.count }))
|
||||
) {
|
||||
const parsedRuns = parseWorkflowRuns(runs)
|
||||
const summary = summarizeWorkflowRuns(parsedRuns, options)
|
||||
|
||||
return {
|
||||
total,
|
||||
completed: completedRuns.length,
|
||||
successful,
|
||||
failed,
|
||||
cancelled,
|
||||
inProgress,
|
||||
successRate,
|
||||
mostRecent,
|
||||
recentRuns,
|
||||
topFailingWorkflows,
|
||||
failingBranches,
|
||||
failingEvents,
|
||||
summary,
|
||||
formatted: formatWorkflowRunAnalysis(summary),
|
||||
}
|
||||
}
|
||||
|
||||
export function formatWorkflowRunAnalysis(summary: WorkflowRunSummary) {
|
||||
const lines: string[] = []
|
||||
|
||||
lines.push('Workflow Run Analysis')
|
||||
lines.push('---------------------')
|
||||
lines.push(`Total runs: ${summary.total}`)
|
||||
lines.push(
|
||||
`Completed: ${summary.completed} (success: ${summary.successful}, failed: ${summary.failed}, cancelled: ${summary.cancelled})`
|
||||
)
|
||||
lines.push(`In progress: ${summary.inProgress}`)
|
||||
lines.push(`Success rate: ${summary.successRate}%`)
|
||||
|
||||
if (summary.mostRecent) {
|
||||
lines.push('')
|
||||
lines.push('Most recent run:')
|
||||
lines.push(
|
||||
`- ${summary.mostRecent.name} | ${summary.mostRecent.status}${
|
||||
summary.mostRecent.conclusion ? `/${summary.mostRecent.conclusion}` : ''
|
||||
} | ${summary.mostRecent.head_branch} | ${summary.mostRecent.updated_at}`
|
||||
)
|
||||
}
|
||||
|
||||
if (summary.recentRuns.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Recent runs:')
|
||||
summary.recentRuns.forEach((run) => {
|
||||
lines.push(
|
||||
`- ${run.name} | ${run.status}${
|
||||
run.conclusion ? `/${run.conclusion}` : ''
|
||||
} | ${run.head_branch} | ${run.updated_at}`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.topFailingWorkflows.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Top failing workflows:')
|
||||
summary.topFailingWorkflows.forEach((entry) => {
|
||||
lines.push(`- ${entry.name}: ${entry.failures}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.failingBranches.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Failing branches:')
|
||||
summary.failingBranches.forEach((entry) => {
|
||||
lines.push(`- ${entry.branch}: ${entry.failures}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.failingEvents.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Failing events:')
|
||||
summary.failingEvents.forEach((entry) => {
|
||||
lines.push(`- ${entry.event}: ${entry.failures}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.total === 0) {
|
||||
lines.push('')
|
||||
lines.push('No workflow runs available to analyze.')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export type WorkflowRunLike = {
|
||||
id: number
|
||||
name: string
|
||||
status: string
|
||||
conclusion: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
head_branch: string
|
||||
event: string
|
||||
}
|
||||
|
||||
const FALLBACK_NAME = 'Unknown workflow'
|
||||
const FALLBACK_STATUS = 'unknown'
|
||||
const FALLBACK_BRANCH = 'unknown'
|
||||
const FALLBACK_EVENT = 'unknown'
|
||||
|
||||
function toStringOrFallback(value: unknown, fallback: string) {
|
||||
return typeof value === 'string' && value.trim() ? value : fallback
|
||||
}
|
||||
|
||||
export function parseWorkflowRuns(runs: unknown[]): WorkflowRunLike[] {
|
||||
if (!Array.isArray(runs)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return runs
|
||||
.map((run) => {
|
||||
const candidate = run as Partial<WorkflowRunLike> & { id?: unknown }
|
||||
const id = Number(candidate.id)
|
||||
|
||||
if (!Number.isFinite(id)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: toStringOrFallback(candidate.name, FALLBACK_NAME),
|
||||
status: toStringOrFallback(candidate.status, FALLBACK_STATUS),
|
||||
conclusion:
|
||||
candidate.conclusion === null || typeof candidate.conclusion === 'string'
|
||||
? candidate.conclusion
|
||||
: null,
|
||||
created_at: toStringOrFallback(candidate.created_at, ''),
|
||||
updated_at: toStringOrFallback(candidate.updated_at, ''),
|
||||
head_branch: toStringOrFallback(candidate.head_branch, FALLBACK_BRANCH),
|
||||
event: toStringOrFallback(candidate.event, FALLBACK_EVENT),
|
||||
}
|
||||
})
|
||||
.filter((run): run is WorkflowRunLike => Boolean(run))
|
||||
}
|
||||
153
frontends/nextjs/src/lib/github/workflows/analysis/runs/stats.ts
Normal file
153
frontends/nextjs/src/lib/github/workflows/analysis/runs/stats.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { WorkflowRunLike } from './parser'
|
||||
|
||||
export type WorkflowRunSummary = {
|
||||
total: number
|
||||
completed: number
|
||||
successful: number
|
||||
failed: number
|
||||
cancelled: number
|
||||
inProgress: number
|
||||
successRate: number
|
||||
mostRecent: WorkflowRunLike | null
|
||||
recentRuns: WorkflowRunLike[]
|
||||
topFailingWorkflows: Array<{ name: string; failures: number }>
|
||||
failingBranches: Array<{ branch: string; failures: number }>
|
||||
failingEvents: Array<{ event: string; failures: number }>
|
||||
}
|
||||
|
||||
const DEFAULT_RECENT_COUNT = 5
|
||||
const DEFAULT_TOP_COUNT = 3
|
||||
|
||||
function toTopCounts(
|
||||
values: string[],
|
||||
topCount: number
|
||||
): Array<{ key: string; count: number }> {
|
||||
const counts = new Map<string, number>()
|
||||
values.forEach((value) => {
|
||||
counts.set(value, (counts.get(value) || 0) + 1)
|
||||
})
|
||||
|
||||
return Array.from(counts.entries())
|
||||
.map(([key, count]) => ({ key, count }))
|
||||
.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
|
||||
.slice(0, topCount)
|
||||
}
|
||||
|
||||
export function summarizeWorkflowRuns(
|
||||
runs: WorkflowRunLike[],
|
||||
options?: { recentCount?: number; topCount?: number }
|
||||
): WorkflowRunSummary {
|
||||
const recentCount = options?.recentCount ?? DEFAULT_RECENT_COUNT
|
||||
const topCount = options?.topCount ?? DEFAULT_TOP_COUNT
|
||||
const total = runs.length
|
||||
|
||||
const completedRuns = runs.filter((run) => run.status === 'completed')
|
||||
const successful = completedRuns.filter((run) => run.conclusion === 'success').length
|
||||
const failed = completedRuns.filter((run) => run.conclusion === 'failure').length
|
||||
const cancelled = completedRuns.filter((run) => run.conclusion === 'cancelled').length
|
||||
const inProgress = total - completedRuns.length
|
||||
const successRate = completedRuns.length
|
||||
? Math.round((successful / completedRuns.length) * 100)
|
||||
: 0
|
||||
|
||||
const sortedByUpdated = [...runs].sort(
|
||||
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
)
|
||||
const mostRecent = sortedByUpdated[0] ?? null
|
||||
const recentRuns = sortedByUpdated.slice(0, recentCount)
|
||||
|
||||
const failureRuns = completedRuns.filter((run) => run.conclusion === 'failure')
|
||||
const topFailingWorkflows = toTopCounts(
|
||||
failureRuns.map((run) => run.name),
|
||||
topCount
|
||||
).map((entry) => ({ name: entry.key, failures: entry.count }))
|
||||
|
||||
const failingBranches = toTopCounts(
|
||||
failureRuns.map((run) => run.head_branch),
|
||||
topCount
|
||||
).map((entry) => ({ branch: entry.key, failures: entry.count }))
|
||||
|
||||
const failingEvents = toTopCounts(
|
||||
failureRuns.map((run) => run.event),
|
||||
topCount
|
||||
).map((entry) => ({ event: entry.key, failures: entry.count }))
|
||||
|
||||
return {
|
||||
total,
|
||||
completed: completedRuns.length,
|
||||
successful,
|
||||
failed,
|
||||
cancelled,
|
||||
inProgress,
|
||||
successRate,
|
||||
mostRecent,
|
||||
recentRuns,
|
||||
topFailingWorkflows,
|
||||
failingBranches,
|
||||
failingEvents,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatWorkflowRunAnalysis(summary: WorkflowRunSummary) {
|
||||
const lines: string[] = []
|
||||
|
||||
lines.push('Workflow Run Analysis')
|
||||
lines.push('---------------------')
|
||||
lines.push(`Total runs: ${summary.total}`)
|
||||
lines.push(
|
||||
`Completed: ${summary.completed} (success: ${summary.successful}, failed: ${summary.failed}, cancelled: ${summary.cancelled})`
|
||||
)
|
||||
lines.push(`In progress: ${summary.inProgress}`)
|
||||
lines.push(`Success rate: ${summary.successRate}%`)
|
||||
|
||||
if (summary.mostRecent) {
|
||||
lines.push('')
|
||||
lines.push('Most recent run:')
|
||||
lines.push(
|
||||
`- ${summary.mostRecent.name} | ${summary.mostRecent.status}${
|
||||
summary.mostRecent.conclusion ? `/${summary.mostRecent.conclusion}` : ''
|
||||
} | ${summary.mostRecent.head_branch} | ${summary.mostRecent.updated_at}`
|
||||
)
|
||||
}
|
||||
|
||||
if (summary.recentRuns.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Recent runs:')
|
||||
summary.recentRuns.forEach((run) => {
|
||||
lines.push(
|
||||
`- ${run.name} | ${run.status}${run.conclusion ? `/${run.conclusion}` : ''} | ${run.head_branch} | ${run.updated_at}`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.topFailingWorkflows.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Top failing workflows:')
|
||||
summary.topFailingWorkflows.forEach((entry) => {
|
||||
lines.push(`- ${entry.name}: ${entry.failures}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.failingBranches.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Failing branches:')
|
||||
summary.failingBranches.forEach((entry) => {
|
||||
lines.push(`- ${entry.branch}: ${entry.failures}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.failingEvents.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Failing events:')
|
||||
summary.failingEvents.forEach((entry) => {
|
||||
lines.push(`- ${entry.event}: ${entry.failures}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (summary.total === 0) {
|
||||
lines.push('')
|
||||
lines.push('No workflow runs available to analyze.')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { PackageTemplateConfig } from '../../types'
|
||||
|
||||
export const ADVANCED_PACKAGE_TEMPLATE_CONFIGS: PackageTemplateConfig[] = []
|
||||
267
frontends/nextjs/src/lib/nerd-mode-ide/templates/configs/base.ts
Normal file
267
frontends/nextjs/src/lib/nerd-mode-ide/templates/configs/base.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import type { PackageTemplateConfig, ReactAppTemplateConfig } from '../../types'
|
||||
|
||||
export const BASE_REACT_APP_TEMPLATE_CONFIG: ReactAppTemplateConfig = {
|
||||
id: 'react_next_starter',
|
||||
name: 'Next.js Web App',
|
||||
description: 'A clean Next.js starter with app router, hero component, and typed config files.',
|
||||
rootName: 'web_app',
|
||||
tags: ['nextjs', 'react', 'web', 'starter'],
|
||||
}
|
||||
|
||||
const socialHubComponents = [
|
||||
{
|
||||
id: 'social_hub_root',
|
||||
type: 'Stack',
|
||||
props: { className: 'flex flex-col gap-6' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_hero',
|
||||
type: 'Card',
|
||||
props: { className: 'p-6' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_heading',
|
||||
type: 'Heading',
|
||||
props: { children: 'Social Hub', level: '2', className: 'text-2xl font-bold' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_subtitle',
|
||||
type: 'Text',
|
||||
props: { children: 'A modern feed for creator updates, curated stories, and live moments.' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stats',
|
||||
type: 'Grid',
|
||||
props: { className: 'grid grid-cols-3 gap-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_1',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_label_1',
|
||||
type: 'Text',
|
||||
props: { children: 'Creators live', className: 'text-sm text-muted-foreground' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_value_1',
|
||||
type: 'Heading',
|
||||
props: { children: '128', level: '3', className: 'text-xl font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_2',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_label_2',
|
||||
type: 'Text',
|
||||
props: { children: 'Trending tags', className: 'text-sm text-muted-foreground' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_value_2',
|
||||
type: 'Heading',
|
||||
props: { children: '42', level: '3', className: 'text-xl font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_3',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_label_3',
|
||||
type: 'Text',
|
||||
props: { children: 'Live rooms', className: 'text-sm text-muted-foreground' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_value_3',
|
||||
type: 'Heading',
|
||||
props: { children: '7', level: '3', className: 'text-xl font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_composer_label',
|
||||
type: 'Label',
|
||||
props: { children: 'Share a quick update' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer_input',
|
||||
type: 'Textarea',
|
||||
props: { placeholder: 'What are you building today?', rows: 3 },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer_actions',
|
||||
type: 'Flex',
|
||||
props: { className: 'flex gap-2' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_composer_publish',
|
||||
type: 'Button',
|
||||
props: { children: 'Publish', variant: 'default' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer_media',
|
||||
type: 'Button',
|
||||
props: { children: 'Add media', variant: 'outline' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed',
|
||||
type: 'Stack',
|
||||
props: { className: 'flex flex-col gap-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_feed_post_1',
|
||||
type: 'Card',
|
||||
props: { className: 'p-5' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_feed_post_1_title',
|
||||
type: 'Heading',
|
||||
props: { children: 'Launch day recap', level: '3', className: 'text-lg font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_1_body',
|
||||
type: 'Text',
|
||||
props: { children: 'We shipped the new live rooms and saw a 32% boost in engagement.' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_1_badge',
|
||||
type: 'Badge',
|
||||
props: { children: 'Community' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_2',
|
||||
type: 'Card',
|
||||
props: { className: 'p-5' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_feed_post_2_title',
|
||||
type: 'Heading',
|
||||
props: { children: 'Creator spotlight', level: '3', className: 'text-lg font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_2_body',
|
||||
type: 'Text',
|
||||
props: { children: 'Nova shares her workflow for livestreaming and managing subscribers.' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_2_badge',
|
||||
type: 'Badge',
|
||||
props: { children: 'Spotlight', variant: 'secondary' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const socialHubExamples = {
|
||||
feedItems: [
|
||||
{
|
||||
id: 'post_001',
|
||||
author: 'Nova',
|
||||
title: 'Launch day recap',
|
||||
summary: 'We shipped live rooms and doubled community sessions.',
|
||||
tags: ['launch', 'community'],
|
||||
},
|
||||
{
|
||||
id: 'post_002',
|
||||
author: 'Kai',
|
||||
title: 'Build log: day 42',
|
||||
summary: 'Refined the moderation pipeline and added creator scorecards.',
|
||||
tags: ['buildinpublic'],
|
||||
},
|
||||
],
|
||||
trendingTags: ['#buildinpublic', '#metabuilder', '#live'],
|
||||
rooms: [
|
||||
{ id: 'room_1', title: 'Creator Q&A', host: 'Eli', live: true },
|
||||
{ id: 'room_2', title: 'Patch Notes', host: 'Nova', live: false },
|
||||
],
|
||||
}
|
||||
|
||||
const socialHubLuaScripts = [
|
||||
{
|
||||
fileName: 'init.lua',
|
||||
description: 'Lifecycle hooks for package installation.',
|
||||
code: 'local M = {}\\n\\nfunction M.on_install(context)\\n return { message = "Social Hub installed", version = context.version }\\nend\\n\\nfunction M.on_uninstall()\\n return { message = "Social Hub removed" }\\nend\\n\\nreturn M',
|
||||
},
|
||||
{
|
||||
fileName: 'permissions.lua',
|
||||
description: 'Role-based access rules for posting and moderation.',
|
||||
code: 'local Permissions = {}\\n\\nfunction Permissions.can_post(user)\\n return user and (user.role == "user" or user.role == "admin" or user.role == "god")\\nend\\n\\nfunction Permissions.can_moderate(user)\\n return user and (user.role == "admin" or user.role == "god" or user.role == "supergod")\\nend\\n\\nreturn Permissions',
|
||||
},
|
||||
{
|
||||
fileName: 'feed_rank.lua',
|
||||
description: 'Score feed items based on recency and engagement.',
|
||||
code: 'local FeedRank = {}\\n\\nfunction FeedRank.score(item)\\n local freshness = item.age_minutes and (100 - item.age_minutes) or 50\\n local engagement = (item.likes or 0) * 2 + (item.comments or 0) * 3\\n return freshness + engagement\\nend\\n\\nreturn FeedRank',
|
||||
},
|
||||
{
|
||||
fileName: 'moderation.lua',
|
||||
description: 'Flag content for review using lightweight heuristics.',
|
||||
code: 'local Moderation = {}\\n\\nfunction Moderation.flag(content)\\n local lowered = string.lower(content or "")\\n if string.find(lowered, "spam") then\\n return { flagged = true, reason = "spam_keyword" }\\n end\\n return { flagged = false }\\nend\\n\\nreturn Moderation',
|
||||
},
|
||||
{
|
||||
fileName: 'analytics.lua',
|
||||
description: 'Aggregate engagement signals for dashboards.',
|
||||
code: 'local Analytics = {}\\n\\nfunction Analytics.aggregate(events)\\n local summary = { views = 0, likes = 0, comments = 0 }\\n for _, event in ipairs(events or {}) do\\n summary.views = summary.views + (event.views or 0)\\n summary.likes = summary.likes + (event.likes or 0)\\n summary.comments = summary.comments + (event.comments or 0)\\n end\\n return summary\\nend\\n\\nreturn Analytics',
|
||||
},
|
||||
]
|
||||
|
||||
export const BASE_PACKAGE_TEMPLATE_CONFIGS: PackageTemplateConfig[] = [
|
||||
{
|
||||
id: 'package_social_hub',
|
||||
name: 'Social Hub Package',
|
||||
description: 'A package blueprint for social feeds, creator updates, and live rooms.',
|
||||
rootName: 'social_hub',
|
||||
packageId: 'social_hub',
|
||||
author: 'MetaBuilder',
|
||||
version: '1.0.0',
|
||||
category: 'social',
|
||||
summary: 'Modern social feed with creator tools and live rooms.',
|
||||
components: socialHubComponents,
|
||||
examples: socialHubExamples,
|
||||
luaScripts: socialHubLuaScripts,
|
||||
tags: ['package', 'social', 'feed', 'lua'],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { PackageTemplateConfig } from '../../types'
|
||||
|
||||
export const EXPERIMENTAL_PACKAGE_TEMPLATE_CONFIGS: PackageTemplateConfig[] = []
|
||||
@@ -1,267 +1,12 @@
|
||||
import type { PackageTemplateConfig, ReactAppTemplateConfig } from './types'
|
||||
import type { PackageTemplateConfig, ReactAppTemplateConfig } from '../types'
|
||||
import { ADVANCED_PACKAGE_TEMPLATE_CONFIGS } from './configs/advanced'
|
||||
import { BASE_PACKAGE_TEMPLATE_CONFIGS, BASE_REACT_APP_TEMPLATE_CONFIG } from './configs/base'
|
||||
import { EXPERIMENTAL_PACKAGE_TEMPLATE_CONFIGS } from './configs/experimental'
|
||||
|
||||
export const REACT_APP_TEMPLATE_CONFIG: ReactAppTemplateConfig = {
|
||||
id: 'react_next_starter',
|
||||
name: 'Next.js Web App',
|
||||
description: 'A clean Next.js starter with app router, hero component, and typed config files.',
|
||||
rootName: 'web_app',
|
||||
tags: ['nextjs', 'react', 'web', 'starter'],
|
||||
}
|
||||
|
||||
const socialHubComponents = [
|
||||
{
|
||||
id: 'social_hub_root',
|
||||
type: 'Stack',
|
||||
props: { className: 'flex flex-col gap-6' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_hero',
|
||||
type: 'Card',
|
||||
props: { className: 'p-6' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_heading',
|
||||
type: 'Heading',
|
||||
props: { children: 'Social Hub', level: '2', className: 'text-2xl font-bold' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_subtitle',
|
||||
type: 'Text',
|
||||
props: { children: 'A modern feed for creator updates, curated stories, and live moments.' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stats',
|
||||
type: 'Grid',
|
||||
props: { className: 'grid grid-cols-3 gap-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_1',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_label_1',
|
||||
type: 'Text',
|
||||
props: { children: 'Creators live', className: 'text-sm text-muted-foreground' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_value_1',
|
||||
type: 'Heading',
|
||||
props: { children: '128', level: '3', className: 'text-xl font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_2',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_label_2',
|
||||
type: 'Text',
|
||||
props: { children: 'Trending tags', className: 'text-sm text-muted-foreground' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_value_2',
|
||||
type: 'Heading',
|
||||
props: { children: '42', level: '3', className: 'text-xl font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_3',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_stat_label_3',
|
||||
type: 'Text',
|
||||
props: { children: 'Live rooms', className: 'text-sm text-muted-foreground' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_stat_value_3',
|
||||
type: 'Heading',
|
||||
props: { children: '7', level: '3', className: 'text-xl font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer',
|
||||
type: 'Card',
|
||||
props: { className: 'p-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_composer_label',
|
||||
type: 'Label',
|
||||
props: { children: 'Share a quick update' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer_input',
|
||||
type: 'Textarea',
|
||||
props: { placeholder: 'What are you building today?', rows: 3 },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer_actions',
|
||||
type: 'Flex',
|
||||
props: { className: 'flex gap-2' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_composer_publish',
|
||||
type: 'Button',
|
||||
props: { children: 'Publish', variant: 'default' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_composer_media',
|
||||
type: 'Button',
|
||||
props: { children: 'Add media', variant: 'outline' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed',
|
||||
type: 'Stack',
|
||||
props: { className: 'flex flex-col gap-4' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_feed_post_1',
|
||||
type: 'Card',
|
||||
props: { className: 'p-5' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_feed_post_1_title',
|
||||
type: 'Heading',
|
||||
props: { children: 'Launch day recap', level: '3', className: 'text-lg font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_1_body',
|
||||
type: 'Text',
|
||||
props: { children: 'We shipped the new live rooms and saw a 32% boost in engagement.' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_1_badge',
|
||||
type: 'Badge',
|
||||
props: { children: 'Community' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_2',
|
||||
type: 'Card',
|
||||
props: { className: 'p-5' },
|
||||
children: [
|
||||
{
|
||||
id: 'social_hub_feed_post_2_title',
|
||||
type: 'Heading',
|
||||
props: { children: 'Creator spotlight', level: '3', className: 'text-lg font-semibold' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_2_body',
|
||||
type: 'Text',
|
||||
props: { children: 'Nova shares her workflow for livestreaming and managing subscribers.' },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'social_hub_feed_post_2_badge',
|
||||
type: 'Badge',
|
||||
props: { children: 'Spotlight', variant: 'secondary' },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const socialHubExamples = {
|
||||
feedItems: [
|
||||
{
|
||||
id: 'post_001',
|
||||
author: 'Nova',
|
||||
title: 'Launch day recap',
|
||||
summary: 'We shipped live rooms and doubled community sessions.',
|
||||
tags: ['launch', 'community'],
|
||||
},
|
||||
{
|
||||
id: 'post_002',
|
||||
author: 'Kai',
|
||||
title: 'Build log: day 42',
|
||||
summary: 'Refined the moderation pipeline and added creator scorecards.',
|
||||
tags: ['buildinpublic'],
|
||||
},
|
||||
],
|
||||
trendingTags: ['#buildinpublic', '#metabuilder', '#live'],
|
||||
rooms: [
|
||||
{ id: 'room_1', title: 'Creator Q&A', host: 'Eli', live: true },
|
||||
{ id: 'room_2', title: 'Patch Notes', host: 'Nova', live: false },
|
||||
],
|
||||
}
|
||||
|
||||
const socialHubLuaScripts = [
|
||||
{
|
||||
fileName: 'init.lua',
|
||||
description: 'Lifecycle hooks for package installation.',
|
||||
code: 'local M = {}\n\nfunction M.on_install(context)\n return { message = "Social Hub installed", version = context.version }\nend\n\nfunction M.on_uninstall()\n return { message = "Social Hub removed" }\nend\n\nreturn M',
|
||||
},
|
||||
{
|
||||
fileName: 'permissions.lua',
|
||||
description: 'Role-based access rules for posting and moderation.',
|
||||
code: 'local Permissions = {}\n\nfunction Permissions.can_post(user)\n return user and (user.role == "user" or user.role == "admin" or user.role == "god")\nend\n\nfunction Permissions.can_moderate(user)\n return user and (user.role == "admin" or user.role == "god" or user.role == "supergod")\nend\n\nreturn Permissions',
|
||||
},
|
||||
{
|
||||
fileName: 'feed_rank.lua',
|
||||
description: 'Score feed items based on recency and engagement.',
|
||||
code: 'local FeedRank = {}\n\nfunction FeedRank.score(item)\n local freshness = item.age_minutes and (100 - item.age_minutes) or 50\n local engagement = (item.likes or 0) * 2 + (item.comments or 0) * 3\n return freshness + engagement\nend\n\nreturn FeedRank',
|
||||
},
|
||||
{
|
||||
fileName: 'moderation.lua',
|
||||
description: 'Flag content for review using lightweight heuristics.',
|
||||
code: 'local Moderation = {}\n\nfunction Moderation.flag(content)\n local lowered = string.lower(content or "")\n if string.find(lowered, "spam") then\n return { flagged = true, reason = "spam_keyword" }\n end\n return { flagged = false }\nend\n\nreturn Moderation',
|
||||
},
|
||||
{
|
||||
fileName: 'analytics.lua',
|
||||
description: 'Aggregate engagement signals for dashboards.',
|
||||
code: 'local Analytics = {}\n\nfunction Analytics.aggregate(events)\n local summary = { views = 0, likes = 0, comments = 0 }\n for _, event in ipairs(events or {}) do\n summary.views = summary.views + (event.views or 0)\n summary.likes = summary.likes + (event.likes or 0)\n summary.comments = summary.comments + (event.comments or 0)\n end\n return summary\nend\n\nreturn Analytics',
|
||||
},
|
||||
]
|
||||
export const REACT_APP_TEMPLATE_CONFIG: ReactAppTemplateConfig = BASE_REACT_APP_TEMPLATE_CONFIG
|
||||
|
||||
export const PACKAGE_TEMPLATE_CONFIGS: PackageTemplateConfig[] = [
|
||||
{
|
||||
id: 'package_social_hub',
|
||||
name: 'Social Hub Package',
|
||||
description: 'A package blueprint for social feeds, creator updates, and live rooms.',
|
||||
rootName: 'social_hub',
|
||||
packageId: 'social_hub',
|
||||
author: 'MetaBuilder',
|
||||
version: '1.0.0',
|
||||
category: 'social',
|
||||
summary: 'Modern social feed with creator tools and live rooms.',
|
||||
components: socialHubComponents,
|
||||
examples: socialHubExamples,
|
||||
luaScripts: socialHubLuaScripts,
|
||||
tags: ['package', 'social', 'feed', 'lua'],
|
||||
},
|
||||
...BASE_PACKAGE_TEMPLATE_CONFIGS,
|
||||
...ADVANCED_PACKAGE_TEMPLATE_CONFIGS,
|
||||
...EXPERIMENTAL_PACKAGE_TEMPLATE_CONFIGS,
|
||||
]
|
||||
|
||||
@@ -1,308 +1,6 @@
|
||||
import type { SchemaConfig } from '../types/schema-types'
|
||||
import { defaultApps } from './default/components'
|
||||
|
||||
export const defaultSchema: SchemaConfig = {
|
||||
apps: [
|
||||
{
|
||||
name: 'blog',
|
||||
label: 'Blog',
|
||||
models: [
|
||||
{
|
||||
name: 'post',
|
||||
label: 'Post',
|
||||
labelPlural: 'Posts',
|
||||
icon: 'Article',
|
||||
listDisplay: ['title', 'author', 'status', 'publishedAt'],
|
||||
listFilter: ['status', 'author'],
|
||||
searchFields: ['title', 'content'],
|
||||
ordering: ['-publishedAt'],
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
label: 'Title',
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 3,
|
||||
maxLength: 200,
|
||||
},
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'string',
|
||||
label: 'Slug',
|
||||
required: true,
|
||||
unique: true,
|
||||
helpText: 'URL-friendly version of the title',
|
||||
validation: {
|
||||
pattern: '^[a-z0-9-]+$',
|
||||
},
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
required: true,
|
||||
helpText: 'Main post content',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'text',
|
||||
label: 'Excerpt',
|
||||
required: false,
|
||||
helpText: ['Short summary of the post', 'Used in list views and previews'],
|
||||
validation: {
|
||||
maxLength: 500,
|
||||
},
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
type: 'relation',
|
||||
label: 'Author',
|
||||
required: true,
|
||||
relatedModel: 'author',
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
label: 'Status',
|
||||
required: true,
|
||||
default: 'draft',
|
||||
choices: [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
],
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'featured',
|
||||
type: 'boolean',
|
||||
label: 'Featured',
|
||||
default: false,
|
||||
helpText: 'Display on homepage',
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'datetime',
|
||||
label: 'Published At',
|
||||
required: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'json',
|
||||
label: 'Tags',
|
||||
required: false,
|
||||
helpText: 'JSON array of tag strings',
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'views',
|
||||
type: 'number',
|
||||
label: 'Views',
|
||||
default: 0,
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
listDisplay: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
labelPlural: 'Authors',
|
||||
icon: 'User',
|
||||
listDisplay: ['name', 'email', 'active', 'createdAt'],
|
||||
listFilter: ['active'],
|
||||
searchFields: ['name', 'email'],
|
||||
ordering: ['name'],
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 2,
|
||||
maxLength: 100,
|
||||
},
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
required: true,
|
||||
unique: true,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'bio',
|
||||
type: 'text',
|
||||
label: 'Bio',
|
||||
required: false,
|
||||
helpText: 'Author biography',
|
||||
validation: {
|
||||
maxLength: 1000,
|
||||
},
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'url',
|
||||
label: 'Website',
|
||||
required: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
type: 'boolean',
|
||||
label: 'Active',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'datetime',
|
||||
label: 'Created At',
|
||||
required: true,
|
||||
editable: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ecommerce',
|
||||
label: 'E-Commerce',
|
||||
models: [
|
||||
{
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
labelPlural: 'Products',
|
||||
icon: 'ShoppingCart',
|
||||
listDisplay: ['name', 'price', 'stock', 'available'],
|
||||
listFilter: ['available', 'category'],
|
||||
searchFields: ['name', 'description'],
|
||||
ordering: ['name'],
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Product Name',
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 3,
|
||||
maxLength: 200,
|
||||
},
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
label: 'Description',
|
||||
required: false,
|
||||
helpText: 'Product description',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
type: 'number',
|
||||
label: 'Price',
|
||||
required: true,
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
type: 'number',
|
||||
label: 'Stock',
|
||||
required: true,
|
||||
default: 0,
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
label: 'Category',
|
||||
required: true,
|
||||
choices: [
|
||||
{ value: 'electronics', label: 'Electronics' },
|
||||
{ value: 'clothing', label: 'Clothing' },
|
||||
{ value: 'books', label: 'Books' },
|
||||
{ value: 'home', label: 'Home & Garden' },
|
||||
{ value: 'toys', label: 'Toys' },
|
||||
],
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'available',
|
||||
type: 'boolean',
|
||||
label: 'Available',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
apps: defaultApps,
|
||||
}
|
||||
|
||||
54
frontends/nextjs/src/lib/schema/default/components.ts
Normal file
54
frontends/nextjs/src/lib/schema/default/components.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { AppSchema, ModelSchema } from '../../types/schema-types'
|
||||
import { authorFields, postFields, productFields } from './forms'
|
||||
|
||||
export const blogModels: ModelSchema[] = [
|
||||
{
|
||||
name: 'post',
|
||||
label: 'Post',
|
||||
labelPlural: 'Posts',
|
||||
icon: 'Article',
|
||||
listDisplay: ['title', 'author', 'status', 'publishedAt'],
|
||||
listFilter: ['status', 'author'],
|
||||
searchFields: ['title', 'content'],
|
||||
ordering: ['-publishedAt'],
|
||||
fields: postFields,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
labelPlural: 'Authors',
|
||||
icon: 'User',
|
||||
listDisplay: ['name', 'email', 'active', 'createdAt'],
|
||||
listFilter: ['active'],
|
||||
searchFields: ['name', 'email'],
|
||||
ordering: ['name'],
|
||||
fields: authorFields,
|
||||
},
|
||||
]
|
||||
|
||||
export const ecommerceModels: ModelSchema[] = [
|
||||
{
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
labelPlural: 'Products',
|
||||
icon: 'ShoppingCart',
|
||||
listDisplay: ['name', 'price', 'stock', 'available'],
|
||||
listFilter: ['available', 'category'],
|
||||
searchFields: ['name', 'description'],
|
||||
ordering: ['name'],
|
||||
fields: productFields,
|
||||
},
|
||||
]
|
||||
|
||||
export const defaultApps: AppSchema[] = [
|
||||
{
|
||||
name: 'blog',
|
||||
label: 'Blog',
|
||||
models: blogModels,
|
||||
},
|
||||
{
|
||||
name: 'ecommerce',
|
||||
label: 'E-Commerce',
|
||||
models: ecommerceModels,
|
||||
},
|
||||
]
|
||||
244
frontends/nextjs/src/lib/schema/default/forms.ts
Normal file
244
frontends/nextjs/src/lib/schema/default/forms.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { FieldSchema } from '../../types/schema-types'
|
||||
import { authorValidations, postValidations, productValidations } from './validation'
|
||||
|
||||
export const postFields: FieldSchema[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
label: 'Title',
|
||||
required: true,
|
||||
validation: postValidations.title,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'string',
|
||||
label: 'Slug',
|
||||
required: true,
|
||||
unique: true,
|
||||
helpText: 'URL-friendly version of the title',
|
||||
validation: postValidations.slug,
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
required: true,
|
||||
helpText: 'Main post content',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'text',
|
||||
label: 'Excerpt',
|
||||
required: false,
|
||||
helpText: ['Short summary of the post', 'Used in list views and previews'],
|
||||
validation: postValidations.excerpt,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
type: 'relation',
|
||||
label: 'Author',
|
||||
required: true,
|
||||
relatedModel: 'author',
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
label: 'Status',
|
||||
required: true,
|
||||
default: 'draft',
|
||||
choices: [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
],
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'featured',
|
||||
type: 'boolean',
|
||||
label: 'Featured',
|
||||
default: false,
|
||||
helpText: 'Display on homepage',
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'datetime',
|
||||
label: 'Published At',
|
||||
required: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'json',
|
||||
label: 'Tags',
|
||||
required: false,
|
||||
helpText: 'JSON array of tag strings',
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'views',
|
||||
type: 'number',
|
||||
label: 'Views',
|
||||
default: 0,
|
||||
validation: postValidations.views,
|
||||
listDisplay: false,
|
||||
},
|
||||
]
|
||||
|
||||
export const authorFields: FieldSchema[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
validation: authorValidations.name,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
required: true,
|
||||
unique: true,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'bio',
|
||||
type: 'text',
|
||||
label: 'Bio',
|
||||
required: false,
|
||||
helpText: 'Author biography',
|
||||
validation: authorValidations.bio,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'url',
|
||||
label: 'Website',
|
||||
required: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
type: 'boolean',
|
||||
label: 'Active',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'datetime',
|
||||
label: 'Created At',
|
||||
required: true,
|
||||
editable: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const productFields: FieldSchema[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Product Name',
|
||||
required: true,
|
||||
validation: productValidations.name,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
label: 'Description',
|
||||
required: false,
|
||||
helpText: 'Product description',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
type: 'number',
|
||||
label: 'Price',
|
||||
required: true,
|
||||
validation: productValidations.price,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
type: 'number',
|
||||
label: 'Stock',
|
||||
required: true,
|
||||
default: 0,
|
||||
validation: productValidations.stock,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
label: 'Category',
|
||||
required: true,
|
||||
choices: [
|
||||
{ value: 'electronics', label: 'Electronics' },
|
||||
{ value: 'clothing', label: 'Clothing' },
|
||||
{ value: 'books', label: 'Books' },
|
||||
{ value: 'home', label: 'Home & Garden' },
|
||||
{ value: 'toys', label: 'Toys' },
|
||||
],
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'available',
|
||||
type: 'boolean',
|
||||
label: 'Available',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
]
|
||||
19
frontends/nextjs/src/lib/schema/default/validation.ts
Normal file
19
frontends/nextjs/src/lib/schema/default/validation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { FieldSchema } from '../../types/schema-types'
|
||||
|
||||
export const postValidations: Record<string, FieldSchema['validation']> = {
|
||||
title: { minLength: 3, maxLength: 200 },
|
||||
slug: { pattern: '^[a-z0-9-]+$' },
|
||||
excerpt: { maxLength: 500 },
|
||||
views: { min: 0 },
|
||||
}
|
||||
|
||||
export const authorValidations: Record<string, FieldSchema['validation']> = {
|
||||
name: { minLength: 2, maxLength: 100 },
|
||||
bio: { maxLength: 1000 },
|
||||
}
|
||||
|
||||
export const productValidations: Record<string, FieldSchema['validation']> = {
|
||||
name: { minLength: 3, maxLength: 200 },
|
||||
price: { min: 0 },
|
||||
stock: { min: 0 },
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Schema utilities exports
|
||||
export * from './schema-utils'
|
||||
export { defaultSchema } from './default-schema'
|
||||
export * from './default/components'
|
||||
export * from './default/forms'
|
||||
export * from './default/validation'
|
||||
|
||||
@@ -4,181 +4,12 @@
|
||||
*/
|
||||
|
||||
import type { SecurityPattern } from '../types'
|
||||
import { JAVASCRIPT_INJECTION_PATTERNS } from './javascript/injection'
|
||||
import { JAVASCRIPT_MISC_PATTERNS } from './javascript/misc'
|
||||
import { JAVASCRIPT_XSS_PATTERNS } from './javascript/xss'
|
||||
|
||||
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'
|
||||
}
|
||||
...JAVASCRIPT_INJECTION_PATTERNS,
|
||||
...JAVASCRIPT_XSS_PATTERNS,
|
||||
...JAVASCRIPT_MISC_PATTERNS
|
||||
]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { SecurityPattern } from '../../types'
|
||||
|
||||
export const JAVASCRIPT_INJECTION_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: /import\s+.*\s+from\s+['"]https?:/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'critical',
|
||||
message: 'Remote code import detected',
|
||||
recommendation: 'Only import from trusted, local sources'
|
||||
},
|
||||
{
|
||||
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: /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'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { SecurityPattern } from '../../types'
|
||||
|
||||
export const JAVASCRIPT_MISC_PATTERNS: SecurityPattern[] = [
|
||||
{
|
||||
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: /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: /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'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { SecurityPattern } from '../../types'
|
||||
|
||||
export const JAVASCRIPT_XSS_PATTERNS: SecurityPattern[] = [
|
||||
{
|
||||
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: /<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'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,234 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { scanForVulnerabilities, securityScanner } from '@/lib/security-scanner'
|
||||
|
||||
describe('security-scanner detection', () => {
|
||||
describe('scanJavaScript', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag eval usage as critical',
|
||||
code: ['const safe = true;', 'const result = eval("1 + 1")'].join('\n'),
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'dangerous',
|
||||
expectedIssuePattern: 'eval',
|
||||
expectedLine: 2,
|
||||
},
|
||||
{
|
||||
name: 'warn on localStorage usage but stay safe',
|
||||
code: 'localStorage.setItem("k", "v")',
|
||||
expectedSeverity: 'low',
|
||||
expectedSafe: true,
|
||||
expectedIssueType: 'warning',
|
||||
expectedIssuePattern: 'localStorage',
|
||||
},
|
||||
{
|
||||
name: 'return safe for benign code',
|
||||
code: 'const sum = (a, b) => a + b',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])(
|
||||
'should $name',
|
||||
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
|
||||
const result = securityScanner.scanJavaScript(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
if (expectedLine !== undefined) {
|
||||
expect(issue?.line).toBe(expectedLine)
|
||||
}
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('scanLua', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag os.execute usage as critical',
|
||||
code: 'os.execute("rm -rf /")',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'malicious',
|
||||
expectedIssuePattern: 'os.execute',
|
||||
},
|
||||
{
|
||||
name: 'return safe for simple Lua function',
|
||||
code: 'function add(a, b) return a + b end',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanLua(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanJSON', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag invalid JSON as medium severity',
|
||||
json: '{"value": }',
|
||||
expectedSeverity: 'medium',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: 'JSON parse error',
|
||||
},
|
||||
{
|
||||
name: 'flag prototype pollution in JSON as critical',
|
||||
json: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: '__proto__',
|
||||
},
|
||||
{
|
||||
name: 'return safe for valid JSON',
|
||||
json: '{"ok": true}',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ json, expectedSeverity, expectedSafe, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanJSON(json)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssuePattern) {
|
||||
expect(result.issues.some(issue => issue.pattern.includes(expectedIssuePattern))).toBe(true)
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(json)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanHTML', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag script tags as critical',
|
||||
html: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'flag inline handlers as high',
|
||||
html: '<button onclick="alert(1)">Click</button>',
|
||||
expectedSeverity: 'high',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'return safe for plain markup',
|
||||
html: '<div><span>Safe</span></div>',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ html, expectedSeverity, expectedSafe }) => {
|
||||
const result = securityScanner.scanHTML(html)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeInput', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'remove script tags and inline handlers from text',
|
||||
input: '<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
|
||||
type: 'text' as const,
|
||||
shouldExclude: ['<script', 'onclick', 'javascript:'],
|
||||
},
|
||||
{
|
||||
name: 'remove data html URIs from html',
|
||||
input: '<img src="data:text/html;base64,abc"><script>alert(1)</script>',
|
||||
type: 'html' as const,
|
||||
shouldExclude: ['data:text/html', '<script'],
|
||||
},
|
||||
{
|
||||
name: 'neutralize prototype pollution in json',
|
||||
input: '{"__proto__": {"polluted": true}, "note": "constructor[\\"prototype\\"]"}',
|
||||
type: 'json' as const,
|
||||
shouldInclude: ['_proto_'],
|
||||
shouldExclude: ['__proto__', 'constructor["prototype"]'],
|
||||
},
|
||||
])('should $name', ({ input, type, shouldExclude = [], shouldInclude = [] }) => {
|
||||
const sanitized = securityScanner.sanitizeInput(input, type)
|
||||
shouldExclude.forEach(value => {
|
||||
expect(sanitized).not.toContain(value)
|
||||
})
|
||||
shouldInclude.forEach(value => {
|
||||
expect(sanitized).toContain(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanForVulnerabilities', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'auto-detects JSON and flags prototype pollution',
|
||||
code: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects Lua when function/end present',
|
||||
code: 'function dangerous() os.execute("rm -rf /") end',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects HTML and flags script tags',
|
||||
code: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'falls back to JavaScript scanning',
|
||||
code: 'const result = eval("1 + 1")',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'honors explicit type parameter',
|
||||
code: 'return 1',
|
||||
type: 'lua' as const,
|
||||
expectedSeverity: 'safe',
|
||||
},
|
||||
])('should $name', ({ code, type, expectedSeverity }) => {
|
||||
const result = scanForVulnerabilities(code, type)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
|
||||
|
||||
describe('security-scanner reporting', () => {
|
||||
describe('getSeverityColor', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: 'error' },
|
||||
{ severity: 'high', expected: 'warning' },
|
||||
{ severity: 'medium', expected: 'info' },
|
||||
{ severity: 'low', expected: 'secondary' },
|
||||
{ severity: 'safe', expected: 'success' },
|
||||
])('should map $severity to expected classes', ({ severity, expected }) => {
|
||||
expect(getSeverityColor(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSeverityIcon', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: '\u{1F6A8}' },
|
||||
{ severity: 'high', expected: '\u26A0\uFE0F' },
|
||||
{ severity: 'medium', expected: '\u26A1' },
|
||||
{ severity: 'low', expected: '\u2139\uFE0F' },
|
||||
{ severity: 'safe', expected: '\u2713' },
|
||||
])('should map $severity to expected icon', ({ severity, expected }) => {
|
||||
expect(getSeverityIcon(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,257 +1,2 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { securityScanner, scanForVulnerabilities, getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
|
||||
|
||||
describe('security-scanner', () => {
|
||||
describe('scanJavaScript', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag eval usage as critical',
|
||||
code: ['const safe = true;', 'const result = eval("1 + 1")'].join('\n'),
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'dangerous',
|
||||
expectedIssuePattern: 'eval',
|
||||
expectedLine: 2,
|
||||
},
|
||||
{
|
||||
name: 'warn on localStorage usage but stay safe',
|
||||
code: 'localStorage.setItem("k", "v")',
|
||||
expectedSeverity: 'low',
|
||||
expectedSafe: true,
|
||||
expectedIssueType: 'warning',
|
||||
expectedIssuePattern: 'localStorage',
|
||||
},
|
||||
{
|
||||
name: 'return safe for benign code',
|
||||
code: 'const sum = (a, b) => a + b',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])(
|
||||
'should $name',
|
||||
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
|
||||
const result = securityScanner.scanJavaScript(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
if (expectedLine !== undefined) {
|
||||
expect(issue?.line).toBe(expectedLine)
|
||||
}
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('scanLua', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag os.execute usage as critical',
|
||||
code: 'os.execute("rm -rf /")',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'malicious',
|
||||
expectedIssuePattern: 'os.execute',
|
||||
},
|
||||
{
|
||||
name: 'return safe for simple Lua function',
|
||||
code: 'function add(a, b) return a + b end',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanLua(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanJSON', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag invalid JSON as medium severity',
|
||||
json: '{"value": }',
|
||||
expectedSeverity: 'medium',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: 'JSON parse error',
|
||||
},
|
||||
{
|
||||
name: 'flag prototype pollution in JSON as critical',
|
||||
json: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: '__proto__',
|
||||
},
|
||||
{
|
||||
name: 'return safe for valid JSON',
|
||||
json: '{"ok": true}',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ json, expectedSeverity, expectedSafe, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanJSON(json)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssuePattern) {
|
||||
expect(result.issues.some(issue => issue.pattern.includes(expectedIssuePattern))).toBe(true)
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(json)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanHTML', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag script tags as critical',
|
||||
html: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'flag inline handlers as high',
|
||||
html: '<button onclick="alert(1)">Click</button>',
|
||||
expectedSeverity: 'high',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'return safe for plain markup',
|
||||
html: '<div><span>Safe</span></div>',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ html, expectedSeverity, expectedSafe }) => {
|
||||
const result = securityScanner.scanHTML(html)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeInput', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'remove script tags and inline handlers from text',
|
||||
input: '<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
|
||||
type: 'text' as const,
|
||||
shouldExclude: ['<script', 'onclick', 'javascript:'],
|
||||
},
|
||||
{
|
||||
name: 'remove data html URIs from html',
|
||||
input: '<img src="data:text/html;base64,abc"><script>alert(1)</script>',
|
||||
type: 'html' as const,
|
||||
shouldExclude: ['data:text/html', '<script'],
|
||||
},
|
||||
{
|
||||
name: 'neutralize prototype pollution in json',
|
||||
input: '{"__proto__": {"polluted": true}, "note": "constructor[\\"prototype\\"]"}',
|
||||
type: 'json' as const,
|
||||
shouldInclude: ['_proto_'],
|
||||
shouldExclude: ['__proto__', 'constructor["prototype"]'],
|
||||
},
|
||||
])('should $name', ({ input, type, shouldExclude = [], shouldInclude = [] }) => {
|
||||
const sanitized = securityScanner.sanitizeInput(input, type)
|
||||
shouldExclude.forEach(value => {
|
||||
expect(sanitized).not.toContain(value)
|
||||
})
|
||||
shouldInclude.forEach(value => {
|
||||
expect(sanitized).toContain(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSeverityColor', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: 'error' },
|
||||
{ severity: 'high', expected: 'warning' },
|
||||
{ severity: 'medium', expected: 'info' },
|
||||
{ severity: 'low', expected: 'secondary' },
|
||||
{ severity: 'safe', expected: 'success' },
|
||||
])('should map $severity to expected classes', ({ severity, expected }) => {
|
||||
expect(getSeverityColor(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSeverityIcon', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: '\u{1F6A8}' },
|
||||
{ severity: 'high', expected: '\u26A0\uFE0F' },
|
||||
{ severity: 'medium', expected: '\u26A1' },
|
||||
{ severity: 'low', expected: '\u2139\uFE0F' },
|
||||
{ severity: 'safe', expected: '\u2713' },
|
||||
])('should map $severity to expected icon', ({ severity, expected }) => {
|
||||
expect(getSeverityIcon(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanForVulnerabilities', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'auto-detects JSON and flags prototype pollution',
|
||||
code: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects Lua when function/end present',
|
||||
code: 'function dangerous() os.execute("rm -rf /") end',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects HTML and flags script tags',
|
||||
code: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'falls back to JavaScript scanning',
|
||||
code: 'const result = eval("1 + 1")',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'honors explicit type parameter',
|
||||
code: 'return 1',
|
||||
type: 'lua' as const,
|
||||
expectedSeverity: 'safe',
|
||||
},
|
||||
])('should $name', ({ code, type, expectedSeverity }) => {
|
||||
const result = scanForVulnerabilities(code, type)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
})
|
||||
})
|
||||
})
|
||||
import './__tests__/security-scanner.detection.test'
|
||||
import './__tests__/security-scanner.reporting.test'
|
||||
|
||||
71
frontends/nextjs/src/theme/types/components.d.ts
vendored
Normal file
71
frontends/nextjs/src/theme/types/components.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import '@mui/material/styles'
|
||||
import '@mui/material/Typography'
|
||||
import '@mui/material/Button'
|
||||
import '@mui/material/Chip'
|
||||
import '@mui/material/IconButton'
|
||||
import '@mui/material/Badge'
|
||||
import '@mui/material/Alert'
|
||||
|
||||
// Typography variants and component overrides
|
||||
declare module '@mui/material/styles' {
|
||||
interface TypographyVariants {
|
||||
code: React.CSSProperties
|
||||
kbd: React.CSSProperties
|
||||
label: React.CSSProperties
|
||||
}
|
||||
|
||||
interface TypographyVariantsOptions {
|
||||
code?: React.CSSProperties
|
||||
kbd?: React.CSSProperties
|
||||
label?: React.CSSProperties
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Typography' {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
code: true
|
||||
kbd: true
|
||||
label: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
soft: true
|
||||
ghost: true
|
||||
}
|
||||
|
||||
interface ButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Chip' {
|
||||
interface ChipPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
|
||||
interface ChipPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/IconButton' {
|
||||
interface IconButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Badge' {
|
||||
interface BadgePropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Alert' {
|
||||
interface AlertPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
70
frontends/nextjs/src/theme/types/layout.d.ts
vendored
Normal file
70
frontends/nextjs/src/theme/types/layout.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
import '@mui/material/styles'
|
||||
|
||||
// Custom theme properties for layout and design tokens
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme {
|
||||
custom: {
|
||||
fonts: {
|
||||
body: string
|
||||
heading: string
|
||||
mono: string
|
||||
}
|
||||
borderRadius: {
|
||||
none: number
|
||||
sm: number
|
||||
md: number
|
||||
lg: number
|
||||
xl: number
|
||||
full: number
|
||||
}
|
||||
contentWidth: {
|
||||
sm: string
|
||||
md: string
|
||||
lg: string
|
||||
xl: string
|
||||
full: string
|
||||
}
|
||||
sidebar: {
|
||||
width: number
|
||||
collapsedWidth: number
|
||||
}
|
||||
header: {
|
||||
height: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ThemeOptions {
|
||||
custom?: {
|
||||
fonts?: {
|
||||
body?: string
|
||||
heading?: string
|
||||
mono?: string
|
||||
}
|
||||
borderRadius?: {
|
||||
none?: number
|
||||
sm?: number
|
||||
md?: number
|
||||
lg?: number
|
||||
xl?: number
|
||||
full?: number
|
||||
}
|
||||
contentWidth?: {
|
||||
sm?: string
|
||||
md?: string
|
||||
lg?: string
|
||||
xl?: string
|
||||
full?: string
|
||||
}
|
||||
sidebar?: {
|
||||
width?: number
|
||||
collapsedWidth?: number
|
||||
}
|
||||
header?: {
|
||||
height?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
38
frontends/nextjs/src/theme/types/palette.d.ts
vendored
Normal file
38
frontends/nextjs/src/theme/types/palette.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import '@mui/material/styles'
|
||||
|
||||
// Extend palette with custom neutral colors
|
||||
declare module '@mui/material/styles' {
|
||||
interface Palette {
|
||||
neutral: {
|
||||
50: string
|
||||
100: string
|
||||
200: string
|
||||
300: string
|
||||
400: string
|
||||
500: string
|
||||
600: string
|
||||
700: string
|
||||
800: string
|
||||
900: string
|
||||
950: string
|
||||
}
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
neutral?: {
|
||||
50?: string
|
||||
100?: string
|
||||
200?: string
|
||||
300?: string
|
||||
400?: string
|
||||
500?: string
|
||||
600?: string
|
||||
700?: string
|
||||
800?: string
|
||||
900?: string
|
||||
950?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
202
frontends/nextjs/src/theme/types/theme.d.ts
vendored
202
frontends/nextjs/src/theme/types/theme.d.ts
vendored
@@ -1,200 +1,10 @@
|
||||
/**
|
||||
* MUI Theme Type Extensions
|
||||
*
|
||||
* This file extends Material-UI's theme interface with custom properties.
|
||||
* All custom design tokens and component variants should be declared here.
|
||||
*
|
||||
* This file aggregates the theme augmentation modules to keep the
|
||||
* main declaration lightweight while still exposing all custom tokens.
|
||||
*/
|
||||
|
||||
import '@mui/material/styles'
|
||||
import '@mui/material/Typography'
|
||||
import '@mui/material/Button'
|
||||
|
||||
// ============================================================================
|
||||
// Custom Palette Extensions
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
// Extend palette with custom neutral colors
|
||||
interface Palette {
|
||||
neutral: {
|
||||
50: string
|
||||
100: string
|
||||
200: string
|
||||
300: string
|
||||
400: string
|
||||
500: string
|
||||
600: string
|
||||
700: string
|
||||
800: string
|
||||
900: string
|
||||
950: string
|
||||
}
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
neutral?: {
|
||||
50?: string
|
||||
100?: string
|
||||
200?: string
|
||||
300?: string
|
||||
400?: string
|
||||
500?: string
|
||||
600?: string
|
||||
700?: string
|
||||
800?: string
|
||||
900?: string
|
||||
950?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Custom typography variants
|
||||
interface TypographyVariants {
|
||||
code: React.CSSProperties
|
||||
kbd: React.CSSProperties
|
||||
label: React.CSSProperties
|
||||
}
|
||||
|
||||
interface TypographyVariantsOptions {
|
||||
code?: React.CSSProperties
|
||||
kbd?: React.CSSProperties
|
||||
label?: React.CSSProperties
|
||||
}
|
||||
|
||||
// Custom theme properties
|
||||
interface Theme {
|
||||
custom: {
|
||||
fonts: {
|
||||
body: string
|
||||
heading: string
|
||||
mono: string
|
||||
}
|
||||
borderRadius: {
|
||||
none: number
|
||||
sm: number
|
||||
md: number
|
||||
lg: number
|
||||
xl: number
|
||||
full: number
|
||||
}
|
||||
contentWidth: {
|
||||
sm: string
|
||||
md: string
|
||||
lg: string
|
||||
xl: string
|
||||
full: string
|
||||
}
|
||||
sidebar: {
|
||||
width: number
|
||||
collapsedWidth: number
|
||||
}
|
||||
header: {
|
||||
height: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ThemeOptions {
|
||||
custom?: {
|
||||
fonts?: {
|
||||
body?: string
|
||||
heading?: string
|
||||
mono?: string
|
||||
}
|
||||
borderRadius?: {
|
||||
none?: number
|
||||
sm?: number
|
||||
md?: number
|
||||
lg?: number
|
||||
xl?: number
|
||||
full?: number
|
||||
}
|
||||
contentWidth?: {
|
||||
sm?: string
|
||||
md?: string
|
||||
lg?: string
|
||||
xl?: string
|
||||
full?: string
|
||||
}
|
||||
sidebar?: {
|
||||
width?: number
|
||||
collapsedWidth?: number
|
||||
}
|
||||
header?: {
|
||||
height?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Typography Variant Mapping
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Typography' {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
code: true
|
||||
kbd: true
|
||||
label: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Button Variants & Colors
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
soft: true
|
||||
ghost: true
|
||||
}
|
||||
|
||||
interface ButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chip Variants
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Chip' {
|
||||
interface ChipPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
|
||||
interface ChipPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IconButton Colors
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/IconButton' {
|
||||
interface IconButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Badge Colors
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Badge' {
|
||||
interface BadgePropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Alert Variants
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Alert' {
|
||||
interface AlertPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
export * from './palette'
|
||||
export * from './layout'
|
||||
export * from './components'
|
||||
|
||||
Reference in New Issue
Block a user