Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s

Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -0,0 +1,167 @@
/**
* AI Prompt Injection Guard
*
* Detects and strips common prompt injection patterns from user-supplied text
* before passing to AI services. Called before every AI service that uses
* user-supplied criteria, descriptions, or free-text fields.
*
* Patterns detected:
* - ChatML tags (<|im_start|>, <|im_end|>, <|endoftext|>)
* - Role impersonation ("system:", "assistant:")
* - Instruction override ("ignore previous instructions", "disregard above")
* - Encoded injection attempts (base64 encoded instructions, unicode tricks)
*/
// ─── Injection Patterns ─────────────────────────────────────────────────────
const CHATML_PATTERN = /<\|(?:im_start|im_end|endoftext|system|user|assistant)\|>/gi
const ROLE_IMPERSONATION_PATTERN =
/^\s*(?:system|assistant|user|human|ai|bot)\s*:/gim
const INSTRUCTION_OVERRIDE_PATTERNS = [
/ignore\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+instructions?/gi,
/disregard\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context)/gi,
/forget\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context)/gi,
/override\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?)/gi,
/you\s+are\s+now\s+(?:a|an)\s+/gi,
/new\s+instructions?\s*:/gi,
/begin\s+(?:new\s+)?(?:prompt|instructions?|session)/gi,
/\[INST\]/gi,
/\[\/INST\]/gi,
/<<SYS>>/gi,
/<\/SYS>>/gi,
]
const ENCODED_INJECTION_PATTERNS = [
// Base64 encoded common injection phrases
/aWdub3JlIHByZXZpb3Vz/gi, // "ignore previous" in base64
/ZGlzcmVnYXJkIGFib3Zl/gi, // "disregard above" in base64
]
// ─── Types ──────────────────────────────────────────────────────────────────
export type SanitizationResult = {
sanitized: string
wasModified: boolean
detectedPatterns: string[]
}
// ─── Core Functions ─────────────────────────────────────────────────────────
/**
* Sanitize user-supplied text by stripping injection patterns.
* Returns the sanitized text and metadata about what was detected/removed.
*/
export function sanitizeUserInput(text: string): SanitizationResult {
if (!text || typeof text !== 'string') {
return { sanitized: '', wasModified: false, detectedPatterns: [] }
}
let sanitized = text
const detectedPatterns: string[] = []
// Strip ChatML tags
if (CHATML_PATTERN.test(sanitized)) {
detectedPatterns.push('ChatML tags')
sanitized = sanitized.replace(CHATML_PATTERN, '')
}
// Strip role impersonation
if (ROLE_IMPERSONATION_PATTERN.test(sanitized)) {
detectedPatterns.push('Role impersonation')
sanitized = sanitized.replace(ROLE_IMPERSONATION_PATTERN, '')
}
// Strip instruction overrides
for (const pattern of INSTRUCTION_OVERRIDE_PATTERNS) {
// Reset lastIndex for global patterns
pattern.lastIndex = 0
if (pattern.test(sanitized)) {
detectedPatterns.push('Instruction override attempt')
pattern.lastIndex = 0
sanitized = sanitized.replace(pattern, '[FILTERED]')
}
}
// Strip encoded injections
for (const pattern of ENCODED_INJECTION_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(sanitized)) {
detectedPatterns.push('Encoded injection')
pattern.lastIndex = 0
sanitized = sanitized.replace(pattern, '[FILTERED]')
}
}
// Trim excessive whitespace left by removals
sanitized = sanitized.replace(/\n{3,}/g, '\n\n').trim()
const wasModified = sanitized !== text.trim()
if (wasModified && detectedPatterns.length > 0) {
console.warn(
`[PromptGuard] Detected injection patterns in user input: ${detectedPatterns.join(', ')}`
)
}
return { sanitized, wasModified, detectedPatterns }
}
/**
* Quick check: does the text contain any injection patterns?
* Faster than full sanitization when you only need a boolean check.
*/
export function containsInjectionPatterns(text: string): boolean {
if (!text) return false
if (CHATML_PATTERN.test(text)) return true
CHATML_PATTERN.lastIndex = 0
if (ROLE_IMPERSONATION_PATTERN.test(text)) return true
ROLE_IMPERSONATION_PATTERN.lastIndex = 0
for (const pattern of INSTRUCTION_OVERRIDE_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(text)) return true
}
for (const pattern of ENCODED_INJECTION_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(text)) return true
}
return false
}
/**
* Sanitize all string values in a criteria/config object.
* Recursively processes nested objects and arrays.
*/
export function sanitizeCriteriaObject(
obj: Record<string, unknown>
): { sanitized: Record<string, unknown>; detectedPatterns: string[] } {
const allDetected: string[] = []
function processValue(value: unknown): unknown {
if (typeof value === 'string') {
const result = sanitizeUserInput(value)
allDetected.push(...result.detectedPatterns)
return result.sanitized
}
if (Array.isArray(value)) {
return value.map(processValue)
}
if (value && typeof value === 'object') {
const processed: Record<string, unknown> = {}
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
processed[k] = processValue(v)
}
return processed
}
return value
}
const sanitized = processValue(obj) as Record<string, unknown>
return { sanitized, detectedPatterns: [...new Set(allDetected)] }
}