From 05bd714fff6d18348a12760da846c414f21ac65e Mon Sep 17 00:00:00 2001 From: tr1cks Date: Sat, 20 Sep 2025 18:12:02 +0200 Subject: [PATCH 1/3] feat: add goal meta support with size validation - Add meta Json? field to Goal model in Prisma schema - Create migration with 4096 character constraint using JSONB - Add GoalValidation utility for application-level validation - Integrate meta validation into Goals API endpoint - Update Goal.json schema to include meta field - Support auto-tracking keys and flexible metadata storage --- .../migration.sql | 6 +++ api/prisma/schema.prisma | 1 + api/src/routes/goals/Goals.ts | 15 +++++- api/src/util/GoalValidation.ts | 50 +++++++++++++++++++ schema/schemas/Goal.json | 8 ++- 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 api/prisma/migrations/20250920152804_add_goal_meta_field/migration.sql create mode 100644 api/src/util/GoalValidation.ts diff --git a/api/prisma/migrations/20250920152804_add_goal_meta_field/migration.sql b/api/prisma/migrations/20250920152804_add_goal_meta_field/migration.sql new file mode 100644 index 00000000..38619190 --- /dev/null +++ b/api/prisma/migrations/20250920152804_add_goal_meta_field/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Goal" ADD COLUMN "meta" JSONB; + +-- Add constraint to limit meta JSON size to 1024 characters +ALTER TABLE "Goal" ADD CONSTRAINT "Goal_meta_size_check" +CHECK ("meta" IS NULL OR LENGTH("meta"::text) <= 4096); diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 9ddf7451..9f95543d 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -17,6 +17,7 @@ model Goal { oldCategories String[] categories Category[] @relation("GoalCategories") difficulty Int? + meta Json? game Game @relation(fields: [gameId], references: [id]) variants GoalVariant[] createdAt DateTime @default(now()) diff --git a/api/src/routes/goals/Goals.ts b/api/src/routes/goals/Goals.ts index d62d802d..36ee191e 100644 --- a/api/src/routes/goals/Goals.ts +++ b/api/src/routes/goals/Goals.ts @@ -4,6 +4,7 @@ import { deleteGoal, editGoal, gameForGoal } from '../../database/games/Goals'; import upload from './Upload'; import goalCategories from './GoalCategories'; import { Prisma } from '@prisma/client'; +import { validateGoalMeta } from '../../util/GoalValidation'; const goals = Router(); @@ -31,16 +32,26 @@ goals.post('/:id', async (req, res) => { } const { id } = req.params; - const { goal, description, categories, difficulty } = req.body; + const { goal, description, categories, difficulty, meta } = req.body; - if (!goal && description === undefined && !categories && !difficulty) { + if (!goal && description === undefined && !categories && !difficulty && meta === undefined) { res.status(400).send('No changes submitted'); return; } + // Validate meta data if provided + if (meta !== undefined) { + const metaValidation = validateGoalMeta(meta); + if (!metaValidation.valid) { + res.status(400).json({ error: metaValidation.error }); + return; + } + } + const input: Prisma.GoalUpdateInput = { goal, description, + meta, }; if (difficulty === 0) { diff --git a/api/src/util/GoalValidation.ts b/api/src/util/GoalValidation.ts new file mode 100644 index 00000000..d5da2a8b --- /dev/null +++ b/api/src/util/GoalValidation.ts @@ -0,0 +1,50 @@ +/** + * Validation utilities for Goal meta data + */ + +// Maximum size for goal meta JSON in characters +const GOAL_META_MAX_SIZE = 4096; + +export interface GoalMetaValidationResult { + valid: boolean; + error?: string; +} + +/** + * Validates goal meta data against size constraints + * @param meta - The meta data to validate (can be any JSON-serializable value) + * @returns Validation result with success status and optional error message + */ +export function validateGoalMeta(meta: any): GoalMetaValidationResult { + // Allow null/undefined values + if (meta === null || meta === undefined) { + return { valid: true }; + } + + try { + // Convert to JSON string to check size + const metaString = JSON.stringify(meta); // also checks for invalid JSON + + if (metaString.length > GOAL_META_MAX_SIZE) { + return { + valid: false, + error: `Goal meta exceeds maximum size of ${GOAL_META_MAX_SIZE} characters (got ${metaString.length})` + }; + } + + return { valid: true }; + } catch (error) { + return { + valid: false, + error: `Invalid JSON data for goal meta: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } +} + +/** + * Gets the maximum allowed size for goal meta data + * @returns Maximum size in characters + */ +export function getGoalMetaMaxSize(): number { + return GOAL_META_MAX_SIZE; +} diff --git a/schema/schemas/Goal.json b/schema/schemas/Goal.json index 3f9a2033..234f2706 100644 --- a/schema/schemas/Goal.json +++ b/schema/schemas/Goal.json @@ -9,6 +9,12 @@ "goal": {"type": "string"}, "description": {"type": ["string", "null"]}, "difficulty": {"type": ["number", "null"]}, - "categories": {"type": "array", "items": {"type": "string"}} + "categories": {"type": "array", "items": {"type": "string"}}, + "meta": { + "type": ["object", "null"], + "description": "Optional metadata for goal auto-tracking and additional properties", + "maxProperties": 20, + "additionalProperties": true + } } } \ No newline at end of file From 6c693b007d89ed7c17a1e5b906a2e39d46d18436 Mon Sep 17 00:00:00 2001 From: tr1cks Date: Wed, 24 Sep 2025 10:37:18 +0200 Subject: [PATCH 2/3] feat: add comprehensive nested property validation for goal meta --- api/src/util/GoalValidation.ts | 142 +++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 5 deletions(-) diff --git a/api/src/util/GoalValidation.ts b/api/src/util/GoalValidation.ts index d5da2a8b..4f148fe4 100644 --- a/api/src/util/GoalValidation.ts +++ b/api/src/util/GoalValidation.ts @@ -1,17 +1,100 @@ /** - * Validation utilities for Goal meta data + * Validation utilities for Goal meta data. + * + * This module provides comprehensive validation for goal metadata to prevent abuse + * through oversized payloads, excessive property counts, and deep nesting attacks. + * It enforces three key constraints: + * - Maximum JSON string size (4096 characters) + * - Maximum total properties across all nesting levels (20) + * - Maximum nesting depth (5 levels) + * + * The validation is designed to prevent common attack vectors while allowing + * reasonable flexibility for legitimate goal metadata use cases. */ -// Maximum size for goal meta JSON in characters +/** + * Maximum size for goal meta JSON in characters. + * Prevents memory exhaustion and DoS attacks through oversized payloads. + * 4096 characters is sufficient for reasonable metadata while preventing abuse. + */ const GOAL_META_MAX_SIZE = 4096; +/** + * Maximum total properties across all nesting levels in goal meta. + * Prevents abuse through nested objects that bypass flat property limits. + * 20 properties is reasonable for goal metadata while preventing spam/abuse. + */ +const GOAL_META_MAX_TOTAL_PROPERTIES = 20; + +/** + * Maximum nesting depth for goal meta objects. + * Prevents deep recursion attacks and excessive nesting that could cause stack overflow. + * 5 levels deep is sufficient for reasonable nested metadata structures. + */ +const GOAL_META_MAX_DEPTH = 5; + +/** + * Result of goal meta validation containing success status and optional error message. + */ export interface GoalMetaValidationResult { valid: boolean; error?: string; } /** - * Validates goal meta data against size constraints + * Analysis result of nested object structure containing property count and depth information. + */ +export interface NestedPropertyCount { + totalProperties: number; + maxDepth: number; +} + +/** + * Recursively counts all properties in nested objects and arrays. + * This function traverses the entire object structure to count properties at all nesting levels, + * which is essential for preventing abuse through nested object structures. + * + * @param obj - The object to analyze (can be any JSON-serializable value) + * @param currentDepth - Current nesting depth (starts at 0 for root level) + * @returns Object containing total property count and maximum depth reached + */ +function countNestedProperties(obj: any, currentDepth: number = 0): NestedPropertyCount { + if (obj === null || obj === undefined || typeof obj !== 'object') { + return { totalProperties: 0, maxDepth: currentDepth }; + } + + let totalProperties = 0; + let maxDepth = currentDepth; + + if (Array.isArray(obj)) { + // For arrays, count each element + for (const item of obj) { + const itemCount = countNestedProperties(item, currentDepth + 1); + totalProperties += itemCount.totalProperties; + maxDepth = Math.max(maxDepth, itemCount.maxDepth); + } + } else { + // For objects, count each property + for (const [key, value] of Object.entries(obj)) { + totalProperties++; // Count the property itself + + // Recursively count nested properties + const nestedCount = countNestedProperties(value, currentDepth + 1); + totalProperties += nestedCount.totalProperties; + maxDepth = Math.max(maxDepth, nestedCount.maxDepth); + } + } + + return { totalProperties, maxDepth }; +} + +/** + * Validates goal meta data against size and structure constraints. + * This function enforces three key security and performance limits: + * 1. JSON string size limit to prevent memory exhaustion + * 2. Total property count limit to prevent abuse through nested objects + * 3. Maximum nesting depth to prevent recursion attacks + * * @param meta - The meta data to validate (can be any JSON-serializable value) * @returns Validation result with success status and optional error message */ @@ -32,6 +115,25 @@ export function validateGoalMeta(meta: any): GoalMetaValidationResult { }; } + // Count nested properties and depth + const { totalProperties, maxDepth } = countNestedProperties(meta); + + // Check total property count + if (totalProperties > GOAL_META_MAX_TOTAL_PROPERTIES) { + return { + valid: false, + error: `Goal meta exceeds maximum total properties of ${GOAL_META_MAX_TOTAL_PROPERTIES} (got ${totalProperties})` + }; + } + + // Check maximum depth + if (maxDepth > GOAL_META_MAX_DEPTH) { + return { + valid: false, + error: `Goal meta exceeds maximum nesting depth of ${GOAL_META_MAX_DEPTH} (got ${maxDepth})` + }; + } + return { valid: true }; } catch (error) { return { @@ -42,9 +144,39 @@ export function validateGoalMeta(meta: any): GoalMetaValidationResult { } /** - * Gets the maximum allowed size for goal meta data - * @returns Maximum size in characters + * Gets the maximum allowed size for goal meta data. + * @returns Maximum size in characters (4096) */ export function getGoalMetaMaxSize(): number { return GOAL_META_MAX_SIZE; } + +/** + * Gets the maximum allowed total properties for goal meta data. + * @returns Maximum total properties across all nesting levels (20) + */ +export function getGoalMetaMaxTotalProperties(): number { + return GOAL_META_MAX_TOTAL_PROPERTIES; +} + +/** + * Gets the maximum allowed nesting depth for goal meta data. + * @returns Maximum nesting depth (5) + */ +export function getGoalMetaMaxDepth(): number { + return GOAL_META_MAX_DEPTH; +} + +/** + * Analyzes goal meta data structure without performing validation. + * Useful for debugging, logging, or understanding the structure of meta data. + * + * @param meta - The meta data to analyze (can be any JSON-serializable value) + * @returns Analysis result with property count and depth information + */ +export function analyzeGoalMeta(meta: any): NestedPropertyCount { + if (meta === null || meta === undefined) { + return { totalProperties: 0, maxDepth: 0 }; + } + return countNestedProperties(meta); +} From 107e92c951e32477bccb4f61e0073cb2056d699e Mon Sep 17 00:00:00 2001 From: tr1cks Date: Wed, 24 Sep 2025 17:34:36 +0200 Subject: [PATCH 3/3] dangerous key protection, max_bytes protection --- api/src/tests/GoalValidation.test.ts | 189 +++++++++++++++++++++++++++ api/src/util/GoalValidation.ts | 181 ++++++------------------- schema/schemas/Goal.json | 1 - 3 files changed, 229 insertions(+), 142 deletions(-) create mode 100644 api/src/tests/GoalValidation.test.ts diff --git a/api/src/tests/GoalValidation.test.ts b/api/src/tests/GoalValidation.test.ts new file mode 100644 index 00000000..9b1c13e8 --- /dev/null +++ b/api/src/tests/GoalValidation.test.ts @@ -0,0 +1,189 @@ +import { validateGoalMeta } from '../util/GoalValidation'; + +describe('GoalValidation', () => { + describe('validateGoalMeta', () => { + it('should allow null and undefined values', () => { + expect(validateGoalMeta(null)).toEqual({ valid: true }); + expect(validateGoalMeta(undefined as any)).toEqual({ valid: true }); + }); + + it('should allow simple valid objects', () => { + const validMeta = { + type: 'auto-track', + config: { enabled: true }, + tags: ['important', 'urgent'] + }; + expect(validateGoalMeta(validMeta)).toEqual({ valid: true }); + }); + + it('should reject objects that exceed byte size limit', () => { + // Create a large string that exceeds 4096 bytes + const largeString = 'x'.repeat(5000); + const largeMeta = { data: largeString }; + + const result = validateGoalMeta(largeMeta); + expect(result.valid).toBe(false); + expect(result.error).toContain('exceeds maximum size of 4096 bytes'); + }); + + it('should allow deeply nested objects under byte limit', () => { + // Create a deeply nested object that's still under byte limit + function makeDeepObject(depth: number) { + let obj: any = {}; + let cur = obj; + for (let i = 0; i < depth; i++) { + cur.a = {}; + cur = cur.a; + } + return obj; + } + + const deepObject = makeDeepObject(10); // Deep but small + const result = validateGoalMeta(deepObject); + + expect(result.valid).toBe(true); + }); + + it('should reject objects with dangerous prototype pollution keys', () => { + // Create object with dangerous key using Object.defineProperty to bypass JS filtering + const dangerousMeta: any = { normal: 'value' }; + Object.defineProperty(dangerousMeta, '__proto__', { + value: { isAdmin: true }, + enumerable: true, + configurable: true + }); + + const result = validateGoalMeta(dangerousMeta); + expect(result.valid).toBe(false); + expect(result.error).toContain('contains forbidden key "__proto__"'); + }); + + it('should reject objects with prototype key', () => { + const dangerousMeta = { + prototype: { isAdmin: true }, + normal: 'value' + }; + + const result = validateGoalMeta(dangerousMeta); + expect(result.valid).toBe(false); + expect(result.error).toContain('contains forbidden key "prototype"'); + }); + + it('should reject objects with constructor key', () => { + const dangerousMeta = { + constructor: { isAdmin: true }, + normal: 'value' + }; + + const result = validateGoalMeta(dangerousMeta); + expect(result.valid).toBe(false); + expect(result.error).toContain('contains forbidden key "constructor"'); + }); + + it('should reject objects with toString key', () => { + const dangerousMeta = { + toString: 'hacked', + normal: 'value' + }; + + const result = validateGoalMeta(dangerousMeta as any); + expect(result.valid).toBe(false); + expect(result.error).toContain('contains forbidden key "toString"'); + }); + + it('should reject objects with valueOf key', () => { + const dangerousMeta = { + valueOf: 'hacked', + normal: 'value' + }; + + const result = validateGoalMeta(dangerousMeta as any); + expect(result.valid).toBe(false); + expect(result.error).toContain('contains forbidden key "valueOf"'); + }); + + it('should handle arrays correctly', () => { + const arrayMeta = { + items: [1, 2, 3, { nested: true }], + count: 4 + }; + + expect(validateGoalMeta(arrayMeta)).toEqual({ valid: true }); + }); + + it('should handle deeply nested arrays', () => { + const deepArray = [[[[[['deep']]]]]]; // 6 levels deep + const result = validateGoalMeta(deepArray); + + expect(result.valid).toBe(true); // No depth limit, only byte limit + }); + + it('should reject non-JSON-serializable data', () => { + // Create truly non-serializable data with circular reference + const nonSerializable: any = { + normal: 'value' + }; + nonSerializable.self = nonSerializable; // Circular reference + + const result = validateGoalMeta(nonSerializable); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid JSON data for goal meta'); + }); + + it('should handle circular references gracefully', () => { + const circular: any = { name: 'test' }; + circular.self = circular; + + const result = validateGoalMeta(circular); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid JSON data for goal meta'); + }); + }); + + // Removed analyzeGoalMeta tests - function no longer exists + + describe('Depth Bomb Attack Simulation', () => { + it('should handle depth bomb with recursive traversal (safe for 4KB payloads)', () => { + // Create a depth bomb that would crash recursive implementations + function makeDepthBomb(depth: number) { + let obj: any = {}; + let cur = obj; + for (let i = 0; i < depth; i++) { + cur.a = {}; + cur = cur.a; + } + return obj; + } + + // Test with a depth that would cause stack overflow in recursive implementations + const depthBomb = makeDepthBomb(500); // Deep but under byte limit + const json = JSON.stringify(depthBomb); + + // Verify it's under byte limit + expect(Buffer.byteLength(json, 'utf8')).toBeLessThan(4096); + + // Our recursive implementation should handle this gracefully for 4KB payloads + const result = validateGoalMeta(depthBomb); + expect(result.valid).toBe(true); // Valid because under byte limit + }); + + it('should handle wide but shallow objects', () => { + // Create an object with many properties but shallow depth + const wideObject: any = {}; + for (let i = 0; i < 100; i++) { + wideObject[`prop${i}`] = `value${i}`; + } + + // Should be valid (no property count limit, shallow depth) + expect(validateGoalMeta(wideObject)).toEqual({ valid: true }); + }); + + it('should handle large arrays', () => { + // Create a large array (no array length limit) but keep it under byte limit + const largeArray = new Array(100).fill(0).map((_, i) => ({ id: i, value: `item${i}` })); + + // Should be valid (no array length limit) + expect(validateGoalMeta(largeArray)).toEqual({ valid: true }); + }); + }); +}); diff --git a/api/src/util/GoalValidation.ts b/api/src/util/GoalValidation.ts index 4f148fe4..ecb741b2 100644 --- a/api/src/util/GoalValidation.ts +++ b/api/src/util/GoalValidation.ts @@ -1,182 +1,81 @@ -/** - * Validation utilities for Goal meta data. - * - * This module provides comprehensive validation for goal metadata to prevent abuse - * through oversized payloads, excessive property counts, and deep nesting attacks. - * It enforces three key constraints: - * - Maximum JSON string size (4096 characters) - * - Maximum total properties across all nesting levels (20) - * - Maximum nesting depth (5 levels) - * - * The validation is designed to prevent common attack vectors while allowing - * reasonable flexibility for legitimate goal metadata use cases. - */ +import { Prisma } from '@prisma/client'; -/** - * Maximum size for goal meta JSON in characters. - * Prevents memory exhaustion and DoS attacks through oversized payloads. - * 4096 characters is sufficient for reasonable metadata while preventing abuse. - */ -const GOAL_META_MAX_SIZE = 4096; +const MAX_BYTES = 4096; // measure bytes, not string length -/** - * Maximum total properties across all nesting levels in goal meta. - * Prevents abuse through nested objects that bypass flat property limits. - * 20 properties is reasonable for goal metadata while preventing spam/abuse. - */ -const GOAL_META_MAX_TOTAL_PROPERTIES = 20; - -/** - * Maximum nesting depth for goal meta objects. - * Prevents deep recursion attacks and excessive nesting that could cause stack overflow. - * 5 levels deep is sufficient for reasonable nested metadata structures. - */ -const GOAL_META_MAX_DEPTH = 5; - -/** - * Result of goal meta validation containing success status and optional error message. - */ export interface GoalMetaValidationResult { valid: boolean; error?: string; } -/** - * Analysis result of nested object structure containing property count and depth information. - */ -export interface NestedPropertyCount { - totalProperties: number; - maxDepth: number; -} +const DANGEROUS_KEYS = new Set([ + '__proto__', // Direct prototype access + 'prototype', // Constructor prototype + 'constructor', // Constructor function + 'toString', // Could override object stringification + 'valueOf' // Could override object value conversion +]); /** - * Recursively counts all properties in nested objects and arrays. - * This function traverses the entire object structure to count properties at all nesting levels, - * which is essential for preventing abuse through nested object structures. - * - * @param obj - The object to analyze (can be any JSON-serializable value) - * @param currentDepth - Current nesting depth (starts at 0 for root level) - * @returns Object containing total property count and maximum depth reached + * Check for dangerous keys in the meta object. + * Simple recursive check - safe for 4KB payloads. */ -function countNestedProperties(obj: any, currentDepth: number = 0): NestedPropertyCount { - if (obj === null || obj === undefined || typeof obj !== 'object') { - return { totalProperties: 0, maxDepth: currentDepth }; +function checkDangerousKeys(meta: Prisma.JsonValue): string | null { + if (meta === null || meta === undefined || typeof meta !== 'object') { + return null; } - let totalProperties = 0; - let maxDepth = currentDepth; - - if (Array.isArray(obj)) { - // For arrays, count each element - for (const item of obj) { - const itemCount = countNestedProperties(item, currentDepth + 1); - totalProperties += itemCount.totalProperties; - maxDepth = Math.max(maxDepth, itemCount.maxDepth); + if (Array.isArray(meta)) { + // Check array elements + for (const item of meta) { + const result = checkDangerousKeys(item); + if (result) return result; } } else { - // For objects, count each property - for (const [key, value] of Object.entries(obj)) { - totalProperties++; // Count the property itself - - // Recursively count nested properties - const nestedCount = countNestedProperties(value, currentDepth + 1); - totalProperties += nestedCount.totalProperties; - maxDepth = Math.max(maxDepth, nestedCount.maxDepth); + // Check object keys + for (const [key, value] of Object.entries(meta) as [string, Prisma.JsonValue][]) { + if (DANGEROUS_KEYS.has(key)) { + return `Goal meta contains forbidden key "${key}"`; + } + // Recursively check nested values + const result = checkDangerousKeys(value); + if (result) return result; } } - return { totalProperties, maxDepth }; + return null; } /** - * Validates goal meta data against size and structure constraints. - * This function enforces three key security and performance limits: - * 1. JSON string size limit to prevent memory exhaustion - * 2. Total property count limit to prevent abuse through nested objects - * 3. Maximum nesting depth to prevent recursion attacks - * - * @param meta - The meta data to validate (can be any JSON-serializable value) - * @returns Validation result with success status and optional error message + * Validate meta with all guards and early exits. */ -export function validateGoalMeta(meta: any): GoalMetaValidationResult { +export function validateGoalMeta(meta: Prisma.JsonValue): GoalMetaValidationResult { // Allow null/undefined values if (meta === null || meta === undefined) { return { valid: true }; } try { - // Convert to JSON string to check size - const metaString = JSON.stringify(meta); // also checks for invalid JSON - - if (metaString.length > GOAL_META_MAX_SIZE) { - return { - valid: false, - error: `Goal meta exceeds maximum size of ${GOAL_META_MAX_SIZE} characters (got ${metaString.length})` - }; - } + // Convert to JSON and measure bytes (UTF-8) + const json = JSON.stringify(meta); // also ensures it's JSON-serializable + const bytes = Buffer.byteLength(json, 'utf8'); - // Count nested properties and depth - const { totalProperties, maxDepth } = countNestedProperties(meta); - - // Check total property count - if (totalProperties > GOAL_META_MAX_TOTAL_PROPERTIES) { + if (bytes > MAX_BYTES) { return { valid: false, - error: `Goal meta exceeds maximum total properties of ${GOAL_META_MAX_TOTAL_PROPERTIES} (got ${totalProperties})` + error: `Goal meta exceeds maximum size of ${MAX_BYTES} bytes (got ${bytes})`, }; } - // Check maximum depth - if (maxDepth > GOAL_META_MAX_DEPTH) { - return { - valid: false, - error: `Goal meta exceeds maximum nesting depth of ${GOAL_META_MAX_DEPTH} (got ${maxDepth})` - }; + const dangerousKeyError = checkDangerousKeys(meta); + if (dangerousKeyError) { + return { valid: false, error: dangerousKeyError }; } return { valid: true }; } catch (error) { return { valid: false, - error: `Invalid JSON data for goal meta: ${error instanceof Error ? error.message : 'Unknown error'}` + error: `Invalid JSON data for goal meta: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } -} - -/** - * Gets the maximum allowed size for goal meta data. - * @returns Maximum size in characters (4096) - */ -export function getGoalMetaMaxSize(): number { - return GOAL_META_MAX_SIZE; -} - -/** - * Gets the maximum allowed total properties for goal meta data. - * @returns Maximum total properties across all nesting levels (20) - */ -export function getGoalMetaMaxTotalProperties(): number { - return GOAL_META_MAX_TOTAL_PROPERTIES; -} - -/** - * Gets the maximum allowed nesting depth for goal meta data. - * @returns Maximum nesting depth (5) - */ -export function getGoalMetaMaxDepth(): number { - return GOAL_META_MAX_DEPTH; -} - -/** - * Analyzes goal meta data structure without performing validation. - * Useful for debugging, logging, or understanding the structure of meta data. - * - * @param meta - The meta data to analyze (can be any JSON-serializable value) - * @returns Analysis result with property count and depth information - */ -export function analyzeGoalMeta(meta: any): NestedPropertyCount { - if (meta === null || meta === undefined) { - return { totalProperties: 0, maxDepth: 0 }; - } - return countNestedProperties(meta); -} +} \ No newline at end of file diff --git a/schema/schemas/Goal.json b/schema/schemas/Goal.json index 234f2706..8ade8419 100644 --- a/schema/schemas/Goal.json +++ b/schema/schemas/Goal.json @@ -13,7 +13,6 @@ "meta": { "type": ["object", "null"], "description": "Optional metadata for goal auto-tracking and additional properties", - "maxProperties": 20, "additionalProperties": true } }