@character-foundry/character-foundry 0.4.3-dev.1766103111 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charx.cjs +18 -72
- package/dist/charx.cjs.map +1 -1
- package/dist/charx.d.cts +22 -22
- package/dist/charx.d.ts +22 -22
- package/dist/charx.js +18 -72
- package/dist/charx.js.map +1 -1
- package/dist/exporter.cjs +18 -72
- package/dist/exporter.cjs.map +1 -1
- package/dist/exporter.d.cts +19 -19
- package/dist/exporter.d.ts +19 -19
- package/dist/exporter.js +18 -72
- package/dist/exporter.js.map +1 -1
- package/dist/federation.d.cts +19 -19
- package/dist/federation.d.ts +19 -19
- package/dist/index.cjs +18 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -29
- package/dist/index.d.ts +29 -29
- package/dist/index.js +18 -72
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +18 -72
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.d.cts +23 -23
- package/dist/loader.d.ts +23 -23
- package/dist/loader.js +18 -72
- package/dist/loader.js.map +1 -1
- package/dist/lorebook.d.cts +23 -23
- package/dist/lorebook.d.ts +23 -23
- package/dist/normalizer.cjs +18 -72
- package/dist/normalizer.cjs.map +1 -1
- package/dist/normalizer.d.cts +37 -37
- package/dist/normalizer.d.ts +37 -37
- package/dist/normalizer.js +18 -72
- package/dist/normalizer.js.map +1 -1
- package/dist/png.cjs +18 -72
- package/dist/png.cjs.map +1 -1
- package/dist/png.d.cts +25 -25
- package/dist/png.d.ts +25 -25
- package/dist/png.js +18 -72
- package/dist/png.js.map +1 -1
- package/dist/schemas.cjs +18 -75
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +67 -85
- package/dist/schemas.d.ts +67 -85
- package/dist/schemas.js +18 -75
- package/dist/schemas.js.map +1 -1
- package/dist/voxta.cjs +18 -72
- package/dist/voxta.cjs.map +1 -1
- package/dist/voxta.d.cts +23 -23
- package/dist/voxta.d.ts +23 -23
- package/dist/voxta.js +18 -72
- package/dist/voxta.js.map +1 -1
- package/package.json +4 -4
package/dist/charx.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../core/src/binary.ts","../../core/src/base64.ts","../../core/src/errors.ts","../../core/src/data-url.ts","../../core/src/uri.ts","../../core/src/image.ts","../../core/src/uuid.ts","../../core/src/binary.ts","../../core/src/zip.ts","../../schemas/src/common.ts","../../schemas/src/ccv2.ts","../../schemas/src/ccv3.ts","../../schemas/src/risu.ts","../../schemas/src/normalized.ts","../../schemas/src/feature-deriver.ts","../../schemas/src/detection.ts","../../schemas/src/normalizer.ts","../../schemas/src/validation.ts","../../charx/src/reader.ts","../../charx/src/writer.ts"],"sourcesContent":["/**\n * Binary Data Utilities\n *\n * Universal binary data operations using Uint8Array.\n * Works in both Node.js and browser environments.\n */\n\n/**\n * Universal binary data type (works in both environments)\n */\nexport type BinaryData = Uint8Array;\n\n/**\n * Read a 32-bit big-endian unsigned integer\n */\nexport function readUInt32BE(data: BinaryData, offset: number): number {\n return (\n (data[offset]! << 24) |\n (data[offset + 1]! << 16) |\n (data[offset + 2]! << 8) |\n data[offset + 3]!\n ) >>> 0;\n}\n\n/**\n * Write a 32-bit big-endian unsigned integer\n */\nexport function writeUInt32BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 24) & 0xff;\n data[offset + 1] = (value >>> 16) & 0xff;\n data[offset + 2] = (value >>> 8) & 0xff;\n data[offset + 3] = value & 0xff;\n}\n\n/**\n * Read a 16-bit big-endian unsigned integer\n */\nexport function readUInt16BE(data: BinaryData, offset: number): number {\n return ((data[offset]! << 8) | data[offset + 1]!) >>> 0;\n}\n\n/**\n * Write a 16-bit big-endian unsigned integer\n */\nexport function writeUInt16BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 8) & 0xff;\n data[offset + 1] = value & 0xff;\n}\n\n/**\n * Find a byte sequence in binary data\n */\nexport function indexOf(data: BinaryData, search: BinaryData, fromIndex = 0): number {\n outer: for (let i = fromIndex; i <= data.length - search.length; i++) {\n for (let j = 0; j < search.length; j++) {\n if (data[i + j] !== search[j]) continue outer;\n }\n return i;\n }\n return -1;\n}\n\n/**\n * Concatenate multiple binary arrays\n */\nexport function concat(...arrays: BinaryData[]): BinaryData {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * Slice binary data (returns a view, not a copy)\n */\nexport function slice(data: BinaryData, start: number, end?: number): BinaryData {\n return data.subarray(start, end);\n}\n\n/**\n * Copy a portion of binary data (returns a new array)\n */\nexport function copy(data: BinaryData, start: number, end?: number): BinaryData {\n return data.slice(start, end);\n}\n\n/**\n * Convert string to binary (UTF-8)\n */\nexport function fromString(str: string): BinaryData {\n return new TextEncoder().encode(str);\n}\n\n/**\n * Convert binary to string (UTF-8)\n */\nexport function toString(data: BinaryData): string {\n return new TextDecoder().decode(data);\n}\n\n/**\n * Convert string to binary (Latin1 - for PNG keywords and similar)\n */\nexport function fromLatin1(str: string): BinaryData {\n const result = new Uint8Array(str.length);\n for (let i = 0; i < str.length; i++) {\n result[i] = str.charCodeAt(i) & 0xff;\n }\n return result;\n}\n\n/**\n * Convert binary to string (Latin1)\n */\nexport function toLatin1(data: BinaryData): string {\n let result = '';\n for (let i = 0; i < data.length; i++) {\n result += String.fromCharCode(data[i]!);\n }\n return result;\n}\n\n/**\n * Compare two binary arrays for equality\n */\nexport function equals(a: BinaryData, b: BinaryData): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Create a new Uint8Array filled with zeros\n */\nexport function alloc(size: number): BinaryData {\n return new Uint8Array(size);\n}\n\n/**\n * Create a Uint8Array from an array of numbers\n */\nexport function from(data: number[] | ArrayBuffer | BinaryData): BinaryData {\n if (data instanceof Uint8Array) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n return new Uint8Array(data);\n}\n\n/**\n * Check if value is a Uint8Array\n */\nexport function isBinaryData(value: unknown): value is BinaryData {\n return value instanceof Uint8Array;\n}\n\n/**\n * Convert Node.js Buffer to Uint8Array (no-op if already Uint8Array)\n * This provides compatibility when interfacing with Node.js code\n */\nexport function toUint8Array(data: BinaryData | Buffer): BinaryData {\n if (data instanceof Uint8Array) {\n // Buffer extends Uint8Array, but we want a plain Uint8Array\n // This ensures we get a proper Uint8Array in all cases\n if (Object.getPrototypeOf(data).constructor.name === 'Buffer') {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n return data;\n }\n return new Uint8Array(data);\n}\n\n/**\n * Convert binary data to hex string\n */\nexport function toHex(data: BinaryData): string {\n return Array.from(data)\n .map(b => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Convert hex string to binary data\n */\nexport function fromHex(hex: string): BinaryData {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n }\n return bytes;\n}\n","/**\n * Universal Base64 Encoding/Decoding\n *\n * Works in both Node.js and browser environments.\n */\n\nimport type { BinaryData } from './binary.js';\n\n/**\n * Check if we're in a Node.js environment\n */\nconst isNode = typeof process !== 'undefined' &&\n process.versions != null &&\n process.versions.node != null;\n\n/**\n * Threshold for switching to chunked encoding in browsers (1MB)\n * Below this, simple string concatenation is fast enough.\n * Above this, quadratic string growth becomes a problem.\n */\nconst LARGE_BUFFER_THRESHOLD = 1024 * 1024;\n\n/**\n * Encode binary data to base64 string\n *\n * PERFORMANCE: For large buffers (>1MB) in browsers, this automatically\n * uses the chunked implementation to avoid quadratic string concatenation.\n */\nexport function encode(data: BinaryData): string {\n if (isNode) {\n // Node.js: Buffer handles large data efficiently\n return Buffer.from(data).toString('base64');\n }\n\n // Browser: use chunked encoding for large buffers to avoid O(n²) string growth\n if (data.length > LARGE_BUFFER_THRESHOLD) {\n return encodeChunked(data);\n }\n\n // Small buffers: simple approach is fast enough\n let binary = '';\n for (let i = 0; i < data.length; i++) {\n binary += String.fromCharCode(data[i]!);\n }\n return btoa(binary);\n}\n\n/**\n * Decode base64 string to binary data\n */\nexport function decode(base64: string): BinaryData {\n if (isNode) {\n // Node.js: use Buffer\n return new Uint8Array(Buffer.from(base64, 'base64'));\n }\n\n // Browser: use atob\n const binary = atob(base64);\n const result = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n result[i] = binary.charCodeAt(i);\n }\n return result;\n}\n\n/**\n * Check if a string is valid base64\n */\nexport function isBase64(str: string): boolean {\n if (str.length === 0) return false;\n // Base64 regex: only valid base64 characters, length multiple of 4 (with padding)\n const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;\n return base64Regex.test(str) && str.length % 4 === 0;\n}\n\n/**\n * Encode binary data to URL-safe base64 string\n * Replaces + with -, / with _, and removes padding\n */\nexport function encodeUrlSafe(data: BinaryData): string {\n return encode(data)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Decode URL-safe base64 string to binary data\n */\nexport function decodeUrlSafe(base64: string): BinaryData {\n // Add back padding if needed\n let padded = base64\n .replace(/-/g, '+')\n .replace(/_/g, '/');\n\n while (padded.length % 4 !== 0) {\n padded += '=';\n }\n\n return decode(padded);\n}\n\n/**\n * Chunk size for encoding large buffers (64KB)\n * Prevents stack overflow when using String.fromCharCode with spread operator\n */\nconst ENCODE_CHUNK_SIZE = 64 * 1024;\n\n/**\n * Encode binary data to base64 string with chunking for large buffers.\n * Handles buffers >10MB without stack overflow.\n *\n * @param data - Binary data to encode\n * @returns Base64 encoded string\n *\n * @example\n * ```typescript\n * const largeBuffer = new Uint8Array(20 * 1024 * 1024); // 20MB\n * const base64 = encodeChunked(largeBuffer); // No stack overflow\n * ```\n */\nexport function encodeChunked(data: BinaryData): string {\n if (isNode) {\n // Node.js: Buffer handles large data efficiently\n return Buffer.from(data).toString('base64');\n }\n\n // Browser: process in chunks to avoid stack overflow\n const chunks: string[] = [];\n\n for (let i = 0; i < data.length; i += ENCODE_CHUNK_SIZE) {\n const chunk = data.subarray(i, Math.min(i + ENCODE_CHUNK_SIZE, data.length));\n let binary = '';\n for (let j = 0; j < chunk.length; j++) {\n binary += String.fromCharCode(chunk[j]!);\n }\n chunks.push(binary);\n }\n\n return btoa(chunks.join(''));\n}\n","/**\n * Error Classes\n *\n * Specific error types for character card operations.\n * All errors extend FoundryError for consistent handling.\n */\n\n/** Symbol to identify FoundryError instances across ESM/CJS boundaries */\nconst FOUNDRY_ERROR_MARKER = Symbol.for('@character-foundry/core:FoundryError');\n\n/**\n * Base error class for all Character Foundry errors\n */\nexport class FoundryError extends Error {\n /** @internal Marker for cross-module identification */\n readonly [FOUNDRY_ERROR_MARKER] = true;\n\n constructor(message: string, public readonly code: string) {\n super(message);\n this.name = 'FoundryError';\n // Maintains proper stack trace in V8 environments\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error during card parsing\n */\nexport class ParseError extends FoundryError {\n constructor(message: string, public readonly format?: string) {\n super(message, 'PARSE_ERROR');\n this.name = 'ParseError';\n }\n}\n\n/**\n * Error during card validation\n */\nexport class ValidationError extends FoundryError {\n constructor(message: string, public readonly field?: string) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Asset not found in card or archive\n */\nexport class AssetNotFoundError extends FoundryError {\n constructor(public readonly uri: string) {\n super(`Asset not found: ${uri}`, 'ASSET_NOT_FOUND');\n this.name = 'AssetNotFoundError';\n }\n}\n\n/**\n * Format not supported for operation\n */\nexport class FormatNotSupportedError extends FoundryError {\n constructor(public readonly format: string, operation?: string) {\n const msg = operation\n ? `Format '${format}' not supported for ${operation}`\n : `Format not supported: ${format}`;\n super(msg, 'FORMAT_NOT_SUPPORTED');\n this.name = 'FormatNotSupportedError';\n }\n}\n\n/**\n * File size exceeds limits\n */\nexport class SizeLimitError extends FoundryError {\n constructor(\n public readonly actualSize: number,\n public readonly maxSize: number,\n context?: string\n ) {\n const actualMB = (actualSize / 1024 / 1024).toFixed(2);\n const maxMB = (maxSize / 1024 / 1024).toFixed(2);\n const msg = context\n ? `${context}: Size ${actualMB}MB exceeds limit ${maxMB}MB`\n : `Size ${actualMB}MB exceeds limit ${maxMB}MB`;\n super(msg, 'SIZE_LIMIT_EXCEEDED');\n this.name = 'SizeLimitError';\n }\n}\n\n/**\n * Path traversal or unsafe path detected\n */\nexport class PathTraversalError extends FoundryError {\n constructor(public readonly path: string) {\n super(`Unsafe path detected: ${path}`, 'PATH_TRAVERSAL');\n this.name = 'PathTraversalError';\n }\n}\n\n/**\n * Export operation would lose data\n */\nexport class DataLossError extends FoundryError {\n constructor(\n public readonly lostFields: string[],\n public readonly targetFormat: string\n ) {\n const fields = lostFields.slice(0, 3).join(', ');\n const more = lostFields.length > 3 ? ` and ${lostFields.length - 3} more` : '';\n super(\n `Export to ${targetFormat} would lose: ${fields}${more}`,\n 'DATA_LOSS'\n );\n this.name = 'DataLossError';\n }\n}\n\n/**\n * Check if an error is a FoundryError\n *\n * Uses Symbol.for() marker instead of instanceof to handle dual ESM/CJS package loading.\n * In dual-package environments, instanceof can fail if the error comes from a different\n * module instance (e.g., ESM vs CJS version of the same package). Symbol.for() creates\n * a global symbol shared across all module instances.\n */\nexport function isFoundryError(error: unknown): error is FoundryError {\n return (\n error instanceof Error &&\n FOUNDRY_ERROR_MARKER in error &&\n (error as Record<symbol, unknown>)[FOUNDRY_ERROR_MARKER] === true\n );\n}\n\n/**\n * Wrap unknown errors in a FoundryError\n */\nexport function wrapError(error: unknown, context?: string): FoundryError {\n if (isFoundryError(error)) {\n return error;\n }\n\n const message = error instanceof Error\n ? error.message\n : String(error);\n\n return new FoundryError(\n context ? `${context}: ${message}` : message,\n 'UNKNOWN_ERROR'\n );\n}\n","/**\n * Data URL Utilities\n *\n * Convert between Uint8Array buffers and data URLs.\n * Handles large buffers (>10MB) without stack overflow by processing in chunks.\n */\n\nimport type { BinaryData } from './binary.js';\nimport { encodeChunked as base64Encode, decode as base64Decode } from './base64.js';\nimport { ValidationError } from './errors.js';\n\n/**\n * Convert Uint8Array to data URL.\n * Handles large buffers (>10MB) without stack overflow by processing in chunks.\n *\n * @param buffer - Binary data to encode\n * @param mimeType - MIME type for the data URL (e.g., 'image/png', 'application/octet-stream')\n * @returns Data URL string\n *\n * @example\n * ```typescript\n * const png = new Uint8Array([...]);\n * const dataUrl = toDataURL(png, 'image/png');\n * // => \"data:image/png;base64,iVBORw0KGgo...\"\n * ```\n */\nexport function toDataURL(buffer: BinaryData, mimeType: string): string {\n // Use chunked encoding to handle large buffers without stack overflow\n const base64 = base64Encode(buffer);\n return `data:${mimeType};base64,${base64}`;\n}\n\n/**\n * Parse a data URL back to buffer and MIME type.\n * Validates the data URL format before parsing.\n *\n * @param dataUrl - Data URL string to parse\n * @returns Object containing the decoded buffer and MIME type\n * @throws Error if the data URL format is invalid\n *\n * @example\n * ```typescript\n * const { buffer, mimeType } = fromDataURL('data:image/png;base64,iVBORw0KGgo...');\n * // buffer: Uint8Array\n * // mimeType: 'image/png'\n * ```\n */\nexport function fromDataURL(dataUrl: string): { buffer: Uint8Array; mimeType: string } {\n // Validate data URL format\n if (!dataUrl.startsWith('data:')) {\n throw new ValidationError('Invalid data URL: must start with \"data:\"', 'dataUrl');\n }\n\n const commaIndex = dataUrl.indexOf(',');\n if (commaIndex === -1) {\n throw new ValidationError('Invalid data URL: missing comma separator', 'dataUrl');\n }\n\n const header = dataUrl.slice(5, commaIndex); // Skip 'data:'\n const data = dataUrl.slice(commaIndex + 1);\n\n // Parse header: [<mediatype>][;base64]\n let mimeType = 'text/plain';\n let isBase64 = false;\n\n const parts = header.split(';');\n for (const part of parts) {\n if (part === 'base64') {\n isBase64 = true;\n } else if (part && !part.includes('=')) {\n // MIME type (not a parameter like charset=utf-8)\n mimeType = part;\n }\n }\n\n if (!isBase64) {\n // URL-encoded text data\n throw new ValidationError('Non-base64 data URLs are not supported', 'dataUrl');\n }\n\n const buffer = base64Decode(data);\n return { buffer, mimeType };\n}\n\n/**\n * Check if a string is a valid data URL\n *\n * @param str - String to check\n * @returns true if the string is a valid data URL format\n */\nexport function isDataURL(str: string): boolean {\n if (!str.startsWith('data:')) return false;\n const commaIndex = str.indexOf(',');\n if (commaIndex === -1) return false;\n const header = str.slice(5, commaIndex);\n return header.includes('base64');\n}\n","/**\n * URI Utilities\n *\n * Handles different asset URI schemes used in character cards.\n * Supports: embeded://, embedded://, ccdefault:, https://, http://,\n * data:, file://, __asset:, asset:, chara-ext-asset_\n */\n\nexport type URIScheme =\n | 'embeded' // embeded:// (CharX standard, note intentional typo)\n | 'ccdefault' // ccdefault:\n | 'https' // https://\n | 'http' // http://\n | 'data' // data:mime;base64,...\n | 'file' // file://\n | 'internal' // Internal asset ID (UUID/string)\n | 'pngchunk' // PNG chunk reference (__asset:, asset:, chara-ext-asset_)\n | 'unknown';\n\nexport interface ParsedURI {\n scheme: URIScheme;\n originalUri: string;\n normalizedUri: string; // Normalized form of the URI\n path?: string; // For embeded://, file://\n url?: string; // For http://, https://\n data?: string; // For data: URIs\n mimeType?: string; // For data: URIs\n encoding?: string; // For data: URIs (e.g., base64)\n chunkKey?: string; // For pngchunk - the key/index to look up\n chunkCandidates?: string[]; // For pngchunk - all possible chunk keys to search\n}\n\n/**\n * Normalize a URI to its canonical form\n * Handles common typos and variant formats\n */\nexport function normalizeURI(uri: string): string {\n const trimmed = uri.trim();\n\n // Fix embedded:// -> embeded:// (common typo, CharX spec uses single 'd')\n if (trimmed.startsWith('embedded://')) {\n return 'embeded://' + trimmed.substring('embedded://'.length);\n }\n\n // Normalize PNG chunk references to pngchunk: scheme\n if (trimmed.startsWith('__asset:')) {\n const id = trimmed.substring('__asset:'.length);\n return `pngchunk:${id}`;\n }\n if (trimmed.startsWith('asset:')) {\n const id = trimmed.substring('asset:'.length);\n return `pngchunk:${id}`;\n }\n if (trimmed.startsWith('chara-ext-asset_:')) {\n const id = trimmed.substring('chara-ext-asset_:'.length);\n return `pngchunk:${id}`;\n }\n if (trimmed.startsWith('chara-ext-asset_')) {\n const id = trimmed.substring('chara-ext-asset_'.length);\n return `pngchunk:${id}`;\n }\n\n return trimmed;\n}\n\n/**\n * Parse a URI and determine its scheme and components\n */\nexport function parseURI(uri: string): ParsedURI {\n const trimmed = uri.trim();\n const normalized = normalizeURI(trimmed);\n\n // PNG chunk references (__asset:, asset:, chara-ext-asset_, pngchunk:)\n if (\n trimmed.startsWith('__asset:') ||\n trimmed.startsWith('asset:') ||\n trimmed.startsWith('chara-ext-asset_') ||\n trimmed.startsWith('pngchunk:')\n ) {\n let assetId: string;\n if (trimmed.startsWith('__asset:')) {\n assetId = trimmed.substring('__asset:'.length);\n } else if (trimmed.startsWith('asset:')) {\n assetId = trimmed.substring('asset:'.length);\n } else if (trimmed.startsWith('chara-ext-asset_:')) {\n assetId = trimmed.substring('chara-ext-asset_:'.length);\n } else if (trimmed.startsWith('pngchunk:')) {\n assetId = trimmed.substring('pngchunk:'.length);\n } else {\n assetId = trimmed.substring('chara-ext-asset_'.length);\n }\n\n // Generate all possible chunk key variations for lookup\n const candidates = [\n assetId, // \"0\" or \"filename.png\"\n trimmed, // Original URI\n `asset:${assetId}`, // \"asset:0\"\n `__asset:${assetId}`, // \"__asset:0\"\n `__asset_${assetId}`, // \"__asset_0\"\n `chara-ext-asset_${assetId}`, // \"chara-ext-asset_0\"\n `chara-ext-asset_:${assetId}`, // \"chara-ext-asset_:0\"\n `pngchunk:${assetId}`, // \"pngchunk:0\"\n ];\n\n return {\n scheme: 'pngchunk',\n originalUri: uri,\n normalizedUri: normalized,\n chunkKey: assetId,\n chunkCandidates: candidates,\n };\n }\n\n // ccdefault: - use default asset\n if (trimmed === 'ccdefault:' || trimmed.startsWith('ccdefault:')) {\n return {\n scheme: 'ccdefault',\n originalUri: uri,\n normalizedUri: normalized,\n };\n }\n\n // embeded:// or embedded:// (normalize typo)\n if (trimmed.startsWith('embeded://') || trimmed.startsWith('embedded://')) {\n const path = trimmed.startsWith('embeded://')\n ? trimmed.substring('embeded://'.length)\n : trimmed.substring('embedded://'.length);\n return {\n scheme: 'embeded',\n originalUri: uri,\n normalizedUri: normalized,\n path,\n };\n }\n\n // https://\n if (trimmed.startsWith('https://')) {\n return {\n scheme: 'https',\n originalUri: uri,\n normalizedUri: normalized,\n url: trimmed,\n };\n }\n\n // http://\n if (trimmed.startsWith('http://')) {\n return {\n scheme: 'http',\n originalUri: uri,\n normalizedUri: normalized,\n url: trimmed,\n };\n }\n\n // data: URIs\n if (trimmed.startsWith('data:')) {\n const parsed = parseDataURI(trimmed);\n return {\n scheme: 'data',\n originalUri: uri,\n normalizedUri: normalized,\n ...parsed,\n };\n }\n\n // file://\n if (trimmed.startsWith('file://')) {\n const path = trimmed.substring('file://'.length);\n return {\n scheme: 'file',\n originalUri: uri,\n normalizedUri: normalized,\n path,\n };\n }\n\n // Internal asset ID (alphanumeric/UUID format)\n if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {\n return {\n scheme: 'internal',\n originalUri: uri,\n normalizedUri: normalized,\n path: trimmed,\n };\n }\n\n // Unknown scheme\n return {\n scheme: 'unknown',\n originalUri: uri,\n normalizedUri: normalized,\n };\n}\n\n/**\n * Parse a data URI into its components\n * Format: data:[<mediatype>][;base64],<data>\n */\nfunction parseDataURI(uri: string): { mimeType?: string; encoding?: string; data?: string } {\n const match = uri.match(/^data:([^;,]+)?(;base64)?,(.*)$/);\n\n if (!match) {\n return {};\n }\n\n return {\n mimeType: match[1] || 'text/plain',\n encoding: match[2] ? 'base64' : undefined,\n data: match[3],\n };\n}\n\n/**\n * Check if extension is an image format\n */\nexport function isImageExt(ext: string): boolean {\n const imageExts = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'avif', 'bmp', 'svg'];\n return imageExts.includes(ext.toLowerCase());\n}\n\n/**\n * Check if extension is an audio format\n */\nexport function isAudioExt(ext: string): boolean {\n const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'];\n return audioExts.includes(ext.toLowerCase());\n}\n\n/**\n * Check if extension is a video format\n */\nexport function isVideoExt(ext: string): boolean {\n const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv'];\n return videoExts.includes(ext.toLowerCase());\n}\n\n/** Safe MIME types for data: URIs that can be used in href/src */\nconst SAFE_DATA_URI_MIME_TYPES = new Set([\n // Images (safe for img src)\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'image/avif',\n 'image/bmp',\n 'image/x-icon',\n // Audio (safe for audio src)\n 'audio/mpeg',\n 'audio/wav',\n 'audio/ogg',\n 'audio/flac',\n 'audio/mp4',\n 'audio/aac',\n // Video (safe for video src)\n 'video/mp4',\n 'video/webm',\n // Text/data (generally safe)\n 'text/plain',\n 'application/json',\n 'application/octet-stream',\n]);\n\n/** Potentially dangerous MIME types that should NOT be used in href/src */\nconst DANGEROUS_DATA_URI_MIME_TYPES = new Set([\n // Executable/script content\n 'text/html',\n 'text/javascript',\n 'application/javascript',\n 'application/x-javascript',\n 'text/css',\n 'image/svg+xml', // SVG can contain scripts\n 'application/xhtml+xml',\n 'application/xml',\n]);\n\n/**\n * Options for URI safety validation\n */\nexport interface URISafetyOptions {\n /** Allow http:// URIs (default: false) */\n allowHttp?: boolean;\n /** Allow file:// URIs (default: false) */\n allowFile?: boolean;\n /**\n * Allowed MIME types for data: URIs (default: all safe types).\n * Set to empty array to reject all data: URIs.\n * Set to undefined to use default safe list.\n */\n allowedDataMimes?: string[];\n}\n\n/**\n * Result of URI safety check with detailed information\n */\nexport interface URISafetyResult {\n /** Whether the URI is safe to use */\n safe: boolean;\n /** Reason if unsafe */\n reason?: string;\n /** Detected scheme */\n scheme: URIScheme;\n /** MIME type for data: URIs */\n mimeType?: string;\n}\n\n/**\n * Validate if a URI is safe to use (detailed version)\n *\n * @param uri - URI to validate\n * @param options - Safety options\n * @returns Detailed safety result\n */\nexport function checkURISafety(uri: string, options: URISafetyOptions = {}): URISafetyResult {\n const parsed = parseURI(uri);\n\n switch (parsed.scheme) {\n case 'embeded':\n case 'ccdefault':\n case 'internal':\n case 'https':\n case 'pngchunk':\n return { safe: true, scheme: parsed.scheme };\n\n case 'data': {\n const mimeType = parsed.mimeType || 'text/plain';\n\n // Check for explicitly dangerous MIME types\n if (DANGEROUS_DATA_URI_MIME_TYPES.has(mimeType)) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: `Data URI with potentially dangerous MIME type: ${mimeType}`,\n };\n }\n\n // If custom allowed list is provided, check against it\n if (options.allowedDataMimes !== undefined) {\n if (options.allowedDataMimes.length === 0) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: 'Data URIs are not allowed',\n };\n }\n if (!options.allowedDataMimes.includes(mimeType)) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: `Data URI MIME type not in allowed list: ${mimeType}`,\n };\n }\n }\n\n // Otherwise use default safe list\n if (!SAFE_DATA_URI_MIME_TYPES.has(mimeType)) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: `Unknown data URI MIME type: ${mimeType}`,\n };\n }\n\n return { safe: true, scheme: parsed.scheme, mimeType };\n }\n\n case 'http':\n if (options.allowHttp === true) {\n return { safe: true, scheme: parsed.scheme };\n }\n return { safe: false, scheme: parsed.scheme, reason: 'HTTP URIs are not allowed' };\n\n case 'file':\n if (options.allowFile === true) {\n return { safe: true, scheme: parsed.scheme };\n }\n return { safe: false, scheme: parsed.scheme, reason: 'File URIs are not allowed' };\n\n case 'unknown':\n default:\n return { safe: false, scheme: parsed.scheme, reason: 'Unknown URI scheme' };\n }\n}\n\n/**\n * Validate if a URI is safe to use (simple boolean version for backwards compatibility)\n *\n * @deprecated Use checkURISafety() for detailed safety information\n */\nexport function isURISafe(uri: string, options: { allowHttp?: boolean; allowFile?: boolean } = {}): boolean {\n return checkURISafety(uri, options).safe;\n}\n\n/**\n * Extract file extension from URI\n */\nexport function getExtensionFromURI(uri: string): string {\n const parsed = parseURI(uri);\n\n if (parsed.path) {\n const parts = parsed.path.split('.');\n if (parts.length > 1) {\n return parts[parts.length - 1]!.toLowerCase();\n }\n }\n\n if (parsed.url) {\n const urlParts = parsed.url.split('?')[0]!.split('.');\n if (urlParts.length > 1) {\n return urlParts[urlParts.length - 1]!.toLowerCase();\n }\n }\n\n if (parsed.mimeType) {\n return getExtFromMimeType(parsed.mimeType);\n }\n\n return 'unknown';\n}\n\n/**\n * Get MIME type from file extension\n */\nexport function getMimeTypeFromExt(ext: string): string {\n const extToMime: Record<string, string> = {\n // Images\n 'png': 'image/png',\n 'jpg': 'image/jpeg',\n 'jpeg': 'image/jpeg',\n 'webp': 'image/webp',\n 'gif': 'image/gif',\n 'avif': 'image/avif',\n 'svg': 'image/svg+xml',\n 'bmp': 'image/bmp',\n 'ico': 'image/x-icon',\n\n // Audio\n 'mp3': 'audio/mpeg',\n 'wav': 'audio/wav',\n 'ogg': 'audio/ogg',\n 'flac': 'audio/flac',\n 'm4a': 'audio/mp4',\n 'aac': 'audio/aac',\n\n // Video\n 'mp4': 'video/mp4',\n 'webm': 'video/webm',\n 'avi': 'video/x-msvideo',\n 'mov': 'video/quicktime',\n 'mkv': 'video/x-matroska',\n\n // Text/Data\n 'json': 'application/json',\n 'txt': 'text/plain',\n 'html': 'text/html',\n 'css': 'text/css',\n 'js': 'application/javascript',\n };\n\n return extToMime[ext.toLowerCase()] || 'application/octet-stream';\n}\n\n/**\n * Get file extension from MIME type\n */\nexport function getExtFromMimeType(mimeType: string): string {\n const mimeToExt: Record<string, string> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n 'image/avif': 'avif',\n 'image/svg+xml': 'svg',\n 'image/bmp': 'bmp',\n 'image/x-icon': 'ico',\n 'audio/mpeg': 'mp3',\n 'audio/wav': 'wav',\n 'audio/ogg': 'ogg',\n 'audio/flac': 'flac',\n 'audio/mp4': 'm4a',\n 'audio/aac': 'aac',\n 'video/mp4': 'mp4',\n 'video/webm': 'webm',\n 'video/x-msvideo': 'avi',\n 'video/quicktime': 'mov',\n 'video/x-matroska': 'mkv',\n 'application/json': 'json',\n 'text/plain': 'txt',\n 'text/html': 'html',\n 'text/css': 'css',\n 'application/javascript': 'js',\n };\n\n return mimeToExt[mimeType] || 'bin';\n}\n\n/**\n * Build a data URI from binary data and MIME type\n */\nexport function buildDataURI(data: string, mimeType: string, isBase64 = true): string {\n if (isBase64) {\n return `data:${mimeType};base64,${data}`;\n }\n return `data:${mimeType},${encodeURIComponent(data)}`;\n}\n","/**\n * Image Analysis Utilities\n *\n * Detect properties of image files from binary data.\n */\n\nimport {\n type BinaryData,\n indexOf,\n fromLatin1,\n} from './binary.js';\n\n/**\n * Check if an image buffer contains animation data.\n * Supports: APNG, WebP (Animated), GIF\n */\nexport function isAnimatedImage(data: BinaryData, _mimeType?: string): boolean {\n // 1. WebP Detection\n // RIFF .... WEBP\n if (\n data.length > 12 &&\n data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 && // RIFF\n data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50 // WEBP\n ) {\n // Check for VP8X chunk\n // VP8X chunk header: 'VP8X' (bytes 12-15)\n if (\n data[12] === 0x56 && data[13] === 0x50 && data[14] === 0x38 && data[15] === 0x58\n ) {\n // Flags byte is at offset 20 (16 + 4 bytes chunk size)\n // Animation bit is bit 1 (0x02)\n const flags = data[20];\n return (flags! & 0x02) !== 0;\n }\n return false;\n }\n\n // 2. PNG/APNG Detection\n // Signature: 89 50 4E 47 0D 0A 1A 0A\n if (\n data.length > 8 &&\n data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E && data[3] === 0x47\n ) {\n // Search for 'acTL' chunk (Animation Control)\n // It must appear before IDAT.\n // Simple search: indexOf('acTL')\n // Note: theoretically 'acTL' string could appear in other data, but highly unlikely in valid PNG structure before IDAT\n // We can iterate chunks to be safe, but indexOf is faster for a quick check\n const actlSig = fromLatin1('acTL');\n const idatSig = fromLatin1('IDAT');\n \n const actlIndex = indexOf(data, actlSig);\n if (actlIndex === -1) return false;\n\n const idatIndex = indexOf(data, idatSig);\n // If acTL exists and is before the first IDAT (or IDAT not found yet), it's APNG\n return idatIndex === -1 || actlIndex < idatIndex;\n }\n\n // 3. GIF Detection\n // Signature: GIF87a or GIF89a\n if (\n data.length > 6 &&\n data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 // GIF\n ) {\n // Check for NETSCAPE2.0 extension (looping animation)\n // This is a heuristic. Static GIFs are rare in this domain but possible.\n // Full frame counting is expensive. Presence of NETSCAPE block is a strong indicator.\n const netscape = fromLatin1('NETSCAPE2.0');\n return indexOf(data, netscape) !== -1;\n }\n\n return false;\n}\n","/**\n * UUID Generation Utilities\n *\n * Provides crypto-grade UUID v4 generation that works in Node.js,\n * browsers (secure contexts), and falls back gracefully.\n */\n\n/**\n * Format 16 random bytes as a UUID v4 string\n */\nfunction formatUUID(bytes: Uint8Array): string {\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n/**\n * Fallback UUID generation using Math.random()\n * Only used when crypto APIs are unavailable (rare)\n */\nfunction mathRandomUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generate a cryptographically secure UUID v4.\n *\n * Uses crypto.randomUUID() when available (Node.js 19+, modern browsers).\n * Falls back to crypto.getRandomValues() if randomUUID is unavailable.\n * Last resort uses Math.random() (non-secure, emits warning in dev).\n *\n * @returns A valid RFC 4122 UUID v4 string\n *\n * @example\n * ```typescript\n * const id = generateUUID();\n * // => \"550e8400-e29b-41d4-a716-446655440000\"\n * ```\n */\nexport function generateUUID(): string {\n // Node.js 19+ or browser with secure context\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n\n // Fallback using crypto.getRandomValues (older Node/browsers)\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n // Set version (4) and variant (RFC 4122)\n bytes[6] = (bytes[6]! & 0x0f) | 0x40; // Version 4\n bytes[8] = (bytes[8]! & 0x3f) | 0x80; // Variant 1\n return formatUUID(bytes);\n }\n\n // Last resort - non-secure fallback\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {\n console.warn('[character-foundry/core] generateUUID: Using insecure Math.random() fallback');\n }\n return mathRandomUUID();\n}\n\n/**\n * Validate if a string is a valid UUID v4\n *\n * @param uuid - String to validate\n * @returns true if valid UUID v4 format\n */\nexport function isValidUUID(uuid: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid);\n}\n","/**\n * Binary Data Utilities\n *\n * Universal binary data operations using Uint8Array.\n * Works in both Node.js and browser environments.\n */\n\n/**\n * Universal binary data type (works in both environments)\n */\nexport type BinaryData = Uint8Array;\n\n/**\n * Read a 32-bit big-endian unsigned integer\n */\nexport function readUInt32BE(data: BinaryData, offset: number): number {\n return (\n (data[offset]! << 24) |\n (data[offset + 1]! << 16) |\n (data[offset + 2]! << 8) |\n data[offset + 3]!\n ) >>> 0;\n}\n\n/**\n * Write a 32-bit big-endian unsigned integer\n */\nexport function writeUInt32BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 24) & 0xff;\n data[offset + 1] = (value >>> 16) & 0xff;\n data[offset + 2] = (value >>> 8) & 0xff;\n data[offset + 3] = value & 0xff;\n}\n\n/**\n * Read a 16-bit big-endian unsigned integer\n */\nexport function readUInt16BE(data: BinaryData, offset: number): number {\n return ((data[offset]! << 8) | data[offset + 1]!) >>> 0;\n}\n\n/**\n * Write a 16-bit big-endian unsigned integer\n */\nexport function writeUInt16BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 8) & 0xff;\n data[offset + 1] = value & 0xff;\n}\n\n/**\n * Find a byte sequence in binary data\n */\nexport function indexOf(data: BinaryData, search: BinaryData, fromIndex = 0): number {\n outer: for (let i = fromIndex; i <= data.length - search.length; i++) {\n for (let j = 0; j < search.length; j++) {\n if (data[i + j] !== search[j]) continue outer;\n }\n return i;\n }\n return -1;\n}\n\n/**\n * Concatenate multiple binary arrays\n */\nexport function concat(...arrays: BinaryData[]): BinaryData {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * Slice binary data (returns a view, not a copy)\n */\nexport function slice(data: BinaryData, start: number, end?: number): BinaryData {\n return data.subarray(start, end);\n}\n\n/**\n * Copy a portion of binary data (returns a new array)\n */\nexport function copy(data: BinaryData, start: number, end?: number): BinaryData {\n return data.slice(start, end);\n}\n\n/**\n * Convert string to binary (UTF-8)\n */\nexport function fromString(str: string): BinaryData {\n return new TextEncoder().encode(str);\n}\n\n/**\n * Convert binary to string (UTF-8)\n */\nexport function toString(data: BinaryData): string {\n return new TextDecoder().decode(data);\n}\n\n/**\n * Convert string to binary (Latin1 - for PNG keywords and similar)\n */\nexport function fromLatin1(str: string): BinaryData {\n const result = new Uint8Array(str.length);\n for (let i = 0; i < str.length; i++) {\n result[i] = str.charCodeAt(i) & 0xff;\n }\n return result;\n}\n\n/**\n * Convert binary to string (Latin1)\n */\nexport function toLatin1(data: BinaryData): string {\n let result = '';\n for (let i = 0; i < data.length; i++) {\n result += String.fromCharCode(data[i]!);\n }\n return result;\n}\n\n/**\n * Compare two binary arrays for equality\n */\nexport function equals(a: BinaryData, b: BinaryData): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Create a new Uint8Array filled with zeros\n */\nexport function alloc(size: number): BinaryData {\n return new Uint8Array(size);\n}\n\n/**\n * Create a Uint8Array from an array of numbers\n */\nexport function from(data: number[] | ArrayBuffer | BinaryData): BinaryData {\n if (data instanceof Uint8Array) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n return new Uint8Array(data);\n}\n\n/**\n * Check if value is a Uint8Array\n */\nexport function isBinaryData(value: unknown): value is BinaryData {\n return value instanceof Uint8Array;\n}\n\n/**\n * Convert Node.js Buffer to Uint8Array (no-op if already Uint8Array)\n * This provides compatibility when interfacing with Node.js code\n */\nexport function toUint8Array(data: BinaryData | Buffer): BinaryData {\n if (data instanceof Uint8Array) {\n // Buffer extends Uint8Array, but we want a plain Uint8Array\n // This ensures we get a proper Uint8Array in all cases\n if (Object.getPrototypeOf(data).constructor.name === 'Buffer') {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n return data;\n }\n return new Uint8Array(data);\n}\n\n/**\n * Convert binary data to hex string\n */\nexport function toHex(data: BinaryData): string {\n return Array.from(data)\n .map(b => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Convert hex string to binary data\n */\nexport function fromHex(hex: string): BinaryData {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n }\n return bytes;\n}\n","/**\n * ZIP Utility Functions\n *\n * Handles ZIP format detection and SFX (self-extracting) archive support.\n * Uses Uint8Array for universal browser/Node.js compatibility.\n */\n\nimport { indexOf, concat, type BinaryData } from './binary.js';\nimport { Unzip, UnzipInflate, UnzipPassThrough, type Unzipped, type UnzipFile } from 'fflate';\n\n// ZIP local file header signature: PK\\x03\\x04\nexport const ZIP_SIGNATURE = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);\n\n// JPEG signatures\nexport const JPEG_SIGNATURE = new Uint8Array([0xff, 0xd8, 0xff]);\n\n/**\n * How to handle unsafe paths (with .. or absolute paths) during extraction.\n *\n * - 'skip': Silently skip unsafe files (default, backwards compatible)\n * - 'warn': Skip and call onUnsafePath callback for logging/monitoring\n * - 'reject': Throw ZipPreflightError immediately (strictest, recommended for untrusted input)\n */\nexport type UnsafePathHandling = 'skip' | 'warn' | 'reject';\n\n/**\n * Size limits for ZIP operations\n */\nexport interface ZipSizeLimits {\n /** Max size per file (default 50MB) */\n maxFileSize: number;\n /** Max total size (default 200MB) */\n maxTotalSize: number;\n /** Max number of files (default 1000) */\n maxFiles: number;\n /**\n * How to handle files with unsafe paths (path traversal attempts).\n *\n * Default: 'skip' (backwards compatible - silently ignores unsafe paths)\n * Recommended: 'reject' for untrusted input (throws on path traversal)\n *\n * @security Path traversal (../) in ZIP entries can lead to arbitrary file\n * overwrites when extracted to disk. While this library returns files in\n * memory, consumers may write to disk using the paths.\n */\n unsafePathHandling?: UnsafePathHandling;\n /**\n * Callback invoked when unsafe path is detected (only with 'warn' handling).\n * Use for logging/monitoring path traversal attempts.\n */\n onUnsafePath?: (path: string, reason: string) => void;\n}\n\nexport const DEFAULT_ZIP_LIMITS: ZipSizeLimits = {\n maxFileSize: 50 * 1024 * 1024, // 50MB per file (Risu standard)\n maxTotalSize: 500 * 1024 * 1024, // 500MB total (CharX can have many expression assets)\n maxFiles: 10000, // CharX cards can have 2k+ expression assets\n unsafePathHandling: 'skip', // Backwards compatible default\n};\n\n/**\n * Check if a buffer contains ZIP data (anywhere in the buffer).\n * This handles both regular ZIPs and SFX (self-extracting) archives.\n * @param data - Binary data to check\n * @returns true if ZIP signature found\n */\nexport function isZipBuffer(data: BinaryData): boolean {\n return indexOf(data, ZIP_SIGNATURE) >= 0;\n}\n\n/**\n * Check if a buffer starts with ZIP signature (standard ZIP detection).\n * @param data - Binary data to check\n * @returns true if data starts with PK\\x03\\x04\n */\nexport function startsWithZipSignature(data: BinaryData): boolean {\n return (\n data.length >= 4 &&\n data[0] === 0x50 &&\n data[1] === 0x4b &&\n data[2] === 0x03 &&\n data[3] === 0x04\n );\n}\n\n/**\n * Check if data starts with JPEG signature\n */\nexport function isJPEG(data: BinaryData): boolean {\n return (\n data.length >= 3 &&\n data[0] === 0xff &&\n data[1] === 0xd8 &&\n data[2] === 0xff\n );\n}\n\n/**\n * Check if data is a JPEG with appended ZIP (JPEG+CharX hybrid)\n */\nexport function isJpegCharX(data: BinaryData): boolean {\n if (!isJPEG(data)) return false;\n // Look for ZIP signature after JPEG data\n return indexOf(data, ZIP_SIGNATURE) > 0;\n}\n\n/**\n * Find ZIP data start in buffer (handles SFX/self-extracting archives).\n * SFX archives have an executable stub prepended to the ZIP data.\n *\n * @param data - Binary data that may contain ZIP data (possibly with SFX prefix)\n * @returns Binary data starting at ZIP signature, or original data if not found/already at start\n */\nexport function findZipStart(data: BinaryData): BinaryData {\n const index = indexOf(data, ZIP_SIGNATURE);\n\n if (index > 0) {\n // SFX archive detected - return data starting at ZIP signature\n return data.subarray(index);\n }\n\n // Either ZIP starts at 0, or no ZIP signature found - return original\n return data;\n}\n\n/**\n * Get the offset of ZIP data within a buffer.\n * @param data - Binary data to search\n * @returns Offset of ZIP signature, or -1 if not found\n */\nexport function getZipOffset(data: BinaryData): number {\n return indexOf(data, ZIP_SIGNATURE);\n}\n\n/**\n * Check if data is a valid ZIP archive (has signature at start or is SFX)\n * @param data - Binary data to check\n * @returns true if data contains valid ZIP structure\n */\nexport function isValidZip(data: BinaryData): boolean {\n const offset = getZipOffset(data);\n if (offset < 0) return false;\n\n // Check if there's enough data after the signature for a minimal ZIP\n // Minimum ZIP: local file header (30 bytes) + central directory (46 bytes) + end of central dir (22 bytes)\n return data.length - offset >= 98;\n}\n\n/**\n * Validate a path for directory traversal attacks\n * @param path - File path to validate\n * @returns true if path is safe\n */\nexport function isPathSafe(path: string): boolean {\n // Reject absolute paths\n if (path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {\n return false;\n }\n\n // Reject path traversal\n if (path.includes('..')) {\n return false;\n }\n\n // Reject backslashes (Windows-style paths that might be used for traversal)\n if (path.includes('\\\\')) {\n return false;\n }\n\n return true;\n}\n\n/**\n * ZIP Central Directory File Header structure\n */\nexport interface ZipCentralDirEntry {\n fileName: string;\n compressedSize: number;\n uncompressedSize: number;\n}\n\n/**\n * Result of preflight ZIP size check\n */\nexport interface ZipPreflightResult {\n entries: ZipCentralDirEntry[];\n totalUncompressedSize: number;\n fileCount: number;\n}\n\n/**\n * Error thrown when ZIP preflight fails due to size limits\n */\nexport class ZipPreflightError extends Error {\n constructor(\n message: string,\n public readonly totalSize?: number,\n public readonly maxSize?: number,\n public readonly oversizedEntry?: string,\n public readonly entrySize?: number,\n public readonly maxEntrySize?: number\n ) {\n super(message);\n this.name = 'ZipPreflightError';\n }\n}\n\n/**\n * Preflight check ZIP central directory to get uncompressed sizes BEFORE extraction.\n * This prevents zip bomb attacks by rejecting archives with dangerous compression ratios\n * or oversized entries without fully decompressing them.\n *\n * The ZIP format stores uncompressed sizes in the central directory at the end of the file.\n * This function reads that metadata without decompressing any actual data.\n *\n * @param data - ZIP file data (can be SFX/self-extracting, will find ZIP start)\n * @param limits - Size limits to enforce\n * @returns Preflight result with entry info and totals\n * @throws ZipPreflightError if limits would be exceeded\n */\nexport function preflightZipSizes(\n data: BinaryData,\n limits: ZipSizeLimits = DEFAULT_ZIP_LIMITS\n): ZipPreflightResult {\n // Find ZIP start (handles SFX/hybrid archives)\n const zipData = findZipStart(data);\n\n // Find End of Central Directory (EOCD) signature: PK\\x05\\x06\n // EOCD is at the end of the file, max comment size is 65535 bytes\n const eocdSignature = new Uint8Array([0x50, 0x4b, 0x05, 0x06]);\n const searchStart = Math.max(0, zipData.length - 65535 - 22);\n\n let eocdOffset = -1;\n for (let i = zipData.length - 22; i >= searchStart; i--) {\n if (\n zipData[i] === eocdSignature[0] &&\n zipData[i + 1] === eocdSignature[1] &&\n zipData[i + 2] === eocdSignature[2] &&\n zipData[i + 3] === eocdSignature[3]\n ) {\n // SECURITY: Validate comment length to avoid false positives\n // If the ZIP comment contains the EOCD signature bytes, we could\n // find the wrong location. Verify by checking comment length.\n // Comment length is at offset +20 (2 bytes, little-endian)\n if (i + 21 < zipData.length) {\n const commentLength = zipData[i + 20]! | (zipData[i + 21]! << 8);\n // True EOCD should be at: file_size - 22 - comment_length\n const expectedOffset = zipData.length - 22 - commentLength;\n if (i === expectedOffset) {\n eocdOffset = i;\n break;\n }\n // Otherwise, this is a false positive (signature in comment), continue searching\n }\n }\n }\n\n if (eocdOffset < 0) {\n throw new ZipPreflightError('Invalid ZIP: End of Central Directory not found');\n }\n\n // Parse EOCD\n // Offset 8: Total number of central directory records\n // Offset 12: Size of central directory\n // Offset 16: Offset of start of central directory\n const totalEntries = zipData[eocdOffset + 8]! | (zipData[eocdOffset + 9]! << 8);\n const _cdSize = readUInt32LEFromBytes(zipData, eocdOffset + 12);\n const cdOffset = readUInt32LEFromBytes(zipData, eocdOffset + 16);\n\n // Check file count limit\n if (totalEntries > limits.maxFiles) {\n throw new ZipPreflightError(\n `ZIP contains ${totalEntries} files, exceeds limit of ${limits.maxFiles}`,\n undefined,\n undefined,\n undefined,\n undefined,\n undefined\n );\n }\n\n // Parse Central Directory entries\n const entries: ZipCentralDirEntry[] = [];\n let totalUncompressedSize = 0;\n let offset = cdOffset;\n\n for (let i = 0; i < totalEntries && offset < eocdOffset; i++) {\n // Central Directory File Header signature: PK\\x01\\x02\n if (\n zipData[offset] !== 0x50 ||\n zipData[offset + 1] !== 0x4b ||\n zipData[offset + 2] !== 0x01 ||\n zipData[offset + 3] !== 0x02\n ) {\n throw new ZipPreflightError('Invalid ZIP: Central Directory header corrupted');\n }\n\n // Offset 20: Compressed size (4 bytes)\n const compressedSize = readUInt32LEFromBytes(zipData, offset + 20);\n // Offset 24: Uncompressed size (4 bytes)\n const uncompressedSize = readUInt32LEFromBytes(zipData, offset + 24);\n // Offset 28: File name length (2 bytes)\n const fileNameLength = zipData[offset + 28]! | (zipData[offset + 29]! << 8);\n // Offset 30: Extra field length (2 bytes)\n const extraLength = zipData[offset + 30]! | (zipData[offset + 31]! << 8);\n // Offset 32: File comment length (2 bytes)\n const commentLength = zipData[offset + 32]! | (zipData[offset + 33]! << 8);\n\n // Read file name\n const fileName = new TextDecoder().decode(\n zipData.subarray(offset + 46, offset + 46 + fileNameLength)\n );\n\n // Skip directories (names ending with /)\n if (!fileName.endsWith('/')) {\n // Check per-entry size limit\n if (uncompressedSize > limits.maxFileSize) {\n throw new ZipPreflightError(\n `File \"${fileName}\" uncompressed size ${uncompressedSize} exceeds limit ${limits.maxFileSize}`,\n undefined,\n undefined,\n fileName,\n uncompressedSize,\n limits.maxFileSize\n );\n }\n\n totalUncompressedSize += uncompressedSize;\n\n // Check total size limit early to fail fast\n if (totalUncompressedSize > limits.maxTotalSize) {\n throw new ZipPreflightError(\n `Total uncompressed size ${totalUncompressedSize} exceeds limit ${limits.maxTotalSize}`,\n totalUncompressedSize,\n limits.maxTotalSize\n );\n }\n\n entries.push({\n fileName,\n compressedSize,\n uncompressedSize,\n });\n }\n\n // Move to next entry\n offset += 46 + fileNameLength + extraLength + commentLength;\n }\n\n return {\n entries,\n totalUncompressedSize,\n fileCount: entries.length,\n };\n}\n\n/**\n * Read a 32-bit little-endian unsigned integer from bytes\n */\nfunction readUInt32LEFromBytes(data: BinaryData, offset: number): number {\n return (\n data[offset]! |\n (data[offset + 1]! << 8) |\n (data[offset + 2]! << 16) |\n (data[offset + 3]! << 24)\n ) >>> 0; // Convert to unsigned\n}\n\n/**\n * Streaming ZIP extraction with real-time byte limit enforcement.\n *\n * Unlike preflightZipSizes which only checks central directory metadata,\n * this function tracks ACTUAL decompressed bytes during extraction and\n * aborts immediately if limits are exceeded. This protects against\n * malicious archives that lie about sizes in their central directory.\n *\n * @security Path safety is enforced based on `limits.unsafePathHandling`:\n * - 'skip' (default): Silently ignores files with unsafe paths\n * - 'warn': Skips unsafe files and calls onUnsafePath callback\n * - 'reject': Throws ZipPreflightError on unsafe paths\n *\n * @performance SYNCHRONOUS BLOCKING OPERATION\n * This function runs synchronously and will block the event loop during\n * decompression. For large archives (>50MB), this can cause UI freezes\n * in browser environments or block the Node.js event loop.\n *\n * Recommendations:\n * - Browser: Use Web Workers for archives >10MB to avoid freezing the UI\n * - Node.js: Consider worker threads for archives >50MB\n * - All environments: Call preflightZipSizes() first to validate before extraction\n *\n * @param data - ZIP file data (can be SFX/self-extracting)\n * @param limits - Size limits to enforce\n * @returns Extracted files (synchronously)\n * @throws ZipPreflightError if limits are exceeded during extraction\n */\nexport function streamingUnzipSync(\n data: BinaryData,\n limits: ZipSizeLimits = DEFAULT_ZIP_LIMITS\n): Unzipped {\n // Find ZIP start (handles SFX/hybrid archives)\n const zipData = findZipStart(data);\n\n const result: Unzipped = {};\n let totalBytes = 0;\n let fileCount = 0;\n let error: Error | null = null;\n\n // Get path handling mode (default to 'skip' for backwards compatibility)\n const unsafePathHandling = limits.unsafePathHandling ?? 'skip';\n\n // Track chunks per file for concatenation\n const fileChunks = new Map<string, Uint8Array[]>();\n\n const unzipper = new Unzip((file: UnzipFile) => {\n if (error) return;\n\n // Skip directories\n if (file.name.endsWith('/')) {\n file.start();\n return;\n }\n\n // SECURITY: Check for path traversal attacks\n if (!isPathSafe(file.name)) {\n const reason = file.name.includes('..')\n ? 'path traversal (..)'\n : file.name.startsWith('/') || /^[a-zA-Z]:/.test(file.name)\n ? 'absolute path'\n : 'backslash in path';\n\n if (unsafePathHandling === 'reject') {\n error = new ZipPreflightError(\n `Unsafe path detected: \"${file.name}\" - ${reason}. ` +\n `This may be a path traversal attack.`\n );\n file.terminate();\n return;\n }\n\n if (unsafePathHandling === 'warn' && limits.onUnsafePath) {\n limits.onUnsafePath(file.name, reason);\n }\n\n // Skip this file (consume but don't store) for 'skip' and 'warn' modes\n // SECURITY: Still count bytes to prevent zip bombs hidden behind unsafe paths\n file.ondata = (err, chunk, _final) => {\n if (error) return;\n if (err) {\n error = err;\n return;\n }\n if (chunk && chunk.length > 0) {\n totalBytes += chunk.length;\n if (totalBytes > limits.maxTotalSize) {\n error = new ZipPreflightError(\n `Total actual size ${totalBytes} exceeds limit ${limits.maxTotalSize}`,\n totalBytes,\n limits.maxTotalSize\n );\n file.terminate();\n }\n }\n };\n file.start();\n return;\n }\n\n fileCount++;\n if (fileCount > limits.maxFiles) {\n error = new ZipPreflightError(\n `File count ${fileCount} exceeds limit ${limits.maxFiles}`\n );\n file.terminate();\n return;\n }\n\n const chunks: Uint8Array[] = [];\n fileChunks.set(file.name, chunks);\n let fileBytes = 0;\n\n file.ondata = (err, chunk, final) => {\n if (error) return;\n\n if (err) {\n error = err;\n return;\n }\n\n if (chunk && chunk.length > 0) {\n fileBytes += chunk.length;\n totalBytes += chunk.length;\n\n // Check per-file size limit (actual decompressed bytes)\n if (fileBytes > limits.maxFileSize) {\n error = new ZipPreflightError(\n `File \"${file.name}\" actual size ${fileBytes} exceeds limit ${limits.maxFileSize}`,\n undefined,\n undefined,\n file.name,\n fileBytes,\n limits.maxFileSize\n );\n file.terminate();\n return;\n }\n\n // Check total size limit (actual decompressed bytes)\n if (totalBytes > limits.maxTotalSize) {\n error = new ZipPreflightError(\n `Total actual size ${totalBytes} exceeds limit ${limits.maxTotalSize}`,\n totalBytes,\n limits.maxTotalSize\n );\n file.terminate();\n return;\n }\n\n chunks.push(chunk);\n }\n\n if (final && !error) {\n // Concatenate all chunks for this file\n result[file.name] = concat(...chunks);\n }\n };\n\n file.start();\n });\n\n // Register decompression handlers\n unzipper.register(UnzipInflate); // DEFLATE (compression method 8)\n unzipper.register(UnzipPassThrough); // Stored (compression method 0)\n\n // Push all data - fflate processes synchronously when given full buffer\n unzipper.push(zipData, true);\n\n // If an error occurred during processing, throw it\n if (error) {\n throw error;\n }\n\n return result;\n}\n\n/**\n * Re-export Unzipped type for convenience\n */\nexport type { Unzipped };\n","/**\n * Common Types\n *\n * Shared types used across all card formats.\n */\n\nimport { z } from 'zod';\n\n// ============================================================================\n// Preprocessing Utilities\n// ============================================================================\n\n/**\n * Preprocess timestamp values to Unix seconds.\n * Handles: ISO strings, numeric strings, milliseconds, and numbers.\n * Returns undefined for invalid/negative values (defensive).\n */\nexport function preprocessTimestamp(val: unknown): number | undefined {\n if (val === null || val === undefined) return undefined;\n\n let num: number;\n\n if (typeof val === 'number') {\n num = val;\n } else if (typeof val === 'string') {\n const trimmed = val.trim();\n if (!trimmed) return undefined;\n\n // Try parsing as number first (numeric string like \"1705314600\")\n const parsed = Number(trimmed);\n if (!isNaN(parsed)) {\n num = parsed;\n } else {\n // Try parsing as ISO date string\n const date = new Date(trimmed);\n if (isNaN(date.getTime())) return undefined;\n num = Math.floor(date.getTime() / 1000);\n }\n } else {\n return undefined;\n }\n\n // Convert milliseconds to seconds if needed (>10 billion = likely ms)\n if (num > 10_000_000_000) {\n num = Math.floor(num / 1000);\n }\n\n // Reject negative timestamps (e.g., .NET default dates)\n if (num < 0) return undefined;\n\n return num;\n}\n\n/**\n * Preprocess numeric values that may come as strings.\n * Returns undefined for invalid values.\n */\nexport function preprocessNumeric(val: unknown): number | undefined {\n if (val === null || val === undefined) return undefined;\n\n if (typeof val === 'number') {\n return isNaN(val) ? undefined : val;\n }\n\n if (typeof val === 'string') {\n const trimmed = val.trim();\n if (!trimmed) return undefined;\n const parsed = Number(trimmed);\n return isNaN(parsed) ? undefined : parsed;\n }\n\n return undefined;\n}\n\n/**\n * Known asset types for coercion\n */\nconst KNOWN_ASSET_TYPES = new Set([\n 'icon', 'background', 'emotion', 'user_icon',\n 'sound', 'video', 'custom', 'x-risu-asset',\n]);\n\n/**\n * Preprocess asset type - coerce unknown types to 'custom'.\n */\nexport function preprocessAssetType(val: unknown): string {\n if (typeof val !== 'string') return 'custom';\n return KNOWN_ASSET_TYPES.has(val) ? val : 'custom';\n}\n\n// ============================================================================\n// Zod Schemas\n// ============================================================================\n\n/**\n * ISO 8601 date string schema\n */\nexport const ISO8601Schema = z.string().datetime();\n\n/**\n * UUID string schema\n */\nexport const UUIDSchema = z.string().uuid();\n\n/**\n * Card specification version schema\n */\nexport const SpecSchema = z.enum(['v2', 'v3']);\n\n/**\n * Source format identifier schema\n */\nexport const SourceFormatSchema = z.enum([\n 'png_v2', // PNG with 'chara' chunk (v2)\n 'png_v3', // PNG with 'ccv3' chunk (v3)\n 'json_v2', // Raw JSON v2\n 'json_v3', // Raw JSON v3\n 'charx', // ZIP with card.json (v3 spec)\n 'charx_risu', // ZIP with card.json + module.risum\n 'charx_jpeg', // JPEG with appended ZIP (read-only)\n 'voxta', // VoxPkg format\n]);\n\n/**\n * Original JSON shape schema\n */\nexport const OriginalShapeSchema = z.enum(['wrapped', 'unwrapped', 'legacy']);\n\n/**\n * Asset type identifier schema.\n * Uses preprocessing to coerce unknown types to 'custom' for forward compatibility.\n */\nexport const AssetTypeSchema = z.preprocess(\n preprocessAssetType,\n z.enum([\n 'icon',\n 'background',\n 'emotion',\n 'user_icon',\n 'sound',\n 'video',\n 'custom',\n 'x-risu-asset',\n ])\n);\n\n/**\n * Asset descriptor schema (v3 spec)\n */\nexport const AssetDescriptorSchema = z.object({\n type: AssetTypeSchema,\n uri: z.string(),\n name: z.string(),\n ext: z.string(),\n});\n\n/**\n * Extracted asset with binary data schema\n */\nexport const ExtractedAssetSchema = z.object({\n descriptor: AssetDescriptorSchema,\n data: z.instanceof(Uint8Array),\n mimeType: z.string(),\n});\n\n// ============================================================================\n// TypeScript Types (inferred from Zod schemas)\n// ============================================================================\n\n/**\n * ISO 8601 date string\n */\nexport type ISO8601 = z.infer<typeof ISO8601Schema>;\n\n/**\n * UUID string\n */\nexport type UUID = z.infer<typeof UUIDSchema>;\n\n/**\n * Card specification version\n */\nexport type Spec = z.infer<typeof SpecSchema>;\n\n/**\n * Source format identifier\n */\nexport type SourceFormat = z.infer<typeof SourceFormatSchema>;\n\n/**\n * Original JSON shape\n */\nexport type OriginalShape = z.infer<typeof OriginalShapeSchema>;\n\n/**\n * Asset type identifier\n */\nexport type AssetType = z.infer<typeof AssetTypeSchema>;\n\n/**\n * Asset descriptor (v3 spec)\n */\nexport type AssetDescriptor = z.infer<typeof AssetDescriptorSchema>;\n\n/**\n * Extracted asset with binary data\n */\nexport type ExtractedAsset = z.infer<typeof ExtractedAssetSchema>;\n","/**\n * Character Card v2 Types\n *\n * Based on: https://github.com/malfoyslastname/character-card-spec-v2\n */\n\nimport { z } from 'zod';\nimport { preprocessNumeric } from './common.js';\n\n// ============================================================================\n// Zod Schemas\n// ============================================================================\n\n/**\n * Lorebook entry schema for v2 cards\n */\nexport const CCv2LorebookEntrySchema = z.object({\n keys: z.array(z.string()).optional(), // Some tools use 'key' instead\n content: z.string(),\n enabled: z.boolean().default(true), // Default to enabled if missing\n insertion_order: z.preprocess((v) => v ?? 0, z.number().int()),\n // Optional fields - be lenient with nulls since wild data has them\n extensions: z.record(z.unknown()).optional(),\n case_sensitive: z.boolean().nullable().optional(),\n name: z.string().optional(),\n priority: z.number().int().nullable().optional(),\n id: z.number().int().nullable().optional(),\n comment: z.string().nullable().optional(),\n selective: z.boolean().nullable().optional(),\n secondary_keys: z.array(z.string()).nullable().optional(),\n constant: z.boolean().nullable().optional(),\n position: z.union([z.enum(['before_char', 'after_char', 'in_chat']), z.number().int(), z.literal('')]).nullable().optional(),\n}).passthrough(); // Allow SillyTavern extensions like depth, probability, etc.\n\n/**\n * Character book (lorebook) schema for v2 cards.\n * Uses preprocessing for numeric fields that often come as strings in wild data.\n */\nexport const CCv2CharacterBookSchema = z.object({\n name: z.string().optional(),\n description: z.string().optional(),\n scan_depth: z.preprocess(preprocessNumeric, z.number().int().nonnegative().optional()),\n token_budget: z.preprocess(preprocessNumeric, z.number().int().nonnegative().optional()),\n recursive_scanning: z.boolean().optional(),\n extensions: z.record(z.unknown()).optional(),\n entries: z.array(CCv2LorebookEntrySchema),\n});\n\n/**\n * Character Card v2 data structure schema\n */\nexport const CCv2DataSchema = z.object({\n // Core fields - use .default('') to handle missing fields in malformed cards\n name: z.string().default(''),\n description: z.string().default(''),\n personality: z.string().nullable().default(''), // Can be null in wild (141 cards)\n scenario: z.string().default(''),\n first_mes: z.string().default(''),\n mes_example: z.string().nullable().default(''), // Can be null in wild (186 cards)\n // Optional fields\n creator_notes: z.string().optional(),\n system_prompt: z.string().optional(),\n post_history_instructions: z.string().optional(),\n alternate_greetings: z.array(z.string()).optional(),\n character_book: CCv2CharacterBookSchema.optional().nullable(),\n tags: z.array(z.string()).optional(),\n creator: z.string().optional(),\n character_version: z.string().optional(),\n extensions: z.record(z.unknown()).optional(),\n});\n\n/**\n * Wrapped v2 card format schema (modern tools)\n */\nexport const CCv2WrappedSchema = z.object({\n spec: z.literal('chara_card_v2'),\n spec_version: z.literal('2.0'),\n data: CCv2DataSchema,\n});\n\n// ============================================================================\n// TypeScript Types (inferred from Zod schemas)\n// ============================================================================\n\n/**\n * Lorebook entry for v2 cards\n */\nexport type CCv2LorebookEntry = z.infer<typeof CCv2LorebookEntrySchema>;\n\n/**\n * Character book (lorebook) for v2 cards\n */\nexport type CCv2CharacterBook = z.infer<typeof CCv2CharacterBookSchema>;\n\n/**\n * Character Card v2 data structure\n */\nexport type CCv2Data = z.infer<typeof CCv2DataSchema>;\n\n/**\n * Wrapped v2 card format (modern tools)\n */\nexport type CCv2Wrapped = z.infer<typeof CCv2WrappedSchema>;\n\n// ============================================================================\n// Type Guards & Parsers\n// ============================================================================\n\n/**\n * Check if data is a wrapped v2 card\n */\nexport function isWrappedV2(data: unknown): data is CCv2Wrapped {\n return CCv2WrappedSchema.safeParse(data).success;\n}\n\n/**\n * Check if data looks like v2 card data (wrapped or unwrapped)\n */\nexport function isV2CardData(data: unknown): data is CCv2Data | CCv2Wrapped {\n return (\n CCv2WrappedSchema.safeParse(data).success ||\n CCv2DataSchema.safeParse(data).success\n );\n}\n\n/**\n * Parse and validate a wrapped v2 card\n */\nexport function parseWrappedV2(data: unknown): CCv2Wrapped {\n return CCv2WrappedSchema.parse(data);\n}\n\n/**\n * Parse and validate v2 card data\n */\nexport function parseV2Data(data: unknown): CCv2Data {\n return CCv2DataSchema.parse(data);\n}\n\n/**\n * Check if data looks like a wrapped V2 card structurally (without strict validation).\n * This is more lenient than isWrappedV2 - it just checks structure, not full schema validity.\n */\nexport function looksLikeWrappedV2(data: unknown): data is { spec: string; data: Record<string, unknown> } {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n return (\n obj.spec === 'chara_card_v2' &&\n obj.data !== null &&\n typeof obj.data === 'object'\n );\n}\n\n/**\n * Get v2 card data from wrapped or unwrapped format.\n *\n * Uses structural check instead of strict Zod validation to handle\n * malformed cards that have the right structure but missing/invalid fields.\n * The caller (e.g., ccv2ToCCv3) handles defaulting missing fields.\n */\nexport function getV2Data(card: CCv2Data | CCv2Wrapped): CCv2Data {\n // Use structural check - more lenient than isWrappedV2 schema validation\n if (looksLikeWrappedV2(card)) {\n return card.data as CCv2Data;\n }\n return card;\n}\n","/**\n * Character Card v3 Types\n *\n * Based on: https://github.com/kwaroran/character-card-spec-v3\n */\n\nimport { z } from 'zod';\nimport { AssetDescriptorSchema, preprocessTimestamp, preprocessNumeric } from './common.js';\n\n// ============================================================================\n// Zod Schemas\n// ============================================================================\n\n/**\n * Lorebook entry schema for v3 cards\n */\nexport const CCv3LorebookEntrySchema = z.object({\n keys: z.array(z.string()).optional(), // Some tools use 'key' instead\n content: z.string(),\n enabled: z.boolean().default(true), // Default to enabled if missing\n insertion_order: z.preprocess((v) => v ?? 0, z.number().int()),\n // Optional fields - be lenient with nulls since wild data has them\n case_sensitive: z.boolean().nullable().optional(),\n name: z.string().optional(),\n priority: z.number().int().nullable().optional(),\n id: z.number().int().nullable().optional(),\n comment: z.string().nullable().optional(),\n selective: z.boolean().nullable().optional(),\n secondary_keys: z.array(z.string()).nullable().optional(),\n constant: z.boolean().nullable().optional(),\n position: z.union([z.enum(['before_char', 'after_char', 'in_chat']), z.number().int(), z.literal('')]).nullable().optional(),\n extensions: z.record(z.unknown()).optional(),\n // v3 specific - also lenient with types since SillyTavern uses numbers for enums\n automation_id: z.string().optional(),\n role: z.union([z.enum(['system', 'user', 'assistant']), z.number().int()]).nullable().optional(),\n group: z.string().optional(),\n scan_frequency: z.number().int().nonnegative().optional(),\n probability: z.number().min(0).max(100).optional(), // Some tools use 0-100 instead of 0-1\n use_regex: z.boolean().optional(),\n depth: z.number().int().nonnegative().optional(),\n selective_logic: z.union([z.enum(['AND', 'NOT']), z.number().int()]).optional(),\n}).passthrough(); // Allow tool-specific extensions\n\n/**\n * Character book (lorebook) schema for v3 cards.\n * Uses preprocessing for numeric fields that often come as strings in wild data.\n */\nexport const CCv3CharacterBookSchema = z.object({\n name: z.string().optional(),\n description: z.string().optional(),\n scan_depth: z.preprocess(preprocessNumeric, z.number().int().nonnegative().optional()),\n token_budget: z.preprocess(preprocessNumeric, z.number().int().nonnegative().optional()),\n recursive_scanning: z.boolean().optional(),\n extensions: z.record(z.unknown()).optional(),\n entries: z.array(CCv3LorebookEntrySchema),\n});\n\n/**\n * Character Card v3 inner data structure schema.\n *\n * Note: Fields like group_only_greetings, creator, character_version, and tags\n * are technically \"required\" per V3 spec but rarely present in wild cards.\n * We use .default() to make parsing lenient while still producing valid output.\n */\nexport const CCv3DataInnerSchema = z.object({\n // Core fields - use .default('') to handle missing fields in malformed cards\n name: z.string().default(''),\n description: z.string().default(''),\n personality: z.string().nullable().default(''), // Can be null in wild (141 cards)\n scenario: z.string().default(''),\n first_mes: z.string().default(''),\n mes_example: z.string().nullable().default(''), // Can be null in wild (186 cards)\n // \"Required\" per spec but often missing in wild - use defaults for leniency\n creator: z.string().default(''),\n character_version: z.string().default(''),\n tags: z.array(z.string()).default([]),\n group_only_greetings: z.array(z.string()).default([]),\n // Optional fields\n creator_notes: z.string().optional(),\n system_prompt: z.string().optional(),\n post_history_instructions: z.string().optional(),\n alternate_greetings: z.array(z.string()).optional(),\n character_book: CCv3CharacterBookSchema.optional().nullable(),\n extensions: z.record(z.unknown()).optional(),\n // v3 specific\n assets: z.array(AssetDescriptorSchema).optional(),\n nickname: z.string().optional(),\n creator_notes_multilingual: z.record(z.string()).optional(),\n source: z.array(z.string()).optional(),\n // Unix timestamps - preprocess to handle ISO strings, numeric strings, milliseconds\n creation_date: z.preprocess(preprocessTimestamp, z.number().int().nonnegative().optional()),\n modification_date: z.preprocess(preprocessTimestamp, z.number().int().nonnegative().optional()),\n});\n\n/**\n * Character Card v3 full structure schema\n */\nexport const CCv3DataSchema = z.object({\n spec: z.literal('chara_card_v3'),\n spec_version: z.literal('3.0'),\n data: CCv3DataInnerSchema,\n});\n\n// ============================================================================\n// TypeScript Types (inferred from Zod schemas)\n// ============================================================================\n\n/**\n * Lorebook entry for v3 cards\n */\nexport type CCv3LorebookEntry = z.infer<typeof CCv3LorebookEntrySchema>;\n\n/**\n * Character book (lorebook) for v3 cards\n */\nexport type CCv3CharacterBook = z.infer<typeof CCv3CharacterBookSchema>;\n\n/**\n * Character Card v3 inner data structure\n */\nexport type CCv3DataInner = z.infer<typeof CCv3DataInnerSchema>;\n\n/**\n * Character Card v3 full structure\n */\nexport type CCv3Data = z.infer<typeof CCv3DataSchema>;\n\n// ============================================================================\n// Type Guards & Parsers\n// ============================================================================\n\n/**\n * Check if data is a v3 card\n */\nexport function isV3Card(data: unknown): data is CCv3Data {\n return CCv3DataSchema.safeParse(data).success;\n}\n\n/**\n * Parse and validate a v3 card\n */\nexport function parseV3Card(data: unknown): CCv3Data {\n return CCv3DataSchema.parse(data);\n}\n\n/**\n * Parse and validate v3 card inner data\n */\nexport function parseV3DataInner(data: unknown): CCv3DataInner {\n return CCv3DataInnerSchema.parse(data);\n}\n\n/**\n * Get v3 card inner data\n */\nexport function getV3Data(card: CCv3Data): CCv3DataInner {\n return card.data;\n}\n\n/**\n * Check if data looks like a V3 card structurally (without strict validation).\n * More lenient than isV3Card - just checks structure, not full schema validity.\n */\nexport function looksLikeV3Card(data: unknown): data is { spec: string; data: Record<string, unknown> } {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n return (\n obj.spec === 'chara_card_v3' &&\n obj.data !== null &&\n typeof obj.data === 'object'\n );\n}\n","/**\n * RisuAI Extension Types\n *\n * These extensions are preserved as opaque blobs.\n * We do NOT interpret or transform the script contents.\n */\n\n/**\n * Risu emotions mapping (v2 style)\n * Format: [name, uri][]\n */\nexport type RisuEmotions = [string, string][];\n\n/**\n * Risu additional assets (v3 style)\n * Format: [name, uri, type][]\n */\nexport type RisuAdditionalAssets = [string, string, string][];\n\n/**\n * Risu depth prompt configuration\n */\nexport interface RisuDepthPrompt {\n depth: number;\n prompt: string;\n}\n\n/**\n * Risu extensions in card.extensions.risuai\n * Preserved as opaque - we don't interpret script contents\n */\nexport interface RisuExtensions {\n // Emotion assets\n emotions?: RisuEmotions;\n additionalAssets?: RisuAdditionalAssets;\n\n // Script data - OPAQUE, do not parse\n triggerscript?: unknown;\n customScripts?: unknown;\n\n // Voice/TTS settings\n vits?: Record<string, string>;\n\n // Depth prompt\n depth_prompt?: RisuDepthPrompt;\n\n // Other Risu-specific fields\n [key: string]: unknown;\n}\n\n/**\n * CharX x_meta entry (PNG chunk metadata preservation)\n */\nexport interface CharxMetaEntry {\n type?: string; // e.g., 'WEBP', 'PNG', 'JPEG'\n [key: string]: unknown;\n}\n\n/**\n * Check if card has Risu extensions\n */\nexport function hasRisuExtensions(extensions?: Record<string, unknown>): boolean {\n if (!extensions) return false;\n return 'risuai' in extensions || 'risu' in extensions;\n}\n\n/**\n * Check if card has Risu scripts (triggerscript or customScripts)\n */\nexport function hasRisuScripts(extensions?: Record<string, unknown>): boolean {\n if (!extensions) return false;\n const risu = extensions.risuai as RisuExtensions | undefined;\n if (!risu) return false;\n return !!risu.triggerscript || !!risu.customScripts;\n}\n\n/**\n * Check if card has depth prompt\n * Checks both SillyTavern style (extensions.depth_prompt) and Risu style (extensions.risuai.depth_prompt)\n */\nexport function hasDepthPrompt(extensions?: Record<string, unknown>): boolean {\n if (!extensions) return false;\n // SillyTavern top-level depth_prompt\n if ('depth_prompt' in extensions && extensions.depth_prompt) return true;\n // Risu-style depth_prompt\n const risu = extensions.risuai as RisuExtensions | undefined;\n return !!risu?.depth_prompt;\n}\n","/**\n * Normalized Card Types\n *\n * Unified view of card data regardless of source format.\n * This is a computed/virtual representation, not stored.\n */\n\nimport type { CCv3CharacterBook } from './ccv3.js';\n\n/**\n * Normalized card representation\n * Provides unified access to card data from any format\n */\nexport interface NormalizedCard {\n // Core fields (always present)\n name: string;\n description: string;\n personality: string;\n scenario: string;\n firstMes: string;\n mesExample: string;\n\n // Optional prompts\n systemPrompt?: string;\n postHistoryInstructions?: string;\n\n // Arrays\n alternateGreetings: string[];\n groupOnlyGreetings: string[];\n tags: string[];\n\n // Metadata\n creator?: string;\n creatorNotes?: string;\n characterVersion?: string;\n\n // Character book (v3 format)\n characterBook?: CCv3CharacterBook;\n\n // Extensions (preserved as-is)\n extensions: Record<string, unknown>;\n}\n\n/**\n * Create empty normalized card with defaults\n */\nexport function createEmptyNormalizedCard(): NormalizedCard {\n return {\n name: '',\n description: '',\n personality: '',\n scenario: '',\n firstMes: '',\n mesExample: '',\n alternateGreetings: [],\n groupOnlyGreetings: [],\n tags: [],\n extensions: {},\n };\n}\n\n/**\n * Derived features extracted from card (not stored in card)\n */\nexport interface DerivedFeatures {\n // Content flags\n hasAlternateGreetings: boolean;\n alternateGreetingsCount: number;\n /** Total greetings = first_mes (1) + alternate_greetings */\n totalGreetingsCount: number;\n hasLorebook: boolean;\n lorebookEntriesCount: number;\n hasEmbeddedImages: boolean;\n embeddedImagesCount: number;\n hasGallery: boolean;\n\n // Format-specific\n hasRisuExtensions: boolean;\n hasRisuScripts: boolean;\n hasDepthPrompt: boolean;\n hasVoxtaAppearance: boolean;\n\n // Token counts (estimated)\n tokens: {\n description: number;\n personality: number;\n scenario: number;\n firstMes: number;\n mesExample: number;\n systemPrompt: number;\n total: number;\n };\n}\n\n/**\n * Create empty derived features\n */\nexport function createEmptyFeatures(): DerivedFeatures {\n return {\n hasAlternateGreetings: false,\n alternateGreetingsCount: 0,\n totalGreetingsCount: 1, // first_mes always counts as 1\n hasLorebook: false,\n lorebookEntriesCount: 0,\n hasEmbeddedImages: false,\n embeddedImagesCount: 0,\n hasGallery: false,\n hasRisuExtensions: false,\n hasRisuScripts: false,\n hasDepthPrompt: false,\n hasVoxtaAppearance: false,\n tokens: {\n description: 0,\n personality: 0,\n scenario: 0,\n firstMes: 0,\n mesExample: 0,\n systemPrompt: 0,\n total: 0,\n },\n };\n}\n","/**\n * Feature Derivation\n *\n * Canonical feature extraction from character cards.\n * Eliminates duplicate implementations across Archive, Federation, and Architect.\n */\n\nimport type { CCv2Data } from './ccv2.js';\nimport type { CCv3DataInner } from './ccv3.js';\nimport type { DerivedFeatures } from './normalized.js';\nimport { hasRisuExtensions, hasRisuScripts, hasDepthPrompt } from './risu.js';\n\n/**\n * Derive features from a character card (V2 or V3 format).\n *\n * This is the canonical implementation - all apps should use this\n * rather than implementing their own feature detection.\n *\n * @param card - Either CCv2Data or CCv3DataInner (unwrapped)\n * @returns DerivedFeatures with all feature flags populated\n *\n * @example\n * ```typescript\n * import { deriveFeatures, parseV3Card } from '@character-foundry/schemas';\n *\n * const card = parseV3Card(data);\n * const features = deriveFeatures(card.data);\n *\n * if (features.hasLorebook) {\n * console.log(`Found ${features.lorebookEntriesCount} lorebook entries`);\n * }\n * ```\n */\nexport function deriveFeatures(card: CCv2Data | CCv3DataInner): DerivedFeatures {\n // Detect format by checking for V3-specific field\n const isV3 = 'assets' in card;\n\n // Alternate greetings\n const altGreetings = card.alternate_greetings ?? [];\n const hasAlternateGreetings = altGreetings.length > 0;\n const alternateGreetingsCount = altGreetings.length;\n // Total = first_mes (1) + alternate_greetings\n const totalGreetingsCount = 1 + alternateGreetingsCount;\n\n // Lorebook\n const characterBook = card.character_book;\n const hasLorebook = !!characterBook && characterBook.entries.length > 0;\n const lorebookEntriesCount = characterBook?.entries.length ?? 0;\n\n // Assets (V3 only) - check for visual asset types\n const assets = isV3 ? (card as CCv3DataInner).assets ?? [] : [];\n const imageAssetTypes = ['icon', 'background', 'emotion', 'custom'];\n const imageAssets = assets.filter(\n (a) =>\n imageAssetTypes.includes(a.type) ||\n ['png', 'jpg', 'jpeg', 'webp', 'gif'].includes(a.ext.toLowerCase()),\n );\n const hasGallery = imageAssets.length > 0;\n\n // Embedded images - check for data URLs in text fields\n const embeddedImageCount = countEmbeddedImages(card);\n const hasEmbeddedImages = embeddedImageCount > 0;\n\n // Extensions\n const extensions = card.extensions ?? {};\n const hasRisu = hasRisuExtensions(extensions);\n const hasScripts = hasRisuScripts(extensions);\n const hasDepth = hasDepthPrompt(extensions);\n const hasVoxta = checkVoxtaAppearance(extensions);\n\n // Token counts - initialize to zero (actual counting happens in tokenizers package)\n const tokens = {\n description: 0,\n personality: 0,\n scenario: 0,\n firstMes: 0,\n mesExample: 0,\n systemPrompt: 0,\n total: 0,\n };\n\n return {\n hasAlternateGreetings,\n alternateGreetingsCount,\n totalGreetingsCount,\n hasLorebook,\n lorebookEntriesCount,\n hasEmbeddedImages,\n embeddedImagesCount: embeddedImageCount,\n hasGallery,\n hasRisuExtensions: hasRisu,\n hasRisuScripts: hasScripts,\n hasDepthPrompt: hasDepth,\n hasVoxtaAppearance: hasVoxta,\n tokens,\n };\n}\n\n/**\n * Count embedded images (data URLs) in card text fields.\n * Looks for base64-encoded images in description, personality, scenario, etc.\n */\nfunction countEmbeddedImages(card: CCv2Data | CCv3DataInner): number {\n const textFields = [\n card.description,\n card.personality,\n card.scenario,\n card.first_mes,\n card.mes_example,\n card.creator_notes,\n card.system_prompt,\n card.post_history_instructions,\n ...(card.alternate_greetings ?? []),\n ].filter((field): field is string => typeof field === 'string');\n\n // Add group_only_greetings if V3\n if ('group_only_greetings' in card) {\n textFields.push(...(card.group_only_greetings ?? []));\n }\n\n let count = 0;\n const dataUrlPattern = /data:image\\/[^;]+;base64,/g;\n\n for (const text of textFields) {\n const matches = text.match(dataUrlPattern);\n if (matches) {\n count += matches.length;\n }\n }\n\n return count;\n}\n\n/**\n * Check if card has Voxta appearance data.\n * Voxta stores appearance in extensions.voxta.appearance\n */\nfunction checkVoxtaAppearance(extensions: Record<string, unknown>): boolean {\n if (!extensions.voxta) return false;\n const voxta = extensions.voxta as Record<string, unknown>;\n return !!voxta.appearance;\n}\n","/**\n * Format Detection\n *\n * Detect card specification version from JSON data.\n */\n\nimport type { Spec } from './common.js';\n\n/**\n * V3-only fields that indicate a V3 card\n */\nconst V3_ONLY_FIELDS = ['group_only_greetings', 'creation_date', 'modification_date', 'assets'] as const;\n\n/**\n * Result from detailed spec detection\n */\nexport interface SpecDetectionResult {\n /** Detected spec version */\n spec: Spec | null;\n /** Confidence level of detection */\n confidence: 'high' | 'medium' | 'low';\n /** What fields/values indicated this spec */\n indicators: string[];\n /** Anomalies or inconsistencies detected */\n warnings: string[];\n}\n\n/**\n * Detect card spec version from parsed JSON\n * Returns 'v2', 'v3', or null if not recognized\n */\nexport function detectSpec(data: unknown): Spec | null {\n return detectSpecDetailed(data).spec;\n}\n\n/**\n * Detailed spec detection with confidence and reasoning.\n * Useful for debugging and logging.\n */\nexport function detectSpecDetailed(data: unknown): SpecDetectionResult {\n const result: SpecDetectionResult = {\n spec: null,\n confidence: 'low',\n indicators: [],\n warnings: [],\n };\n\n if (!data || typeof data !== 'object') {\n result.indicators.push('Input is not an object');\n return result;\n }\n\n const obj = data as Record<string, unknown>;\n const dataObj = (obj.data && typeof obj.data === 'object' ? obj.data : null) as Record<\n string,\n unknown\n > | null;\n\n // Check for explicit spec markers (HIGH confidence)\n\n // Explicit v3 spec marker\n if (obj.spec === 'chara_card_v3') {\n result.spec = 'v3';\n result.confidence = 'high';\n result.indicators.push('spec field is \"chara_card_v3\"');\n\n // Check for inconsistencies\n if (obj.spec_version && obj.spec_version !== '3.0') {\n result.warnings.push(`spec_version \"${obj.spec_version}\" inconsistent with v3 spec`);\n }\n\n return result;\n }\n\n // Explicit v2 spec marker\n if (obj.spec === 'chara_card_v2') {\n result.spec = 'v2';\n result.confidence = 'high';\n result.indicators.push('spec field is \"chara_card_v2\"');\n\n // Check for inconsistencies - V3-only fields in V2 card\n if (dataObj) {\n for (const field of V3_ONLY_FIELDS) {\n if (field in dataObj) {\n result.warnings.push(`V3-only field \"${field}\" found in V2 card`);\n }\n }\n }\n\n if (obj.spec_version && obj.spec_version !== '2.0') {\n result.warnings.push(`spec_version \"${obj.spec_version}\" inconsistent with v2 spec`);\n }\n\n return result;\n }\n\n // Check spec_version field (HIGH confidence)\n if (typeof obj.spec_version === 'string') {\n if (obj.spec_version.startsWith('3')) {\n result.spec = 'v3';\n result.confidence = 'high';\n result.indicators.push(`spec_version \"${obj.spec_version}\" starts with \"3\"`);\n return result;\n }\n if (obj.spec_version.startsWith('2')) {\n result.spec = 'v2';\n result.confidence = 'high';\n result.indicators.push(`spec_version \"${obj.spec_version}\" starts with \"2\"`);\n return result;\n }\n }\n\n if (obj.spec_version === 2.0 || obj.spec_version === 2) {\n result.spec = 'v2';\n result.confidence = 'high';\n result.indicators.push(`spec_version is numeric ${obj.spec_version}`);\n return result;\n }\n\n // Check for V3-only fields (MEDIUM confidence)\n if (dataObj) {\n const v3Fields: string[] = [];\n for (const field of V3_ONLY_FIELDS) {\n if (field in dataObj) {\n v3Fields.push(field);\n }\n }\n\n if (v3Fields.length > 0) {\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push(`Has V3-only fields: ${v3Fields.join(', ')}`);\n return result;\n }\n }\n\n // Check root level for V3-only fields (also MEDIUM confidence)\n const rootV3Fields: string[] = [];\n for (const field of V3_ONLY_FIELDS) {\n if (field in obj) {\n rootV3Fields.push(field);\n }\n }\n if (rootV3Fields.length > 0) {\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push(`Has V3-only fields at root: ${rootV3Fields.join(', ')}`);\n result.warnings.push('V3 fields found at root level instead of data object');\n return result;\n }\n\n // Wrapped format with data object (MEDIUM confidence)\n if (obj.spec && dataObj) {\n const dataName = dataObj.name;\n if (dataName && typeof dataName === 'string') {\n // Infer from spec string\n if (typeof obj.spec === 'string') {\n if (obj.spec.includes('v3') || obj.spec.includes('3')) {\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push(`spec field \"${obj.spec}\" contains \"v3\" or \"3\"`);\n return result;\n }\n if (obj.spec.includes('v2') || obj.spec.includes('2')) {\n result.spec = 'v2';\n result.confidence = 'medium';\n result.indicators.push(`spec field \"${obj.spec}\" contains \"v2\" or \"2\"`);\n return result;\n }\n }\n // Default wrapped format to v3 (modern)\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push('Has wrapped format with spec and data.name');\n return result;\n }\n }\n\n // Unwrapped format - V1/V2 like structure (MEDIUM confidence)\n if (obj.name && typeof obj.name === 'string') {\n if ('description' in obj || 'personality' in obj || 'scenario' in obj) {\n result.spec = 'v2';\n result.confidence = 'medium';\n result.indicators.push('Unwrapped format with name, description/personality/scenario');\n return result;\n }\n }\n\n // Check if data object has card-like structure without spec (LOW confidence)\n if (dataObj && typeof dataObj.name === 'string') {\n if ('description' in dataObj || 'personality' in dataObj) {\n result.spec = 'v2';\n result.confidence = 'low';\n result.indicators.push('Has data object with name and card fields, but no spec');\n result.warnings.push('Missing spec field');\n return result;\n }\n }\n\n result.indicators.push('No card structure detected');\n return result;\n}\n\n/**\n * Check if card has a lorebook\n */\nexport function hasLorebook(data: unknown): boolean {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n\n // Check wrapped format\n const wrapped = obj.data as Record<string, unknown> | undefined;\n if (wrapped?.character_book) {\n const book = wrapped.character_book as Record<string, unknown>;\n if (Array.isArray(book.entries) && book.entries.length > 0) return true;\n }\n\n // Check unwrapped format\n if (obj.character_book) {\n const book = obj.character_book as Record<string, unknown>;\n if (Array.isArray(book.entries) && book.entries.length > 0) return true;\n }\n\n return false;\n}\n\n/**\n * Check if data looks like a valid card structure\n */\nexport function looksLikeCard(data: unknown): boolean {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n\n // Has explicit spec marker\n if (obj.spec === 'chara_card_v2' || obj.spec === 'chara_card_v3') {\n return true;\n }\n\n // Has wrapped data with name\n if (obj.data && typeof obj.data === 'object') {\n const dataObj = obj.data as Record<string, unknown>;\n if (typeof dataObj.name === 'string' && dataObj.name.length > 0) {\n return true;\n }\n }\n\n // Has unwrapped card-like structure\n if (typeof obj.name === 'string' && obj.name.length > 0) {\n if ('description' in obj || 'personality' in obj || 'first_mes' in obj) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Card Normalizer\n *\n * Handles normalization of malformed card data from various sources.\n * Fixes common issues like wrong spec values, misplaced fields, missing required fields.\n */\n\nimport type { CCv2Data, CCv2Wrapped, CCv2CharacterBook, CCv2LorebookEntry } from './ccv2.js';\nimport type { CCv3Data, CCv3CharacterBook, CCv3LorebookEntry } from './ccv3.js';\nimport { detectSpec } from './detection.js';\n\n/**\n * Position values as numbers (non-standard) and their string equivalents\n */\nconst POSITION_MAP: Record<number, 'before_char' | 'after_char'> = {\n 0: 'before_char',\n 1: 'after_char',\n};\n\n/**\n * V3-only lorebook entry fields that should be moved to extensions for V2\n */\nconst V3_ONLY_ENTRY_FIELDS = [\n 'probability',\n 'depth',\n 'group',\n 'scan_frequency',\n 'use_regex',\n 'selective_logic',\n 'role',\n 'automation_id',\n] as const;\n\n/**\n * Required V2 card fields with their defaults\n */\nconst V2_REQUIRED_DEFAULTS: Partial<CCv2Data> = {\n name: '',\n description: '',\n personality: '',\n scenario: '',\n first_mes: '',\n mes_example: '',\n};\n\n/**\n * Required V3 card fields with their defaults\n */\nconst V3_REQUIRED_DEFAULTS: Partial<CCv3Data['data']> = {\n name: '',\n description: '',\n personality: '',\n scenario: '',\n first_mes: '',\n mes_example: '',\n creator: '',\n character_version: '1.0',\n tags: [],\n group_only_greetings: [],\n};\n\n/**\n * Fields that belong at root level for wrapped format\n */\nconst _ROOT_FIELDS = ['spec', 'spec_version', 'data'] as const;\n\n/**\n * Fields that belong in the data object\n */\nconst DATA_FIELDS = [\n 'name',\n 'description',\n 'personality',\n 'scenario',\n 'first_mes',\n 'mes_example',\n 'creator_notes',\n 'system_prompt',\n 'post_history_instructions',\n 'alternate_greetings',\n 'character_book',\n 'tags',\n 'creator',\n 'character_version',\n 'extensions',\n 'assets',\n 'nickname',\n 'creator_notes_multilingual',\n 'source',\n 'creation_date',\n 'modification_date',\n 'group_only_greetings',\n] as const;\n\n/**\n * Deep clone an object without mutating the original\n */\nfunction deepClone<T>(obj: T): T {\n if (obj === null || obj === undefined) {\n return obj;\n }\n if (Array.isArray(obj)) {\n return obj.map((item) => deepClone(item)) as T;\n }\n if (typeof obj === 'object') {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n result[key] = deepClone(value);\n }\n return result as T;\n }\n return obj;\n}\n\n/**\n * Check if a timestamp is in milliseconds (13+ digits)\n */\nfunction isMilliseconds(timestamp: number): boolean {\n // Timestamps before year 2001 in seconds: < 1000000000\n // Timestamps in milliseconds are typically 13 digits: 1000000000000+\n return timestamp > 10000000000;\n}\n\n/**\n * CardNormalizer - handles normalization of malformed card data\n */\nexport const CardNormalizer = {\n /**\n * Normalize card data to valid schema format.\n *\n * Handles:\n * - Fixing spec/spec_version values\n * - Moving misplaced fields to correct locations\n * - Adding missing required fields with defaults\n * - Handling hybrid formats (fields at root AND in data object)\n *\n * @param data - Raw card data (potentially malformed)\n * @param spec - Target spec version\n * @returns Normalized card data (does not mutate input)\n */\n normalize(data: unknown, spec: 'v2' | 'v3'): CCv2Wrapped | CCv3Data {\n if (!data || typeof data !== 'object') {\n // Return minimal valid card\n if (spec === 'v3') {\n return {\n spec: 'chara_card_v3',\n spec_version: '3.0',\n data: { ...V3_REQUIRED_DEFAULTS } as CCv3Data['data'],\n };\n }\n return {\n spec: 'chara_card_v2',\n spec_version: '2.0',\n data: { ...V2_REQUIRED_DEFAULTS } as CCv2Data,\n };\n }\n\n const obj = data as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n // Build merged data object from root fields + existing data object\n const existingData = (obj.data && typeof obj.data === 'object' ? obj.data : {}) as Record<\n string,\n unknown\n >;\n const mergedData: Record<string, unknown> = {};\n\n // Copy existing data first\n for (const [key, value] of Object.entries(existingData)) {\n mergedData[key] = deepClone(value);\n }\n\n // Move any misplaced root-level data fields into data object\n // (ChubAI hybrid format fix)\n for (const field of DATA_FIELDS) {\n if (field in obj && !(field in mergedData)) {\n mergedData[field] = deepClone(obj[field]);\n }\n }\n\n // Handle character_book: null -> remove entirely\n if (mergedData.character_book === null) {\n delete mergedData.character_book;\n }\n\n // Normalize character_book if present\n if (mergedData.character_book && typeof mergedData.character_book === 'object') {\n mergedData.character_book = CardNormalizer.normalizeCharacterBook(\n mergedData.character_book as Record<string, unknown>,\n spec\n );\n }\n\n // Apply defaults for required fields\n const defaults = spec === 'v3' ? V3_REQUIRED_DEFAULTS : V2_REQUIRED_DEFAULTS;\n for (const [key, defaultValue] of Object.entries(defaults)) {\n if (!(key in mergedData) || mergedData[key] === undefined) {\n mergedData[key] = Array.isArray(defaultValue) ? [...defaultValue] : defaultValue;\n }\n }\n\n // Ensure arrays are actually arrays\n if (mergedData.tags && !Array.isArray(mergedData.tags)) {\n mergedData.tags = [];\n }\n if (mergedData.alternate_greetings && !Array.isArray(mergedData.alternate_greetings)) {\n mergedData.alternate_greetings = [];\n }\n if (spec === 'v3') {\n if (\n mergedData.group_only_greetings &&\n !Array.isArray(mergedData.group_only_greetings)\n ) {\n mergedData.group_only_greetings = [];\n }\n }\n\n // Build result with correct spec\n if (spec === 'v3') {\n result.spec = 'chara_card_v3';\n result.spec_version = '3.0';\n result.data = CardNormalizer.fixTimestampsInner(mergedData);\n } else {\n result.spec = 'chara_card_v2';\n result.spec_version = '2.0';\n result.data = mergedData;\n }\n\n return result as unknown as CCv2Wrapped | CCv3Data;\n },\n\n /**\n * Normalize a character book (lorebook).\n *\n * Handles:\n * - Ensuring required fields exist\n * - Converting numeric position values to string enums\n * - Moving V3-only fields to extensions for V2 compatibility\n *\n * @param book - Raw character book data\n * @param spec - Target spec version\n * @returns Normalized character book\n */\n normalizeCharacterBook(\n book: Record<string, unknown>,\n spec: 'v2' | 'v3'\n ): CCv2CharacterBook | CCv3CharacterBook {\n const result: Record<string, unknown> = {};\n\n // Copy book-level fields\n if (book.name !== undefined) result.name = book.name;\n if (book.description !== undefined) result.description = book.description;\n if (book.scan_depth !== undefined) result.scan_depth = book.scan_depth;\n if (book.token_budget !== undefined) result.token_budget = book.token_budget;\n if (book.recursive_scanning !== undefined)\n result.recursive_scanning = book.recursive_scanning;\n if (book.extensions !== undefined) result.extensions = deepClone(book.extensions);\n\n // Normalize entries\n const entries = Array.isArray(book.entries) ? book.entries : [];\n result.entries = entries.map((entry) =>\n CardNormalizer.normalizeEntry(entry as Record<string, unknown>, spec)\n );\n\n return result as unknown as CCv2CharacterBook | CCv3CharacterBook;\n },\n\n /**\n * Normalize a single lorebook entry.\n *\n * Handles:\n * - Converting numeric position to string enum\n * - Moving V3-only fields to extensions for V2\n * - Ensuring required fields exist\n *\n * @param entry - Raw entry data\n * @param spec - Target spec version\n * @returns Normalized entry\n */\n normalizeEntry(\n entry: Record<string, unknown>,\n spec: 'v2' | 'v3'\n ): CCv2LorebookEntry | CCv3LorebookEntry {\n const result: Record<string, unknown> = {};\n\n // Required fields with defaults\n result.keys = Array.isArray(entry.keys) ? [...entry.keys] : [];\n result.content = typeof entry.content === 'string' ? entry.content : '';\n result.enabled = entry.enabled !== false; // default true\n result.insertion_order =\n typeof entry.insertion_order === 'number' ? entry.insertion_order : 0;\n\n // For V2, extensions is required\n if (spec === 'v2') {\n result.extensions =\n entry.extensions && typeof entry.extensions === 'object'\n ? deepClone(entry.extensions)\n : {};\n }\n\n // Optional fields\n if (entry.case_sensitive !== undefined) result.case_sensitive = entry.case_sensitive;\n if (entry.name !== undefined) result.name = entry.name;\n if (entry.priority !== undefined) result.priority = entry.priority;\n if (entry.id !== undefined) result.id = entry.id;\n if (entry.comment !== undefined) result.comment = entry.comment;\n if (entry.selective !== undefined) result.selective = entry.selective;\n if (entry.secondary_keys !== undefined) {\n result.secondary_keys = Array.isArray(entry.secondary_keys)\n ? [...entry.secondary_keys]\n : [];\n }\n if (entry.constant !== undefined) result.constant = entry.constant;\n\n // Position: convert numeric to string enum\n if (entry.position !== undefined) {\n if (typeof entry.position === 'number') {\n result.position = POSITION_MAP[entry.position] || 'before_char';\n } else if (entry.position === 'before_char' || entry.position === 'after_char') {\n result.position = entry.position;\n }\n }\n\n // Handle V3-only fields\n if (spec === 'v3') {\n // Copy V3 fields directly\n if (entry.extensions !== undefined) result.extensions = deepClone(entry.extensions);\n for (const field of V3_ONLY_ENTRY_FIELDS) {\n if (entry[field] !== undefined) {\n result[field] = entry[field];\n }\n }\n } else {\n // V2: Move V3-only fields to extensions\n const ext = (result.extensions || {}) as Record<string, unknown>;\n for (const field of V3_ONLY_ENTRY_FIELDS) {\n if (entry[field] !== undefined) {\n ext[field] = entry[field];\n }\n }\n result.extensions = ext;\n }\n\n return result as unknown as CCv2LorebookEntry | CCv3LorebookEntry;\n },\n\n /**\n * Fix CharacterTavern timestamp format (milliseconds -> seconds).\n *\n * CCv3 spec requires timestamps in seconds (Unix epoch).\n * CharacterTavern exports timestamps in milliseconds.\n *\n * @param data - V3 card data\n * @returns Card data with fixed timestamps (does not mutate input)\n */\n fixTimestamps(data: CCv3Data): CCv3Data {\n const result = deepClone(data);\n result.data = CardNormalizer.fixTimestampsInner(\n result.data as unknown as Record<string, unknown>\n ) as unknown as CCv3Data['data'];\n return result;\n },\n\n /**\n * Internal: fix timestamps in data object\n */\n fixTimestampsInner(data: Record<string, unknown>): Record<string, unknown> {\n const result = { ...data };\n\n if (typeof result.creation_date === 'number') {\n if (isMilliseconds(result.creation_date)) {\n result.creation_date = Math.floor(result.creation_date / 1000);\n }\n // Sanitize negative timestamps (.NET default dates like 0001-01-01)\n if ((result.creation_date as number) < 0) {\n delete result.creation_date;\n }\n }\n\n if (typeof result.modification_date === 'number') {\n if (isMilliseconds(result.modification_date)) {\n result.modification_date = Math.floor(result.modification_date / 1000);\n }\n // Sanitize negative timestamps (.NET default dates like 0001-01-01)\n if ((result.modification_date as number) < 0) {\n delete result.modification_date;\n }\n }\n\n return result;\n },\n\n /**\n * Auto-detect spec and normalize.\n *\n * @param data - Raw card data\n * @returns Normalized card data, or null if not a valid card\n */\n autoNormalize(data: unknown): CCv2Wrapped | CCv3Data | null {\n const spec = detectSpec(data);\n if (!spec) return null;\n\n // V1 cards get upgraded to V2\n const targetSpec = spec === 'v3' ? 'v3' : 'v2';\n return CardNormalizer.normalize(data, targetSpec);\n },\n};\n\nexport type { CCv2Wrapped, CCv3Data };\n","/**\n * Validation Utilities\n *\n * Helper functions for Zod validation with Foundry error integration.\n */\n\nimport { z } from 'zod';\n\n/**\n * Convert Zod error to human-readable message\n */\nexport function zodErrorToMessage(zodError: z.ZodError, context?: string): string {\n const messages = zodError.errors.map((err) => {\n const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';\n return `${path}${err.message}`;\n });\n\n const message = messages.join('; ');\n return context ? `${context} - ${message}` : message;\n}\n\n/**\n * Get the first error field from Zod error\n */\nexport function getFirstErrorField(zodError: z.ZodError): string | undefined {\n return zodError.errors[0]?.path[0]?.toString();\n}\n\n/**\n * Safe parse with detailed error information\n */\nexport function safeParse<T>(\n schema: z.ZodSchema<T>,\n data: unknown\n): { success: true; data: T } | { success: false; error: string; field?: string } {\n const result = schema.safeParse(data);\n\n if (result.success) {\n return { success: true, data: result.data };\n }\n\n return {\n success: false,\n error: zodErrorToMessage(result.error),\n field: getFirstErrorField(result.error),\n };\n}\n","/**\n * CharX Reader\n *\n * Extracts and parses .charx (ZIP-based character card) files.\n * Supports standard CharX, Risu CharX, and JPEG+ZIP hybrid formats.\n */\n\nimport {\n type BinaryData,\n toString,\n base64Decode,\n parseURI,\n ParseError,\n SizeLimitError,\n} from '@character-foundry/core';\nimport {\n type Unzipped,\n isJpegCharX,\n getZipOffset,\n streamingUnzipSync,\n ZipPreflightError,\n} from '@character-foundry/core/zip';\nimport type { CCv3Data, AssetDescriptor } from '@character-foundry/schemas';\nimport { hasRisuExtensions } from '@character-foundry/schemas';\nimport type {\n CharxData,\n CharxAssetInfo,\n CharxMetaEntry,\n CharxReadOptions,\n AssetFetcher,\n} from './types.js';\n\nconst DEFAULT_OPTIONS: Required<Omit<CharxReadOptions, 'assetFetcher'>> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxAssetSize: 50 * 1024 * 1024, // 50MB (Risu standard)\n maxTotalSize: 200 * 1024 * 1024, // 200MB\n preserveXMeta: true,\n preserveModuleRisum: true,\n};\n\n/**\n * Check if data is a CharX file (ZIP with card.json)\n *\n * Scans the END of the file (ZIP central directory) since JPEG+ZIP hybrids\n * can have large JPEG data at the front. The central directory listing\n * all filenames is always at the tail of the ZIP.\n */\nexport function isCharX(data: BinaryData): boolean {\n const zipOffset = getZipOffset(data);\n if (zipOffset < 0) return false;\n\n const zipData = data.subarray(zipOffset);\n const cardJsonMarker = new TextEncoder().encode('card.json');\n\n // Scan the last 64KB where the central directory lives\n // (covers ZIP with up to ~1000 files in the directory)\n const scanSize = Math.min(zipData.length, 65536);\n const startOffset = zipData.length - scanSize;\n\n for (let i = startOffset; i < zipData.length - cardJsonMarker.length; i++) {\n let found = true;\n for (let j = 0; j < cardJsonMarker.length; j++) {\n if (zipData[i + j] !== cardJsonMarker[j]) {\n found = false;\n break;\n }\n }\n if (found) return true;\n }\n\n return false;\n}\n\n/**\n * Check if data is a JPEG+ZIP hybrid (JPEG with appended CharX)\n */\nexport { isJpegCharX };\n\n/**\n * Extract and parse a CharX buffer\n */\nexport function readCharX(\n data: BinaryData,\n options: CharxReadOptions = {}\n): CharxData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n // SECURITY: Streaming unzip with real-time byte limit enforcement\n // This tracks ACTUAL decompressed bytes and aborts if limits exceeded,\n // protecting against malicious archives that lie about sizes in central directory\n let unzipped: Unzipped;\n try {\n unzipped = streamingUnzipSync(data, {\n maxFileSize: opts.maxAssetSize,\n maxTotalSize: opts.maxTotalSize,\n maxFiles: 10000, // CharX can have many assets\n });\n } catch (err) {\n if (err instanceof ZipPreflightError) {\n throw new SizeLimitError(\n err.totalSize || err.entrySize || 0,\n err.maxSize || err.maxEntrySize || opts.maxTotalSize,\n err.oversizedEntry || 'CharX archive'\n );\n }\n throw new ParseError(\n `Failed to unzip CharX: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n\n let cardJson: CCv3Data | null = null;\n const assets: CharxAssetInfo[] = [];\n const metadata = new Map<number, CharxMetaEntry>();\n let moduleRisum: BinaryData | undefined;\n\n // Process entries (size limits already enforced by streamingUnzipSync)\n for (const [fileName, fileData] of Object.entries(unzipped)) {\n // Skip directories (empty or ends with /)\n if (fileName.endsWith('/') || fileData.length === 0) continue;\n\n // Handle card.json\n if (fileName === 'card.json') {\n if (fileData.length > opts.maxFileSize) {\n throw new SizeLimitError(fileData.length, opts.maxFileSize, 'card.json');\n }\n try {\n const content = toString(fileData);\n cardJson = JSON.parse(content) as CCv3Data;\n } catch (err) {\n throw new ParseError(\n `Failed to parse card.json: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n continue;\n }\n\n // Handle x_meta/*.json\n if (opts.preserveXMeta) {\n const metaMatch = fileName.match(/^x_meta\\/(\\d+)\\.json$/);\n if (metaMatch) {\n const index = parseInt(metaMatch[1]!, 10);\n try {\n const content = toString(fileData);\n const meta = JSON.parse(content) as CharxMetaEntry;\n metadata.set(index, meta);\n } catch {\n // Ignore invalid metadata\n }\n continue;\n }\n }\n\n // Handle module.risum (Risu scripts)\n if (fileName === 'module.risum' && opts.preserveModuleRisum) {\n moduleRisum = fileData;\n continue;\n }\n\n // Handle assets/** files\n if (fileName.startsWith('assets/')) {\n const name = fileName.split('/').pop() || 'unknown';\n const ext = name.split('.').pop() || 'bin';\n\n assets.push({\n path: fileName,\n descriptor: {\n type: 'custom',\n name: name.replace(/\\.[^.]+$/, ''), // Remove extension\n uri: `embeded://${fileName}`,\n ext,\n },\n buffer: fileData,\n });\n continue;\n }\n\n // Unknown files are ignored (readme.txt, etc.)\n }\n\n if (!cardJson) {\n throw new ParseError('CharX file does not contain card.json', 'charx');\n }\n\n // Validate that it's a CCv3 card\n if (cardJson.spec !== 'chara_card_v3') {\n throw new ParseError(\n `Invalid card spec: expected \"chara_card_v3\", got \"${cardJson.spec}\"`,\n 'charx'\n );\n }\n\n // Match assets to their descriptors from card.json\n const matchedAssets = matchAssetsToDescriptors(assets, cardJson.data.assets || []);\n\n // Determine if this is a Risu-format CharX\n const isRisuFormat = !!moduleRisum || hasRisuExtensions(cardJson.data.extensions);\n\n return {\n card: cardJson,\n assets: matchedAssets,\n metadata: metadata.size > 0 ? metadata : undefined,\n moduleRisum,\n isRisuFormat,\n };\n}\n\n/**\n * Match extracted asset files to their descriptors from card.json\n *\n * @performance Uses O(1) Map lookup instead of O(n) linear search per descriptor.\n * This reduces complexity from O(n*m) to O(n+m) for large asset packs.\n */\nfunction matchAssetsToDescriptors(\n extractedAssets: CharxAssetInfo[],\n descriptors: AssetDescriptor[]\n): CharxAssetInfo[] {\n // Build O(1) lookup map for extracted assets by path\n const assetsByPath = new Map<string, CharxAssetInfo>();\n for (const asset of extractedAssets) {\n assetsByPath.set(asset.path, asset);\n }\n\n const matched: CharxAssetInfo[] = [];\n\n for (const descriptor of descriptors) {\n const parsed = parseURI(descriptor.uri);\n\n if (parsed.scheme === 'embeded' && parsed.path) {\n // O(1) lookup instead of O(n) find\n const asset = assetsByPath.get(parsed.path);\n\n if (asset) {\n matched.push({\n ...asset,\n descriptor,\n });\n } else {\n // Asset referenced but not found in ZIP\n matched.push({\n path: parsed.path,\n descriptor,\n buffer: undefined,\n });\n }\n } else if (parsed.scheme === 'ccdefault') {\n // Default asset, no file needed\n matched.push({\n path: 'ccdefault:',\n descriptor,\n buffer: undefined,\n });\n } else if (parsed.scheme === 'https' || parsed.scheme === 'http') {\n // Remote asset, no file needed\n matched.push({\n path: descriptor.uri,\n descriptor,\n buffer: undefined,\n });\n } else if (parsed.scheme === 'data') {\n // Data URI, extract the data\n if (parsed.data && parsed.encoding === 'base64') {\n const buffer = base64Decode(parsed.data);\n matched.push({\n path: 'data:',\n descriptor,\n buffer,\n });\n } else {\n matched.push({\n path: 'data:',\n descriptor,\n buffer: undefined,\n });\n }\n }\n }\n\n return matched;\n}\n\n/**\n * Extract just the card.json from a CharX buffer (quick validation)\n */\nexport function readCardJsonOnly(data: BinaryData): CCv3Data {\n // Use streaming unzip with conservative limits for card.json extraction\n // This protects against zip bombs even for the \"quick validation\" path\n let unzipped: Unzipped;\n try {\n unzipped = streamingUnzipSync(data, {\n maxFileSize: DEFAULT_OPTIONS.maxFileSize, // 10MB for card.json\n maxTotalSize: DEFAULT_OPTIONS.maxTotalSize, // 200MB total\n maxFiles: 10000,\n });\n } catch (err) {\n throw new ParseError(\n `Failed to unzip CharX: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n\n const cardData = unzipped['card.json'];\n if (!cardData) {\n throw new ParseError('card.json not found in CharX file', 'charx');\n }\n\n try {\n const content = toString(cardData);\n return JSON.parse(content) as CCv3Data;\n } catch (err) {\n throw new ParseError(\n `Failed to parse card.json: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n}\n\n/**\n * Async version of readCharX with optional remote asset fetching\n */\nexport async function readCharXAsync(\n data: BinaryData,\n options: CharxReadOptions & { fetchRemoteAssets?: boolean; assetFetcher?: AssetFetcher } = {}\n): Promise<CharxData> {\n // First do the sync extraction\n const result = readCharX(data, options);\n\n // If remote fetching is disabled or no fetcher provided, return as-is\n if (!options.fetchRemoteAssets || !options.assetFetcher) {\n return result;\n }\n\n // Fetch remote assets\n const fetchedAssets = await Promise.all(\n result.assets.map(async (asset) => {\n // Only fetch assets that don't have buffers and have remote URLs\n if (asset.buffer) {\n return asset;\n }\n\n const parsed = parseURI(asset.descriptor.uri);\n\n if ((parsed.scheme === 'https' || parsed.scheme === 'http') && parsed.url) {\n try {\n const buffer = await options.assetFetcher!(parsed.url);\n if (buffer) {\n return { ...asset, buffer };\n }\n } catch {\n // Failed to fetch, leave buffer undefined\n }\n }\n\n return asset;\n })\n );\n\n return {\n ...result,\n assets: fetchedAssets,\n };\n}\n","/**\n * CharX Writer\n *\n * Creates .charx (ZIP-based character card) files.\n */\n\nimport { zipSync, type Zippable } from 'fflate';\nimport {\n fromString,\n getMimeTypeFromExt,\n} from '@character-foundry/core';\nimport type { CCv3Data, AssetDescriptor } from '@character-foundry/schemas';\nimport type {\n CharxWriteAsset,\n CharxWriteOptions,\n CharxBuildResult,\n CompressionLevel,\n} from './types.js';\n\n/** Safe asset types for CharX path construction (whitelist) */\nconst SAFE_ASSET_TYPES = new Set([\n 'icon', 'user_icon', 'emotion', 'background', 'sound', 'video',\n 'custom', 'x-risu-asset', 'data', 'unknown',\n]);\n\n/**\n * Get CharX category from MIME type\n */\nfunction getCharxCategory(mimetype: string): string {\n if (mimetype.startsWith('image/')) return 'images';\n if (mimetype.startsWith('audio/')) return 'audio';\n if (mimetype.startsWith('video/')) return 'video';\n return 'other';\n}\n\n/**\n * Sanitize an asset type for safe use in file paths.\n * Only allows whitelisted types to prevent path traversal.\n */\nfunction sanitizeAssetType(type: string): string {\n // Normalize to lowercase\n const normalized = type.toLowerCase().replace(/[^a-z0-9-_]/g, '-');\n\n // Use whitelist - if not in whitelist, default to 'custom'\n if (SAFE_ASSET_TYPES.has(normalized)) {\n return normalized;\n }\n\n // For unknown types, sanitize strictly\n const sanitized = normalized.replace(/[^a-z0-9]/g, '');\n return sanitized || 'custom';\n}\n\n/**\n * Sanitize a file extension for safe use in file paths.\n *\n * @remarks\n * CharX assets may be arbitrary file types (including scripts/text). We validate\n * for path-safety and normalize minimally, rather than coercing unknown\n * extensions to `.bin`.\n */\nfunction sanitizeExtension(ext: string): string {\n const normalized = ext.trim().replace(/^\\./, '').toLowerCase();\n\n if (!normalized) {\n throw new Error('Invalid asset extension: empty extension');\n }\n\n if (normalized.length > 64) {\n throw new Error(`Invalid asset extension: too long (${normalized.length} chars)`);\n }\n\n // Prevent zip path traversal / separators\n if (normalized.includes('/') || normalized.includes('\\\\') || normalized.includes('\\0')) {\n throw new Error('Invalid asset extension: path separators are not allowed');\n }\n\n // Conservative filename safety while still allowing common multi-part extensions (e.g. tar.gz)\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {\n throw new Error(`Invalid asset extension: \"${ext}\"`);\n }\n\n return normalized;\n}\n\n/**\n * Sanitize a name for use in file paths\n */\nfunction sanitizeName(name: string, ext: string): string {\n let safeName = name;\n\n // Strip extension if present\n if (safeName.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {\n safeName = safeName.substring(0, safeName.length - (ext.length + 1));\n }\n\n // Replace dots and underscores with hyphens, remove special chars, collapse dashes\n safeName = safeName\n .replace(/[._]/g, '-')\n .replace(/[^a-zA-Z0-9-]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n\n if (!safeName) safeName = 'asset';\n\n return safeName;\n}\n\n/**\n * Build a CharX ZIP from card data and assets\n */\nexport function writeCharX(\n card: CCv3Data,\n assets: CharxWriteAsset[],\n options: CharxWriteOptions = {}\n): CharxBuildResult {\n const {\n spec = 'v3',\n compressionLevel = 6,\n emitXMeta = spec === 'risu',\n emitReadme = false,\n moduleRisum,\n } = options;\n\n // Transform card to use embeded:// URIs\n const transformedCard = transformAssetUris(card, assets);\n\n // Create ZIP entries\n const zipEntries: Zippable = {};\n\n // Add card.json\n const cardJson = JSON.stringify(transformedCard, null, 2);\n zipEntries['card.json'] = [fromString(cardJson), { level: compressionLevel as CompressionLevel }];\n\n // Add readme.txt if requested\n if (emitReadme) {\n const readme = `Character: ${card.data.name}\nCreated with Character Foundry\n\nThis is a CharX character card package.\nImport this file into SillyTavern, RisuAI, or other compatible applications.\n`;\n zipEntries['readme.txt'] = [fromString(readme), { level: compressionLevel as CompressionLevel }];\n }\n\n // Add assets\n let assetCount = 0;\n\n for (let i = 0; i < assets.length; i++) {\n const asset = assets[i]!;\n // SECURITY: Sanitize all path components to prevent path traversal\n const safeType = sanitizeAssetType(asset.type);\n const safeExt = sanitizeExtension(asset.ext);\n const mimetype = getMimeTypeFromExt(safeExt);\n const category = getCharxCategory(mimetype);\n const safeName = sanitizeName(asset.name, safeExt);\n\n const assetPath = `assets/${safeType}/${category}/${safeName}.${safeExt}`;\n\n zipEntries[assetPath] = [asset.data, { level: compressionLevel as CompressionLevel }];\n assetCount++;\n\n // Add x_meta if enabled and it's an image\n if (emitXMeta && mimetype.startsWith('image/')) {\n const metaJson = JSON.stringify({\n type: mimetype.split('/')[1]?.toUpperCase() || 'PNG',\n });\n zipEntries[`x_meta/${i}.json`] = [fromString(metaJson), { level: compressionLevel as CompressionLevel }];\n }\n }\n\n // Add module.risum for Risu format (opaque preservation)\n if (moduleRisum) {\n zipEntries['module.risum'] = [moduleRisum, { level: compressionLevel as CompressionLevel }];\n }\n\n // Create ZIP\n const buffer = zipSync(zipEntries);\n\n return {\n buffer,\n assetCount,\n totalSize: buffer.length,\n };\n}\n\n/**\n * Transform asset URIs in card to use embeded:// format\n */\nfunction transformAssetUris(card: CCv3Data, assets: CharxWriteAsset[]): CCv3Data {\n // Clone the card to avoid mutations\n // Note: Using structuredClone where available for better performance and preserving undefined\n const transformed: CCv3Data = typeof structuredClone === 'function'\n ? structuredClone(card)\n : JSON.parse(JSON.stringify(card));\n\n // Generate assets array from provided assets\n transformed.data.assets = assets.map((asset): AssetDescriptor => {\n // SECURITY: Sanitize all path components to prevent path traversal\n const safeType = sanitizeAssetType(asset.type);\n const safeExt = sanitizeExtension(asset.ext);\n const mimetype = getMimeTypeFromExt(safeExt);\n const category = getCharxCategory(mimetype);\n const safeName = sanitizeName(asset.name, safeExt);\n\n return {\n type: asset.type as AssetDescriptor['type'],\n uri: `embeded://assets/${safeType}/${category}/${safeName}.${safeExt}`,\n name: safeName,\n ext: safeExt,\n };\n });\n\n return transformed;\n}\n\n/**\n * Async version of writeCharX\n */\nexport async function writeCharXAsync(\n card: CCv3Data,\n assets: CharxWriteAsset[],\n options: CharxWriteOptions = {}\n): Promise<CharxBuildResult> {\n // For now, just wrap sync version\n return writeCharX(card, assets, options);\n}\n"],"mappings":";AA6FO,SAAS,WAAW,KAAyB;AAClD,SAAO,IAAI,YAAY,EAAE,OAAO,GAAG;AACrC;AAKO,SAAS,SAAS,MAA0B;AACjD,SAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AACtC;AC3FA,IAAM,SAAS,OAAO,YAAY,eAChC,QAAQ,YAAY,QACpB,QAAQ,SAAS,QAAQ;AAO3B,IAAM,yBAAyB,OAAO;AA8B/B,SAAS,OAAO,QAA4B;AACjD,MAAI,QAAQ;AAEV,WAAO,IAAI,WAAW,OAAO,KAAK,QAAQ,QAAQ,CAAC;EACrD;AAGA,QAAM,SAAS,KAAK,MAAM;AAC1B,QAAM,SAAS,IAAI,WAAW,OAAO,MAAM;AAC3C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,WAAO,CAAC,IAAI,OAAO,WAAW,CAAC;EACjC;AACA,SAAO;AACT;AA2CA,IAAM,oBAAoB,KAAK;AClG/B,IAAM,uBAAuB,uBAAO,IAAI,sCAAsC;AAKvE,IAAM,eAAN,cAA2B,MAAM;EAItC,YAAY,SAAiC,MAAc;AACzD,UAAM,OAAO;AAD8B,SAAA,OAAA;AAE3C,SAAK,OAAO;AAEZ,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,KAAK,WAAW;IAChD;EACF;;EATA,CAAU,oBAAoB,IAAI;AAUpC;AAKO,IAAM,aAAN,cAAyB,aAAa;EAC3C,YAAY,SAAiC,QAAiB;AAC5D,UAAM,SAAS,aAAa;AADe,SAAA,SAAA;AAE3C,SAAK,OAAO;EACd;AACF;AAsCO,IAAM,iBAAN,cAA6B,aAAa;EAC/C,YACkB,YACA,SAChB,SACA;AACA,UAAM,YAAY,aAAa,OAAO,MAAM,QAAQ,CAAC;AACrD,UAAM,SAAS,UAAU,OAAO,MAAM,QAAQ,CAAC;AAC/C,UAAM,MAAM,UACR,GAAG,OAAO,UAAU,QAAQ,oBAAoB,KAAK,OACrD,QAAQ,QAAQ,oBAAoB,KAAK;AAC7C,UAAM,KAAK,qBAAqB;AAThB,SAAA,aAAA;AACA,SAAA,UAAA;AAShB,SAAK,OAAO;EACd;AACF;AEnDO,SAAS,aAAa,KAAqB;AAChD,QAAM,UAAU,IAAI,KAAK;AAGzB,MAAI,QAAQ,WAAW,aAAa,GAAG;AACrC,WAAO,eAAe,QAAQ,UAAU,cAAc,MAAM;EAC9D;AAGA,MAAI,QAAQ,WAAW,UAAU,GAAG;AAClC,UAAM,KAAK,QAAQ,UAAU,WAAW,MAAM;AAC9C,WAAO,YAAY,EAAE;EACvB;AACA,MAAI,QAAQ,WAAW,QAAQ,GAAG;AAChC,UAAM,KAAK,QAAQ,UAAU,SAAS,MAAM;AAC5C,WAAO,YAAY,EAAE;EACvB;AACA,MAAI,QAAQ,WAAW,mBAAmB,GAAG;AAC3C,UAAM,KAAK,QAAQ,UAAU,oBAAoB,MAAM;AACvD,WAAO,YAAY,EAAE;EACvB;AACA,MAAI,QAAQ,WAAW,kBAAkB,GAAG;AAC1C,UAAM,KAAK,QAAQ,UAAU,mBAAmB,MAAM;AACtD,WAAO,YAAY,EAAE;EACvB;AAEA,SAAO;AACT;AAKO,SAAS,SAAS,KAAwB;AAC/C,QAAM,UAAU,IAAI,KAAK;AACzB,QAAM,aAAa,aAAa,OAAO;AAGvC,MACE,QAAQ,WAAW,UAAU,KAC7B,QAAQ,WAAW,QAAQ,KAC3B,QAAQ,WAAW,kBAAkB,KACrC,QAAQ,WAAW,WAAW,GAC9B;AACA,QAAI;AACJ,QAAI,QAAQ,WAAW,UAAU,GAAG;AAClC,gBAAU,QAAQ,UAAU,WAAW,MAAM;IAC/C,WAAW,QAAQ,WAAW,QAAQ,GAAG;AACvC,gBAAU,QAAQ,UAAU,SAAS,MAAM;IAC7C,WAAW,QAAQ,WAAW,mBAAmB,GAAG;AAClD,gBAAU,QAAQ,UAAU,oBAAoB,MAAM;IACxD,WAAW,QAAQ,WAAW,WAAW,GAAG;AAC1C,gBAAU,QAAQ,UAAU,YAAY,MAAM;IAChD,OAAO;AACL,gBAAU,QAAQ,UAAU,mBAAmB,MAAM;IACvD;AAGA,UAAM,aAAa;MACjB;;MACA;;MACA,SAAS,OAAO;;MAChB,WAAW,OAAO;;MAClB,WAAW,OAAO;;MAClB,mBAAmB,OAAO;;MAC1B,oBAAoB,OAAO;;MAC3B,YAAY,OAAO;;IACrB;AAEA,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf,UAAU;MACV,iBAAiB;IACnB;EACF;AAGA,MAAI,YAAY,gBAAgB,QAAQ,WAAW,YAAY,GAAG;AAChE,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;IACjB;EACF;AAGA,MAAI,QAAQ,WAAW,YAAY,KAAK,QAAQ,WAAW,aAAa,GAAG;AACzE,UAAM,OAAO,QAAQ,WAAW,YAAY,IACxC,QAAQ,UAAU,aAAa,MAAM,IACrC,QAAQ,UAAU,cAAc,MAAM;AAC1C,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf;IACF;EACF;AAGA,MAAI,QAAQ,WAAW,UAAU,GAAG;AAClC,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf,KAAK;IACP;EACF;AAGA,MAAI,QAAQ,WAAW,SAAS,GAAG;AACjC,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf,KAAK;IACP;EACF;AAGA,MAAI,QAAQ,WAAW,OAAO,GAAG;AAC/B,UAAM,SAAS,aAAa,OAAO;AACnC,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf,GAAG;IACL;EACF;AAGA,MAAI,QAAQ,WAAW,SAAS,GAAG;AACjC,UAAM,OAAO,QAAQ,UAAU,UAAU,MAAM;AAC/C,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf;IACF;EACF;AAGA,MAAI,mBAAmB,KAAK,OAAO,GAAG;AACpC,WAAO;MACL,QAAQ;MACR,aAAa;MACb,eAAe;MACf,MAAM;IACR;EACF;AAGA,SAAO;IACL,QAAQ;IACR,aAAa;IACb,eAAe;EACjB;AACF;AAMA,SAAS,aAAa,KAAsE;AAC1F,QAAM,QAAQ,IAAI,MAAM,iCAAiC;AAEzD,MAAI,CAAC,OAAO;AACV,WAAO,CAAC;EACV;AAEA,SAAO;IACL,UAAU,MAAM,CAAC,KAAK;IACtB,UAAU,MAAM,CAAC,IAAI,WAAW;IAChC,MAAM,MAAM,CAAC;EACf;AACF;AAwNO,SAAS,mBAAmB,KAAqB;AACtD,QAAM,YAAoC;;IAExC,OAAO;IACP,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,OAAO;IACP,OAAO;IACP,OAAO;;IAGP,OAAO;IACP,OAAO;IACP,OAAO;IACP,QAAQ;IACR,OAAO;IACP,OAAO;;IAGP,OAAO;IACP,QAAQ;IACR,OAAO;IACP,OAAO;IACP,OAAO;;IAGP,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,OAAO;IACP,MAAM;EACR;AAEA,SAAO,UAAU,IAAI,YAAY,CAAC,KAAK;AACzC;;;AIxcA,SAAS,OAAO,cAAc,wBAAuD;AD4C9E,SAAS,QAAQ,MAAkB,QAAoB,YAAY,GAAW;AACnF,QAAO,UAAS,IAAI,WAAW,KAAK,KAAK,SAAS,OAAO,QAAQ,KAAK;AACpE,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAI,KAAK,IAAI,CAAC,MAAM,OAAO,CAAC,EAAG,UAAS;IAC1C;AACA,WAAO;EACT;AACA,SAAO;AACT;AAKO,SAAS,UAAU,QAAkC;AAC1D,QAAM,cAAc,OAAO,OAAO,CAAC,KAAK,QAAQ,MAAM,IAAI,QAAQ,CAAC;AACnE,QAAM,SAAS,IAAI,WAAW,WAAW;AACzC,MAAI,SAAS;AACb,aAAW,OAAO,QAAQ;AACxB,WAAO,IAAI,KAAK,MAAM;AACtB,cAAU,IAAI;EAChB;AACA,SAAO;AACT;AC/DO,IAAM,gBAAgB,IAAI,WAAW,CAAC,IAAM,IAAM,GAAM,CAAI,CAAC;AAG7D,IAAM,iBAAiB,IAAI,WAAW,CAAC,KAAM,KAAM,GAAI,CAAC;AAuCxD,IAAM,qBAAoC;EAC/C,aAAa,KAAK,OAAO;;EACzB,cAAc,MAAM,OAAO;;EAC3B,UAAU;;EACV,oBAAoB;;AACtB;AA8BO,SAAS,OAAO,MAA2B;AAChD,SACE,KAAK,UAAU,KACf,KAAK,CAAC,MAAM,OACZ,KAAK,CAAC,MAAM,OACZ,KAAK,CAAC,MAAM;AAEhB;AAKO,SAAS,YAAY,MAA2B;AACrD,MAAI,CAAC,OAAO,IAAI,EAAG,QAAO;AAE1B,SAAO,QAAQ,MAAM,aAAa,IAAI;AACxC;AASO,SAAS,aAAa,MAA8B;AACzD,QAAM,QAAQ,QAAQ,MAAM,aAAa;AAEzC,MAAI,QAAQ,GAAG;AAEb,WAAO,KAAK,SAAS,KAAK;EAC5B;AAGA,SAAO;AACT;AAOO,SAAS,aAAa,MAA0B;AACrD,SAAO,QAAQ,MAAM,aAAa;AACpC;AAqBO,SAAS,WAAW,MAAuB;AAEhD,MAAI,KAAK,WAAW,GAAG,KAAK,aAAa,KAAK,IAAI,GAAG;AACnD,WAAO;EACT;AAGA,MAAI,KAAK,SAAS,IAAI,GAAG;AACvB,WAAO;EACT;AAGA,MAAI,KAAK,SAAS,IAAI,GAAG;AACvB,WAAO;EACT;AAEA,SAAO;AACT;AAuBO,IAAM,oBAAN,cAAgC,MAAM;EAC3C,YACE,SACgB,WACA,SACA,gBACA,WACA,cAChB;AACA,UAAM,OAAO;AANG,SAAA,YAAA;AACA,SAAA,UAAA;AACA,SAAA,iBAAA;AACA,SAAA,YAAA;AACA,SAAA,eAAA;AAGhB,SAAK,OAAO;EACd;AACF;AA+LO,SAAS,mBACd,MACA,SAAwB,oBACd;AAEV,QAAM,UAAU,aAAa,IAAI;AAEjC,QAAM,SAAmB,CAAC;AAC1B,MAAI,aAAa;AACjB,MAAI,YAAY;AAChB,MAAI,QAAsB;AAG1B,QAAM,qBAAqB,OAAO,sBAAsB;AAGxD,QAAM,aAAa,oBAAI,IAA0B;AAEjD,QAAM,WAAW,IAAI,MAAM,CAAC,SAAoB;AAC9C,QAAI,MAAO;AAGX,QAAI,KAAK,KAAK,SAAS,GAAG,GAAG;AAC3B,WAAK,MAAM;AACX;IACF;AAGA,QAAI,CAAC,WAAW,KAAK,IAAI,GAAG;AAC1B,YAAM,SAAS,KAAK,KAAK,SAAS,IAAI,IAClC,wBACA,KAAK,KAAK,WAAW,GAAG,KAAK,aAAa,KAAK,KAAK,IAAI,IACtD,kBACA;AAEN,UAAI,uBAAuB,UAAU;AACnC,gBAAQ,IAAI;UACV,0BAA0B,KAAK,IAAI,OAAO,MAAM;QAElD;AACA,aAAK,UAAU;AACf;MACF;AAEA,UAAI,uBAAuB,UAAU,OAAO,cAAc;AACxD,eAAO,aAAa,KAAK,MAAM,MAAM;MACvC;AAIA,WAAK,SAAS,CAAC,KAAK,OAAO,WAAW;AACpC,YAAI,MAAO;AACX,YAAI,KAAK;AACP,kBAAQ;AACR;QACF;AACA,YAAI,SAAS,MAAM,SAAS,GAAG;AAC7B,wBAAc,MAAM;AACpB,cAAI,aAAa,OAAO,cAAc;AACpC,oBAAQ,IAAI;cACV,qBAAqB,UAAU,kBAAkB,OAAO,YAAY;cACpE;cACA,OAAO;YACT;AACA,iBAAK,UAAU;UACjB;QACF;MACF;AACA,WAAK,MAAM;AACX;IACF;AAEA;AACA,QAAI,YAAY,OAAO,UAAU;AAC/B,cAAQ,IAAI;QACV,cAAc,SAAS,kBAAkB,OAAO,QAAQ;MAC1D;AACA,WAAK,UAAU;AACf;IACF;AAEA,UAAM,SAAuB,CAAC;AAC9B,eAAW,IAAI,KAAK,MAAM,MAAM;AAChC,QAAI,YAAY;AAEhB,SAAK,SAAS,CAAC,KAAK,OAAO,UAAU;AACnC,UAAI,MAAO;AAEX,UAAI,KAAK;AACP,gBAAQ;AACR;MACF;AAEA,UAAI,SAAS,MAAM,SAAS,GAAG;AAC7B,qBAAa,MAAM;AACnB,sBAAc,MAAM;AAGpB,YAAI,YAAY,OAAO,aAAa;AAClC,kBAAQ,IAAI;YACV,SAAS,KAAK,IAAI,iBAAiB,SAAS,kBAAkB,OAAO,WAAW;YAChF;YACA;YACA,KAAK;YACL;YACA,OAAO;UACT;AACA,eAAK,UAAU;AACf;QACF;AAGA,YAAI,aAAa,OAAO,cAAc;AACpC,kBAAQ,IAAI;YACV,qBAAqB,UAAU,kBAAkB,OAAO,YAAY;YACpE;YACA,OAAO;UACT;AACA,eAAK,UAAU;AACf;QACF;AAEA,eAAO,KAAK,KAAK;MACnB;AAEA,UAAI,SAAS,CAAC,OAAO;AAEnB,eAAO,KAAK,IAAI,IAAI,OAAO,GAAG,MAAM;MACtC;IACF;AAEA,SAAK,MAAM;EACb,CAAC;AAGD,WAAS,SAAS,YAAY;AAC9B,WAAS,SAAS,gBAAgB;AAGlC,WAAS,KAAK,SAAS,IAAI;AAG3B,MAAI,OAAO;AACT,UAAM;EACR;AAEA,SAAO;AACT;;;ACzhBA,SAAS,SAAS;ACAlB,SAAS,KAAAA,UAAS;ACAlB,SAAS,KAAAA,UAAS;AMAlB,OAAkB;ARWX,SAAS,oBAAoB,KAAkC;AACpE,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAE9C,MAAI;AAEJ,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM;EACR,WAAW,OAAO,QAAQ,UAAU;AAClC,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,CAAC,QAAS,QAAO;AAGrB,UAAM,SAAS,OAAO,OAAO;AAC7B,QAAI,CAAC,MAAM,MAAM,GAAG;AAClB,YAAM;IACR,OAAO;AAEL,YAAM,OAAO,IAAI,KAAK,OAAO;AAC7B,UAAI,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AAClC,YAAM,KAAK,MAAM,KAAK,QAAQ,IAAI,GAAI;IACxC;EACF,OAAO;AACL,WAAO;EACT;AAGA,MAAI,MAAM,MAAgB;AACxB,UAAM,KAAK,MAAM,MAAM,GAAI;EAC7B;AAGA,MAAI,MAAM,EAAG,QAAO;AAEpB,SAAO;AACT;AAMO,SAAS,kBAAkB,KAAkC;AAClE,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAE9C,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO,MAAM,GAAG,IAAI,SAAY;EAClC;AAEA,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,SAAS,OAAO,OAAO;AAC7B,WAAO,MAAM,MAAM,IAAI,SAAY;EACrC;AAEA,SAAO;AACT;AAKA,IAAM,oBAAoB,oBAAI,IAAI;EAChC;EAAQ;EAAc;EAAW;EACjC;EAAS;EAAS;EAAU;AAC9B,CAAC;AAKM,SAAS,oBAAoB,KAAsB;AACxD,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,SAAO,kBAAkB,IAAI,GAAG,IAAI,MAAM;AAC5C;AASO,IAAM,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAK1C,IAAM,aAAa,EAAE,OAAO,EAAE,KAAK;AAKnC,IAAM,aAAa,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC;AAKtC,IAAM,qBAAqB,EAAE,KAAK;EACvC;;EACA;;EACA;;EACA;;EACA;;EACA;;EACA;;EACA;;AACF,CAAC;AAKM,IAAM,sBAAsB,EAAE,KAAK,CAAC,WAAW,aAAa,QAAQ,CAAC;AAMrE,IAAM,kBAAkB,EAAE;EAC/B;EACA,EAAE,KAAK;IACL;IACA;IACA;IACA;IACA;IACA;IACA;IACA;EACF,CAAC;AACH;AAKO,IAAM,wBAAwB,EAAE,OAAO;EAC5C,MAAM;EACN,KAAK,EAAE,OAAO;EACd,MAAM,EAAE,OAAO;EACf,KAAK,EAAE,OAAO;AAChB,CAAC;AAKM,IAAM,uBAAuB,EAAE,OAAO;EAC3C,YAAY;EACZ,MAAM,EAAE,WAAW,UAAU;EAC7B,UAAU,EAAE,OAAO;AACrB,CAAC;ACnJM,IAAM,0BAA0BA,GAAE,OAAO;EAC9C,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;;EACnC,SAASA,GAAE,OAAO;EAClB,SAASA,GAAE,QAAQ,EAAE,QAAQ,IAAI;;EACjC,iBAAiBA,GAAE,WAAW,CAAC,MAAM,KAAK,GAAGA,GAAE,OAAO,EAAE,IAAI,CAAC;;EAE7D,YAAYA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;EAC3C,gBAAgBA,GAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;EAChD,MAAMA,GAAE,OAAO,EAAE,SAAS;EAC1B,UAAUA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;EAC/C,IAAIA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;EACzC,SAASA,GAAE,OAAO,EAAE,SAAS,EAAE,SAAS;EACxC,WAAWA,GAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;EAC3C,gBAAgBA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;EACxD,UAAUA,GAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;EAC1C,UAAUA,GAAE,MAAM,CAACA,GAAE,KAAK,CAAC,eAAe,cAAc,SAAS,CAAC,GAAGA,GAAE,OAAO,EAAE,IAAI,GAAGA,GAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AAC7H,CAAC,EAAE,YAAY;AAMR,IAAM,0BAA0BA,GAAE,OAAO;EAC9C,MAAMA,GAAE,OAAO,EAAE,SAAS;EAC1B,aAAaA,GAAE,OAAO,EAAE,SAAS;EACjC,YAAYA,GAAE,WAAW,mBAAmBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC;EACrF,cAAcA,GAAE,WAAW,mBAAmBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC;EACvF,oBAAoBA,GAAE,QAAQ,EAAE,SAAS;EACzC,YAAYA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;EAC3C,SAASA,GAAE,MAAM,uBAAuB;AAC1C,CAAC;AAKM,IAAM,iBAAiBA,GAAE,OAAO;;EAErC,MAAMA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAC3B,aAAaA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAClC,aAAaA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE;;EAC7C,UAAUA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAC/B,WAAWA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAChC,aAAaA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE;;;EAE7C,eAAeA,GAAE,OAAO,EAAE,SAAS;EACnC,eAAeA,GAAE,OAAO,EAAE,SAAS;EACnC,2BAA2BA,GAAE,OAAO,EAAE,SAAS;EAC/C,qBAAqBA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;EAClD,gBAAgB,wBAAwB,SAAS,EAAE,SAAS;EAC5D,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;EACnC,SAASA,GAAE,OAAO,EAAE,SAAS;EAC7B,mBAAmBA,GAAE,OAAO,EAAE,SAAS;EACvC,YAAYA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;AAC7C,CAAC;AAKM,IAAM,oBAAoBA,GAAE,OAAO;EACxC,MAAMA,GAAE,QAAQ,eAAe;EAC/B,cAAcA,GAAE,QAAQ,KAAK;EAC7B,MAAM;AACR,CAAC;AC9DM,IAAM,0BAA0BC,GAAE,OAAO;EAC9C,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;;EACnC,SAASA,GAAE,OAAO;EAClB,SAASA,GAAE,QAAQ,EAAE,QAAQ,IAAI;;EACjC,iBAAiBA,GAAE,WAAW,CAAC,MAAM,KAAK,GAAGA,GAAE,OAAO,EAAE,IAAI,CAAC;;EAE7D,gBAAgBA,GAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;EAChD,MAAMA,GAAE,OAAO,EAAE,SAAS;EAC1B,UAAUA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;EAC/C,IAAIA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;EACzC,SAASA,GAAE,OAAO,EAAE,SAAS,EAAE,SAAS;EACxC,WAAWA,GAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;EAC3C,gBAAgBA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS;EACxD,UAAUA,GAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;EAC1C,UAAUA,GAAE,MAAM,CAACA,GAAE,KAAK,CAAC,eAAe,cAAc,SAAS,CAAC,GAAGA,GAAE,OAAO,EAAE,IAAI,GAAGA,GAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;EAC3H,YAAYA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;;EAE3C,eAAeA,GAAE,OAAO,EAAE,SAAS;EACnC,MAAMA,GAAE,MAAM,CAACA,GAAE,KAAK,CAAC,UAAU,QAAQ,WAAW,CAAC,GAAGA,GAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;EAC/F,OAAOA,GAAE,OAAO,EAAE,SAAS;EAC3B,gBAAgBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS;EACxD,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;;EACjD,WAAWA,GAAE,QAAQ,EAAE,SAAS;EAChC,OAAOA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS;EAC/C,iBAAiBA,GAAE,MAAM,CAACA,GAAE,KAAK,CAAC,OAAO,KAAK,CAAC,GAAGA,GAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS;AAChF,CAAC,EAAE,YAAY;AAMR,IAAM,0BAA0BA,GAAE,OAAO;EAC9C,MAAMA,GAAE,OAAO,EAAE,SAAS;EAC1B,aAAaA,GAAE,OAAO,EAAE,SAAS;EACjC,YAAYA,GAAE,WAAW,mBAAmBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC;EACrF,cAAcA,GAAE,WAAW,mBAAmBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC;EACvF,oBAAoBA,GAAE,QAAQ,EAAE,SAAS;EACzC,YAAYA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;EAC3C,SAASA,GAAE,MAAM,uBAAuB;AAC1C,CAAC;AASM,IAAM,sBAAsBA,GAAE,OAAO;;EAE1C,MAAMA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAC3B,aAAaA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAClC,aAAaA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE;;EAC7C,UAAUA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAC/B,WAAWA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAChC,aAAaA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE;;;EAE7C,SAASA,GAAE,OAAO,EAAE,QAAQ,EAAE;EAC9B,mBAAmBA,GAAE,OAAO,EAAE,QAAQ,EAAE;EACxC,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;EACpC,sBAAsBA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;;EAEpD,eAAeA,GAAE,OAAO,EAAE,SAAS;EACnC,eAAeA,GAAE,OAAO,EAAE,SAAS;EACnC,2BAA2BA,GAAE,OAAO,EAAE,SAAS;EAC/C,qBAAqBA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;EAClD,gBAAgB,wBAAwB,SAAS,EAAE,SAAS;EAC5D,YAAYA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;;EAE3C,QAAQA,GAAE,MAAM,qBAAqB,EAAE,SAAS;EAChD,UAAUA,GAAE,OAAO,EAAE,SAAS;EAC9B,4BAA4BA,GAAE,OAAOA,GAAE,OAAO,CAAC,EAAE,SAAS;EAC1D,QAAQA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;;EAErC,eAAeA,GAAE,WAAW,qBAAqBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC;EAC1F,mBAAmBA,GAAE,WAAW,qBAAqBA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC;AAChG,CAAC;AAKM,IAAM,iBAAiBA,GAAE,OAAO;EACrC,MAAMA,GAAE,QAAQ,eAAe;EAC/B,cAAcA,GAAE,QAAQ,KAAK;EAC7B,MAAM;AACR,CAAC;ACxCM,SAAS,kBAAkB,YAA+C;AAC/E,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,YAAY,cAAc,UAAU;AAC7C;;;AO1DA,SAAS,eAA8B;AD0BvC,IAAM,kBAAoE;EACxE,aAAa,KAAK,OAAO;;EACzB,cAAc,KAAK,OAAO;;EAC1B,cAAc,MAAM,OAAO;;EAC3B,eAAe;EACf,qBAAqB;AACvB;AASO,SAAS,QAAQ,MAA2B;AACjD,QAAM,YAAY,aAAa,IAAI;AACnC,MAAI,YAAY,EAAG,QAAO;AAE1B,QAAM,UAAU,KAAK,SAAS,SAAS;AACvC,QAAM,iBAAiB,IAAI,YAAY,EAAE,OAAO,WAAW;AAI3D,QAAM,WAAW,KAAK,IAAI,QAAQ,QAAQ,KAAK;AAC/C,QAAM,cAAc,QAAQ,SAAS;AAErC,WAAS,IAAI,aAAa,IAAI,QAAQ,SAAS,eAAe,QAAQ,KAAK;AACzE,QAAI,QAAQ;AACZ,aAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,UAAI,QAAQ,IAAI,CAAC,MAAM,eAAe,CAAC,GAAG;AACxC,gBAAQ;AACR;MACF;IACF;AACA,QAAI,MAAO,QAAO;EACpB;AAEA,SAAO;AACT;AAUO,SAAS,UACd,MACA,UAA4B,CAAC,GAClB;AACX,QAAM,OAAO,EAAE,GAAG,iBAAiB,GAAG,QAAQ;AAK9C,MAAI;AACJ,MAAI;AACF,eAAW,mBAAmB,MAAM;MAClC,aAAa,KAAK;MAClB,cAAc,KAAK;MACnB,UAAU;;IACZ,CAAC;EACH,SAAS,KAAK;AACZ,QAAI,eAAe,mBAAmB;AACpC,YAAM,IAAI;QACR,IAAI,aAAa,IAAI,aAAa;QAClC,IAAI,WAAW,IAAI,gBAAgB,KAAK;QACxC,IAAI,kBAAkB;MACxB;IACF;AACA,UAAM,IAAI;MACR,0BAA0B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;MAC1E;IACF;EACF;AAEA,MAAI,WAA4B;AAChC,QAAM,SAA2B,CAAC;AAClC,QAAM,WAAW,oBAAI,IAA4B;AACjD,MAAI;AAGJ,aAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAE3D,QAAI,SAAS,SAAS,GAAG,KAAK,SAAS,WAAW,EAAG;AAGrD,QAAI,aAAa,aAAa;AAC5B,UAAI,SAAS,SAAS,KAAK,aAAa;AACtC,cAAM,IAAI,eAAe,SAAS,QAAQ,KAAK,aAAa,WAAW;MACzE;AACA,UAAI;AACF,cAAM,UAAU,SAAS,QAAQ;AACjC,mBAAW,KAAK,MAAM,OAAO;MAC/B,SAAS,KAAK;AACZ,cAAM,IAAI;UACR,8BAA8B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;UAC9E;QACF;MACF;AACA;IACF;AAGA,QAAI,KAAK,eAAe;AACtB,YAAM,YAAY,SAAS,MAAM,uBAAuB;AACxD,UAAI,WAAW;AACb,cAAM,QAAQ,SAAS,UAAU,CAAC,GAAI,EAAE;AACxC,YAAI;AACF,gBAAM,UAAU,SAAS,QAAQ;AACjC,gBAAM,OAAO,KAAK,MAAM,OAAO;AAC/B,mBAAS,IAAI,OAAO,IAAI;QAC1B,QAAQ;QAER;AACA;MACF;IACF;AAGA,QAAI,aAAa,kBAAkB,KAAK,qBAAqB;AAC3D,oBAAc;AACd;IACF;AAGA,QAAI,SAAS,WAAW,SAAS,GAAG;AAClC,YAAM,OAAO,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK;AAC1C,YAAM,MAAM,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AAErC,aAAO,KAAK;QACV,MAAM;QACN,YAAY;UACV,MAAM;UACN,MAAM,KAAK,QAAQ,YAAY,EAAE;;UACjC,KAAK,aAAa,QAAQ;UAC1B;QACF;QACA,QAAQ;MACV,CAAC;AACD;IACF;EAGF;AAEA,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,WAAW,yCAAyC,OAAO;EACvE;AAGA,MAAI,SAAS,SAAS,iBAAiB;AACrC,UAAM,IAAI;MACR,qDAAqD,SAAS,IAAI;MAClE;IACF;EACF;AAGA,QAAM,gBAAgB,yBAAyB,QAAQ,SAAS,KAAK,UAAU,CAAC,CAAC;AAGjF,QAAM,eAAe,CAAC,CAAC,eAAe,kBAAkB,SAAS,KAAK,UAAU;AAEhF,SAAO;IACL,MAAM;IACN,QAAQ;IACR,UAAU,SAAS,OAAO,IAAI,WAAW;IACzC;IACA;EACF;AACF;AAQA,SAAS,yBACP,iBACA,aACkB;AAElB,QAAM,eAAe,oBAAI,IAA4B;AACrD,aAAW,SAAS,iBAAiB;AACnC,iBAAa,IAAI,MAAM,MAAM,KAAK;EACpC;AAEA,QAAM,UAA4B,CAAC;AAEnC,aAAW,cAAc,aAAa;AACpC,UAAM,SAAS,SAAS,WAAW,GAAG;AAEtC,QAAI,OAAO,WAAW,aAAa,OAAO,MAAM;AAE9C,YAAM,QAAQ,aAAa,IAAI,OAAO,IAAI;AAE1C,UAAI,OAAO;AACT,gBAAQ,KAAK;UACX,GAAG;UACH;QACF,CAAC;MACH,OAAO;AAEL,gBAAQ,KAAK;UACX,MAAM,OAAO;UACb;UACA,QAAQ;QACV,CAAC;MACH;IACF,WAAW,OAAO,WAAW,aAAa;AAExC,cAAQ,KAAK;QACX,MAAM;QACN;QACA,QAAQ;MACV,CAAC;IACH,WAAW,OAAO,WAAW,WAAW,OAAO,WAAW,QAAQ;AAEhE,cAAQ,KAAK;QACX,MAAM,WAAW;QACjB;QACA,QAAQ;MACV,CAAC;IACH,WAAW,OAAO,WAAW,QAAQ;AAEnC,UAAI,OAAO,QAAQ,OAAO,aAAa,UAAU;AAC/C,cAAM,SAAS,OAAa,OAAO,IAAI;AACvC,gBAAQ,KAAK;UACX,MAAM;UACN;UACA;QACF,CAAC;MACH,OAAO;AACL,gBAAQ,KAAK;UACX,MAAM;UACN;UACA,QAAQ;QACV,CAAC;MACH;IACF;EACF;AAEA,SAAO;AACT;AAKO,SAAS,iBAAiB,MAA4B;AAG3D,MAAI;AACJ,MAAI;AACF,eAAW,mBAAmB,MAAM;MAClC,aAAa,gBAAgB;;MAC7B,cAAc,gBAAgB;;MAC9B,UAAU;IACZ,CAAC;EACH,SAAS,KAAK;AACZ,UAAM,IAAI;MACR,0BAA0B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;MAC1E;IACF;EACF;AAEA,QAAM,WAAW,SAAS,WAAW;AACrC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,WAAW,qCAAqC,OAAO;EACnE;AAEA,MAAI;AACF,UAAM,UAAU,SAAS,QAAQ;AACjC,WAAO,KAAK,MAAM,OAAO;EAC3B,SAAS,KAAK;AACZ,UAAM,IAAI;MACR,8BAA8B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;MAC9E;IACF;EACF;AACF;AAKA,eAAsB,eACpB,MACA,UAA2F,CAAC,GACxE;AAEpB,QAAM,SAAS,UAAU,MAAM,OAAO;AAGtC,MAAI,CAAC,QAAQ,qBAAqB,CAAC,QAAQ,cAAc;AACvD,WAAO;EACT;AAGA,QAAM,gBAAgB,MAAM,QAAQ;IAClC,OAAO,OAAO,IAAI,OAAO,UAAU;AAEjC,UAAI,MAAM,QAAQ;AAChB,eAAO;MACT;AAEA,YAAM,SAAS,SAAS,MAAM,WAAW,GAAG;AAE5C,WAAK,OAAO,WAAW,WAAW,OAAO,WAAW,WAAW,OAAO,KAAK;AACzE,YAAI;AACF,gBAAM,SAAS,MAAM,QAAQ,aAAc,OAAO,GAAG;AACrD,cAAI,QAAQ;AACV,mBAAO,EAAE,GAAG,OAAO,OAAO;UAC5B;QACF,QAAQ;QAER;MACF;AAEA,aAAO;IACT,CAAC;EACH;AAEA,SAAO;IACL,GAAG;IACH,QAAQ;EACV;AACF;ACtVA,IAAM,mBAAmB,oBAAI,IAAI;EAC/B;EAAQ;EAAa;EAAW;EAAc;EAAS;EACvD;EAAU;EAAgB;EAAQ;AACpC,CAAC;AAKD,SAAS,iBAAiB,UAA0B;AAClD,MAAI,SAAS,WAAW,QAAQ,EAAG,QAAO;AAC1C,MAAI,SAAS,WAAW,QAAQ,EAAG,QAAO;AAC1C,MAAI,SAAS,WAAW,QAAQ,EAAG,QAAO;AAC1C,SAAO;AACT;AAMA,SAAS,kBAAkB,MAAsB;AAE/C,QAAM,aAAa,KAAK,YAAY,EAAE,QAAQ,gBAAgB,GAAG;AAGjE,MAAI,iBAAiB,IAAI,UAAU,GAAG;AACpC,WAAO;EACT;AAGA,QAAM,YAAY,WAAW,QAAQ,cAAc,EAAE;AACrD,SAAO,aAAa;AACtB;AAUA,SAAS,kBAAkB,KAAqB;AAC9C,QAAM,aAAa,IAAI,KAAK,EAAE,QAAQ,OAAO,EAAE,EAAE,YAAY;AAE7D,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,0CAA0C;EAC5D;AAEA,MAAI,WAAW,SAAS,IAAI;AAC1B,UAAM,IAAI,MAAM,sCAAsC,WAAW,MAAM,SAAS;EAClF;AAGA,MAAI,WAAW,SAAS,GAAG,KAAK,WAAW,SAAS,IAAI,KAAK,WAAW,SAAS,IAAI,GAAG;AACtF,UAAM,IAAI,MAAM,0DAA0D;EAC5E;AAGA,MAAI,CAAC,yBAAyB,KAAK,UAAU,GAAG;AAC9C,UAAM,IAAI,MAAM,6BAA6B,GAAG,GAAG;EACrD;AAEA,SAAO;AACT;AAKA,SAAS,aAAa,MAAc,KAAqB;AACvD,MAAI,WAAW;AAGf,MAAI,SAAS,YAAY,EAAE,SAAS,IAAI,IAAI,YAAY,CAAC,EAAE,GAAG;AAC5D,eAAW,SAAS,UAAU,GAAG,SAAS,UAAU,IAAI,SAAS,EAAE;EACrE;AAGA,aAAW,SACR,QAAQ,SAAS,GAAG,EACpB,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,OAAO,GAAG,EAClB,QAAQ,YAAY,EAAE;AAEzB,MAAI,CAAC,SAAU,YAAW;AAE1B,SAAO;AACT;AAKO,SAAS,WACd,MACA,QACA,UAA6B,CAAC,GACZ;AAClB,QAAM;IACJ,OAAO;IACP,mBAAmB;IACnB,YAAY,SAAS;IACrB,aAAa;IACb;EACF,IAAI;AAGJ,QAAM,kBAAkB,mBAAmB,MAAM,MAAM;AAGvD,QAAM,aAAuB,CAAC;AAG9B,QAAM,WAAW,KAAK,UAAU,iBAAiB,MAAM,CAAC;AACxD,aAAW,WAAW,IAAI,CAAC,WAAW,QAAQ,GAAG,EAAE,OAAO,iBAAqC,CAAC;AAGhG,MAAI,YAAY;AACd,UAAM,SAAS,cAAc,KAAK,KAAK,IAAI;;;;;;AAM3C,eAAW,YAAY,IAAI,CAAC,WAAW,MAAM,GAAG,EAAE,OAAO,iBAAqC,CAAC;EACjG;AAGA,MAAI,aAAa;AAEjB,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,QAAQ,OAAO,CAAC;AAEtB,UAAM,WAAW,kBAAkB,MAAM,IAAI;AAC7C,UAAM,UAAU,kBAAkB,MAAM,GAAG;AAC3C,UAAM,WAAW,mBAAmB,OAAO;AAC3C,UAAM,WAAW,iBAAiB,QAAQ;AAC1C,UAAM,WAAW,aAAa,MAAM,MAAM,OAAO;AAEjD,UAAM,YAAY,UAAU,QAAQ,IAAI,QAAQ,IAAI,QAAQ,IAAI,OAAO;AAEvE,eAAW,SAAS,IAAI,CAAC,MAAM,MAAM,EAAE,OAAO,iBAAqC,CAAC;AACpF;AAGA,QAAI,aAAa,SAAS,WAAW,QAAQ,GAAG;AAC9C,YAAM,WAAW,KAAK,UAAU;QAC9B,MAAM,SAAS,MAAM,GAAG,EAAE,CAAC,GAAG,YAAY,KAAK;MACjD,CAAC;AACD,iBAAW,UAAU,CAAC,OAAO,IAAI,CAAC,WAAW,QAAQ,GAAG,EAAE,OAAO,iBAAqC,CAAC;IACzG;EACF;AAGA,MAAI,aAAa;AACf,eAAW,cAAc,IAAI,CAAC,aAAa,EAAE,OAAO,iBAAqC,CAAC;EAC5F;AAGA,QAAM,SAAS,QAAQ,UAAU;AAEjC,SAAO;IACL;IACA;IACA,WAAW,OAAO;EACpB;AACF;AAKA,SAAS,mBAAmB,MAAgB,QAAqC;AAG/E,QAAM,cAAwB,OAAO,oBAAoB,aACrD,gBAAgB,IAAI,IACpB,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC;AAGnC,cAAY,KAAK,SAAS,OAAO,IAAI,CAAC,UAA2B;AAE/D,UAAM,WAAW,kBAAkB,MAAM,IAAI;AAC7C,UAAM,UAAU,kBAAkB,MAAM,GAAG;AAC3C,UAAM,WAAW,mBAAmB,OAAO;AAC3C,UAAM,WAAW,iBAAiB,QAAQ;AAC1C,UAAM,WAAW,aAAa,MAAM,MAAM,OAAO;AAEjD,WAAO;MACL,MAAM,MAAM;MACZ,KAAK,oBAAoB,QAAQ,IAAI,QAAQ,IAAI,QAAQ,IAAI,OAAO;MACpE,MAAM;MACN,KAAK;IACP;EACF,CAAC;AAED,SAAO;AACT;AAKA,eAAsB,gBACpB,MACA,QACA,UAA6B,CAAC,GACH;AAE3B,SAAO,WAAW,MAAM,QAAQ,OAAO;AACzC;","names":["z","z"]}
|
|
1
|
+
{"version":3,"sources":["../../core/src/binary.ts","../../core/src/base64.ts","../../core/src/errors.ts","../../core/src/data-url.ts","../../core/src/uri.ts","../../core/src/image.ts","../../core/src/uuid.ts","../../core/src/binary.ts","../../core/src/zip.ts","../../schemas/src/common.ts","../../schemas/src/ccv2.ts","../../schemas/src/ccv3.ts","../../schemas/src/risu.ts","../../schemas/src/normalized.ts","../../schemas/src/feature-deriver.ts","../../schemas/src/detection.ts","../../schemas/src/normalizer.ts","../../schemas/src/validation.ts","../../charx/src/reader.ts","../../charx/src/writer.ts"],"sourcesContent":["/**\n * Binary Data Utilities\n *\n * Universal binary data operations using Uint8Array.\n * Works in both Node.js and browser environments.\n */\n\n/**\n * Universal binary data type (works in both environments)\n */\nexport type BinaryData = Uint8Array;\n\n/**\n * Read a 32-bit big-endian unsigned integer\n */\nexport function readUInt32BE(data: BinaryData, offset: number): number {\n return (\n (data[offset]! << 24) |\n (data[offset + 1]! << 16) |\n (data[offset + 2]! << 8) |\n data[offset + 3]!\n ) >>> 0;\n}\n\n/**\n * Write a 32-bit big-endian unsigned integer\n */\nexport function writeUInt32BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 24) & 0xff;\n data[offset + 1] = (value >>> 16) & 0xff;\n data[offset + 2] = (value >>> 8) & 0xff;\n data[offset + 3] = value & 0xff;\n}\n\n/**\n * Read a 16-bit big-endian unsigned integer\n */\nexport function readUInt16BE(data: BinaryData, offset: number): number {\n return ((data[offset]! << 8) | data[offset + 1]!) >>> 0;\n}\n\n/**\n * Write a 16-bit big-endian unsigned integer\n */\nexport function writeUInt16BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 8) & 0xff;\n data[offset + 1] = value & 0xff;\n}\n\n/**\n * Find a byte sequence in binary data\n */\nexport function indexOf(data: BinaryData, search: BinaryData, fromIndex = 0): number {\n outer: for (let i = fromIndex; i <= data.length - search.length; i++) {\n for (let j = 0; j < search.length; j++) {\n if (data[i + j] !== search[j]) continue outer;\n }\n return i;\n }\n return -1;\n}\n\n/**\n * Concatenate multiple binary arrays\n */\nexport function concat(...arrays: BinaryData[]): BinaryData {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * Slice binary data (returns a view, not a copy)\n */\nexport function slice(data: BinaryData, start: number, end?: number): BinaryData {\n return data.subarray(start, end);\n}\n\n/**\n * Copy a portion of binary data (returns a new array)\n */\nexport function copy(data: BinaryData, start: number, end?: number): BinaryData {\n return data.slice(start, end);\n}\n\n/**\n * Convert string to binary (UTF-8)\n */\nexport function fromString(str: string): BinaryData {\n return new TextEncoder().encode(str);\n}\n\n/**\n * Convert binary to string (UTF-8)\n */\nexport function toString(data: BinaryData): string {\n return new TextDecoder().decode(data);\n}\n\n/**\n * Convert string to binary (Latin1 - for PNG keywords and similar)\n */\nexport function fromLatin1(str: string): BinaryData {\n const result = new Uint8Array(str.length);\n for (let i = 0; i < str.length; i++) {\n result[i] = str.charCodeAt(i) & 0xff;\n }\n return result;\n}\n\n/**\n * Convert binary to string (Latin1)\n */\nexport function toLatin1(data: BinaryData): string {\n let result = '';\n for (let i = 0; i < data.length; i++) {\n result += String.fromCharCode(data[i]!);\n }\n return result;\n}\n\n/**\n * Compare two binary arrays for equality\n */\nexport function equals(a: BinaryData, b: BinaryData): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Create a new Uint8Array filled with zeros\n */\nexport function alloc(size: number): BinaryData {\n return new Uint8Array(size);\n}\n\n/**\n * Create a Uint8Array from an array of numbers\n */\nexport function from(data: number[] | ArrayBuffer | BinaryData): BinaryData {\n if (data instanceof Uint8Array) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n return new Uint8Array(data);\n}\n\n/**\n * Check if value is a Uint8Array\n */\nexport function isBinaryData(value: unknown): value is BinaryData {\n return value instanceof Uint8Array;\n}\n\n/**\n * Convert Node.js Buffer to Uint8Array (no-op if already Uint8Array)\n * This provides compatibility when interfacing with Node.js code\n */\nexport function toUint8Array(data: BinaryData | Buffer): BinaryData {\n if (data instanceof Uint8Array) {\n // Buffer extends Uint8Array, but we want a plain Uint8Array\n // This ensures we get a proper Uint8Array in all cases\n if (Object.getPrototypeOf(data).constructor.name === 'Buffer') {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n return data;\n }\n return new Uint8Array(data);\n}\n\n/**\n * Convert binary data to hex string\n */\nexport function toHex(data: BinaryData): string {\n return Array.from(data)\n .map(b => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Convert hex string to binary data\n */\nexport function fromHex(hex: string): BinaryData {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n }\n return bytes;\n}\n","/**\n * Universal Base64 Encoding/Decoding\n *\n * Works in both Node.js and browser environments.\n */\n\nimport type { BinaryData } from './binary.js';\n\n/**\n * Check if we're in a Node.js environment\n */\nconst isNode = typeof process !== 'undefined' &&\n process.versions != null &&\n process.versions.node != null;\n\n/**\n * Threshold for switching to chunked encoding in browsers (1MB)\n * Below this, simple string concatenation is fast enough.\n * Above this, quadratic string growth becomes a problem.\n */\nconst LARGE_BUFFER_THRESHOLD = 1024 * 1024;\n\n/**\n * Encode binary data to base64 string\n *\n * PERFORMANCE: For large buffers (>1MB) in browsers, this automatically\n * uses the chunked implementation to avoid quadratic string concatenation.\n */\nexport function encode(data: BinaryData): string {\n if (isNode) {\n // Node.js: Buffer handles large data efficiently\n return Buffer.from(data).toString('base64');\n }\n\n // Browser: use chunked encoding for large buffers to avoid O(n²) string growth\n if (data.length > LARGE_BUFFER_THRESHOLD) {\n return encodeChunked(data);\n }\n\n // Small buffers: simple approach is fast enough\n let binary = '';\n for (let i = 0; i < data.length; i++) {\n binary += String.fromCharCode(data[i]!);\n }\n return btoa(binary);\n}\n\n/**\n * Decode base64 string to binary data\n */\nexport function decode(base64: string): BinaryData {\n if (isNode) {\n // Node.js: use Buffer\n return new Uint8Array(Buffer.from(base64, 'base64'));\n }\n\n // Browser: use atob\n const binary = atob(base64);\n const result = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n result[i] = binary.charCodeAt(i);\n }\n return result;\n}\n\n/**\n * Check if a string is valid base64\n */\nexport function isBase64(str: string): boolean {\n if (str.length === 0) return false;\n // Base64 regex: only valid base64 characters, length multiple of 4 (with padding)\n const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;\n return base64Regex.test(str) && str.length % 4 === 0;\n}\n\n/**\n * Encode binary data to URL-safe base64 string\n * Replaces + with -, / with _, and removes padding\n */\nexport function encodeUrlSafe(data: BinaryData): string {\n return encode(data)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Decode URL-safe base64 string to binary data\n */\nexport function decodeUrlSafe(base64: string): BinaryData {\n // Add back padding if needed\n let padded = base64\n .replace(/-/g, '+')\n .replace(/_/g, '/');\n\n while (padded.length % 4 !== 0) {\n padded += '=';\n }\n\n return decode(padded);\n}\n\n/**\n * Chunk size for encoding large buffers (64KB)\n * Prevents stack overflow when using String.fromCharCode with spread operator\n */\nconst ENCODE_CHUNK_SIZE = 64 * 1024;\n\n/**\n * Encode binary data to base64 string with chunking for large buffers.\n * Handles buffers >10MB without stack overflow.\n *\n * @param data - Binary data to encode\n * @returns Base64 encoded string\n *\n * @example\n * ```typescript\n * const largeBuffer = new Uint8Array(20 * 1024 * 1024); // 20MB\n * const base64 = encodeChunked(largeBuffer); // No stack overflow\n * ```\n */\nexport function encodeChunked(data: BinaryData): string {\n if (isNode) {\n // Node.js: Buffer handles large data efficiently\n return Buffer.from(data).toString('base64');\n }\n\n // Browser: process in chunks to avoid stack overflow\n const chunks: string[] = [];\n\n for (let i = 0; i < data.length; i += ENCODE_CHUNK_SIZE) {\n const chunk = data.subarray(i, Math.min(i + ENCODE_CHUNK_SIZE, data.length));\n let binary = '';\n for (let j = 0; j < chunk.length; j++) {\n binary += String.fromCharCode(chunk[j]!);\n }\n chunks.push(binary);\n }\n\n return btoa(chunks.join(''));\n}\n","/**\n * Error Classes\n *\n * Specific error types for character card operations.\n * All errors extend FoundryError for consistent handling.\n */\n\n/** Symbol to identify FoundryError instances across ESM/CJS boundaries */\nconst FOUNDRY_ERROR_MARKER = Symbol.for('@character-foundry/core:FoundryError');\n\n/**\n * Base error class for all Character Foundry errors\n */\nexport class FoundryError extends Error {\n /** @internal Marker for cross-module identification */\n readonly [FOUNDRY_ERROR_MARKER] = true;\n\n constructor(message: string, public readonly code: string) {\n super(message);\n this.name = 'FoundryError';\n // Maintains proper stack trace in V8 environments\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error during card parsing\n */\nexport class ParseError extends FoundryError {\n constructor(message: string, public readonly format?: string) {\n super(message, 'PARSE_ERROR');\n this.name = 'ParseError';\n }\n}\n\n/**\n * Error during card validation\n */\nexport class ValidationError extends FoundryError {\n constructor(message: string, public readonly field?: string) {\n super(message, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Asset not found in card or archive\n */\nexport class AssetNotFoundError extends FoundryError {\n constructor(public readonly uri: string) {\n super(`Asset not found: ${uri}`, 'ASSET_NOT_FOUND');\n this.name = 'AssetNotFoundError';\n }\n}\n\n/**\n * Format not supported for operation\n */\nexport class FormatNotSupportedError extends FoundryError {\n constructor(public readonly format: string, operation?: string) {\n const msg = operation\n ? `Format '${format}' not supported for ${operation}`\n : `Format not supported: ${format}`;\n super(msg, 'FORMAT_NOT_SUPPORTED');\n this.name = 'FormatNotSupportedError';\n }\n}\n\n/**\n * File size exceeds limits\n */\nexport class SizeLimitError extends FoundryError {\n constructor(\n public readonly actualSize: number,\n public readonly maxSize: number,\n context?: string\n ) {\n const actualMB = (actualSize / 1024 / 1024).toFixed(2);\n const maxMB = (maxSize / 1024 / 1024).toFixed(2);\n const msg = context\n ? `${context}: Size ${actualMB}MB exceeds limit ${maxMB}MB`\n : `Size ${actualMB}MB exceeds limit ${maxMB}MB`;\n super(msg, 'SIZE_LIMIT_EXCEEDED');\n this.name = 'SizeLimitError';\n }\n}\n\n/**\n * Path traversal or unsafe path detected\n */\nexport class PathTraversalError extends FoundryError {\n constructor(public readonly path: string) {\n super(`Unsafe path detected: ${path}`, 'PATH_TRAVERSAL');\n this.name = 'PathTraversalError';\n }\n}\n\n/**\n * Export operation would lose data\n */\nexport class DataLossError extends FoundryError {\n constructor(\n public readonly lostFields: string[],\n public readonly targetFormat: string\n ) {\n const fields = lostFields.slice(0, 3).join(', ');\n const more = lostFields.length > 3 ? ` and ${lostFields.length - 3} more` : '';\n super(\n `Export to ${targetFormat} would lose: ${fields}${more}`,\n 'DATA_LOSS'\n );\n this.name = 'DataLossError';\n }\n}\n\n/**\n * Check if an error is a FoundryError\n *\n * Uses Symbol.for() marker instead of instanceof to handle dual ESM/CJS package loading.\n * In dual-package environments, instanceof can fail if the error comes from a different\n * module instance (e.g., ESM vs CJS version of the same package). Symbol.for() creates\n * a global symbol shared across all module instances.\n */\nexport function isFoundryError(error: unknown): error is FoundryError {\n return (\n error instanceof Error &&\n FOUNDRY_ERROR_MARKER in error &&\n (error as Record<symbol, unknown>)[FOUNDRY_ERROR_MARKER] === true\n );\n}\n\n/**\n * Wrap unknown errors in a FoundryError\n */\nexport function wrapError(error: unknown, context?: string): FoundryError {\n if (isFoundryError(error)) {\n return error;\n }\n\n const message = error instanceof Error\n ? error.message\n : String(error);\n\n return new FoundryError(\n context ? `${context}: ${message}` : message,\n 'UNKNOWN_ERROR'\n );\n}\n","/**\n * Data URL Utilities\n *\n * Convert between Uint8Array buffers and data URLs.\n * Handles large buffers (>10MB) without stack overflow by processing in chunks.\n */\n\nimport type { BinaryData } from './binary.js';\nimport { encodeChunked as base64Encode, decode as base64Decode } from './base64.js';\nimport { ValidationError } from './errors.js';\n\n/**\n * Convert Uint8Array to data URL.\n * Handles large buffers (>10MB) without stack overflow by processing in chunks.\n *\n * @param buffer - Binary data to encode\n * @param mimeType - MIME type for the data URL (e.g., 'image/png', 'application/octet-stream')\n * @returns Data URL string\n *\n * @example\n * ```typescript\n * const png = new Uint8Array([...]);\n * const dataUrl = toDataURL(png, 'image/png');\n * // => \"data:image/png;base64,iVBORw0KGgo...\"\n * ```\n */\nexport function toDataURL(buffer: BinaryData, mimeType: string): string {\n // Use chunked encoding to handle large buffers without stack overflow\n const base64 = base64Encode(buffer);\n return `data:${mimeType};base64,${base64}`;\n}\n\n/**\n * Parse a data URL back to buffer and MIME type.\n * Validates the data URL format before parsing.\n *\n * @param dataUrl - Data URL string to parse\n * @returns Object containing the decoded buffer and MIME type\n * @throws Error if the data URL format is invalid\n *\n * @example\n * ```typescript\n * const { buffer, mimeType } = fromDataURL('data:image/png;base64,iVBORw0KGgo...');\n * // buffer: Uint8Array\n * // mimeType: 'image/png'\n * ```\n */\nexport function fromDataURL(dataUrl: string): { buffer: Uint8Array; mimeType: string } {\n // Validate data URL format\n if (!dataUrl.startsWith('data:')) {\n throw new ValidationError('Invalid data URL: must start with \"data:\"', 'dataUrl');\n }\n\n const commaIndex = dataUrl.indexOf(',');\n if (commaIndex === -1) {\n throw new ValidationError('Invalid data URL: missing comma separator', 'dataUrl');\n }\n\n const header = dataUrl.slice(5, commaIndex); // Skip 'data:'\n const data = dataUrl.slice(commaIndex + 1);\n\n // Parse header: [<mediatype>][;base64]\n let mimeType = 'text/plain';\n let isBase64 = false;\n\n const parts = header.split(';');\n for (const part of parts) {\n if (part === 'base64') {\n isBase64 = true;\n } else if (part && !part.includes('=')) {\n // MIME type (not a parameter like charset=utf-8)\n mimeType = part;\n }\n }\n\n if (!isBase64) {\n // URL-encoded text data\n throw new ValidationError('Non-base64 data URLs are not supported', 'dataUrl');\n }\n\n const buffer = base64Decode(data);\n return { buffer, mimeType };\n}\n\n/**\n * Check if a string is a valid data URL\n *\n * @param str - String to check\n * @returns true if the string is a valid data URL format\n */\nexport function isDataURL(str: string): boolean {\n if (!str.startsWith('data:')) return false;\n const commaIndex = str.indexOf(',');\n if (commaIndex === -1) return false;\n const header = str.slice(5, commaIndex);\n return header.includes('base64');\n}\n","/**\n * URI Utilities\n *\n * Handles different asset URI schemes used in character cards.\n * Supports: embeded://, embedded://, ccdefault:, https://, http://,\n * data:, file://, __asset:, asset:, chara-ext-asset_\n */\n\nexport type URIScheme =\n | 'embeded' // embeded:// (CharX standard, note intentional typo)\n | 'ccdefault' // ccdefault:\n | 'https' // https://\n | 'http' // http://\n | 'data' // data:mime;base64,...\n | 'file' // file://\n | 'internal' // Internal asset ID (UUID/string)\n | 'pngchunk' // PNG chunk reference (__asset:, asset:, chara-ext-asset_)\n | 'unknown';\n\nexport interface ParsedURI {\n scheme: URIScheme;\n originalUri: string;\n normalizedUri: string; // Normalized form of the URI\n path?: string; // For embeded://, file://\n url?: string; // For http://, https://\n data?: string; // For data: URIs\n mimeType?: string; // For data: URIs\n encoding?: string; // For data: URIs (e.g., base64)\n chunkKey?: string; // For pngchunk - the key/index to look up\n chunkCandidates?: string[]; // For pngchunk - all possible chunk keys to search\n}\n\n/**\n * Normalize a URI to its canonical form\n * Handles common typos and variant formats\n */\nexport function normalizeURI(uri: string): string {\n const trimmed = uri.trim();\n\n // Fix embedded:// -> embeded:// (common typo, CharX spec uses single 'd')\n if (trimmed.startsWith('embedded://')) {\n return 'embeded://' + trimmed.substring('embedded://'.length);\n }\n\n // Normalize PNG chunk references to pngchunk: scheme\n if (trimmed.startsWith('__asset:')) {\n const id = trimmed.substring('__asset:'.length);\n return `pngchunk:${id}`;\n }\n if (trimmed.startsWith('asset:')) {\n const id = trimmed.substring('asset:'.length);\n return `pngchunk:${id}`;\n }\n if (trimmed.startsWith('chara-ext-asset_:')) {\n const id = trimmed.substring('chara-ext-asset_:'.length);\n return `pngchunk:${id}`;\n }\n if (trimmed.startsWith('chara-ext-asset_')) {\n const id = trimmed.substring('chara-ext-asset_'.length);\n return `pngchunk:${id}`;\n }\n\n return trimmed;\n}\n\n/**\n * Parse a URI and determine its scheme and components\n */\nexport function parseURI(uri: string): ParsedURI {\n const trimmed = uri.trim();\n const normalized = normalizeURI(trimmed);\n\n // PNG chunk references (__asset:, asset:, chara-ext-asset_, pngchunk:)\n if (\n trimmed.startsWith('__asset:') ||\n trimmed.startsWith('asset:') ||\n trimmed.startsWith('chara-ext-asset_') ||\n trimmed.startsWith('pngchunk:')\n ) {\n let assetId: string;\n if (trimmed.startsWith('__asset:')) {\n assetId = trimmed.substring('__asset:'.length);\n } else if (trimmed.startsWith('asset:')) {\n assetId = trimmed.substring('asset:'.length);\n } else if (trimmed.startsWith('chara-ext-asset_:')) {\n assetId = trimmed.substring('chara-ext-asset_:'.length);\n } else if (trimmed.startsWith('pngchunk:')) {\n assetId = trimmed.substring('pngchunk:'.length);\n } else {\n assetId = trimmed.substring('chara-ext-asset_'.length);\n }\n\n // Generate all possible chunk key variations for lookup\n const candidates = [\n assetId, // \"0\" or \"filename.png\"\n trimmed, // Original URI\n `asset:${assetId}`, // \"asset:0\"\n `__asset:${assetId}`, // \"__asset:0\"\n `__asset_${assetId}`, // \"__asset_0\"\n `chara-ext-asset_${assetId}`, // \"chara-ext-asset_0\"\n `chara-ext-asset_:${assetId}`, // \"chara-ext-asset_:0\"\n `pngchunk:${assetId}`, // \"pngchunk:0\"\n ];\n\n return {\n scheme: 'pngchunk',\n originalUri: uri,\n normalizedUri: normalized,\n chunkKey: assetId,\n chunkCandidates: candidates,\n };\n }\n\n // ccdefault: - use default asset\n if (trimmed === 'ccdefault:' || trimmed.startsWith('ccdefault:')) {\n return {\n scheme: 'ccdefault',\n originalUri: uri,\n normalizedUri: normalized,\n };\n }\n\n // embeded:// or embedded:// (normalize typo)\n if (trimmed.startsWith('embeded://') || trimmed.startsWith('embedded://')) {\n const path = trimmed.startsWith('embeded://')\n ? trimmed.substring('embeded://'.length)\n : trimmed.substring('embedded://'.length);\n return {\n scheme: 'embeded',\n originalUri: uri,\n normalizedUri: normalized,\n path,\n };\n }\n\n // https://\n if (trimmed.startsWith('https://')) {\n return {\n scheme: 'https',\n originalUri: uri,\n normalizedUri: normalized,\n url: trimmed,\n };\n }\n\n // http://\n if (trimmed.startsWith('http://')) {\n return {\n scheme: 'http',\n originalUri: uri,\n normalizedUri: normalized,\n url: trimmed,\n };\n }\n\n // data: URIs\n if (trimmed.startsWith('data:')) {\n const parsed = parseDataURI(trimmed);\n return {\n scheme: 'data',\n originalUri: uri,\n normalizedUri: normalized,\n ...parsed,\n };\n }\n\n // file://\n if (trimmed.startsWith('file://')) {\n const path = trimmed.substring('file://'.length);\n return {\n scheme: 'file',\n originalUri: uri,\n normalizedUri: normalized,\n path,\n };\n }\n\n // Internal asset ID (alphanumeric/UUID format)\n if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {\n return {\n scheme: 'internal',\n originalUri: uri,\n normalizedUri: normalized,\n path: trimmed,\n };\n }\n\n // Unknown scheme\n return {\n scheme: 'unknown',\n originalUri: uri,\n normalizedUri: normalized,\n };\n}\n\n/**\n * Parse a data URI into its components\n * Format: data:[<mediatype>][;base64],<data>\n */\nfunction parseDataURI(uri: string): { mimeType?: string; encoding?: string; data?: string } {\n const match = uri.match(/^data:([^;,]+)?(;base64)?,(.*)$/);\n\n if (!match) {\n return {};\n }\n\n return {\n mimeType: match[1] || 'text/plain',\n encoding: match[2] ? 'base64' : undefined,\n data: match[3],\n };\n}\n\n/**\n * Check if extension is an image format\n */\nexport function isImageExt(ext: string): boolean {\n const imageExts = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'avif', 'bmp', 'svg'];\n return imageExts.includes(ext.toLowerCase());\n}\n\n/**\n * Check if extension is an audio format\n */\nexport function isAudioExt(ext: string): boolean {\n const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'];\n return audioExts.includes(ext.toLowerCase());\n}\n\n/**\n * Check if extension is a video format\n */\nexport function isVideoExt(ext: string): boolean {\n const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv'];\n return videoExts.includes(ext.toLowerCase());\n}\n\n/** Safe MIME types for data: URIs that can be used in href/src */\nconst SAFE_DATA_URI_MIME_TYPES = new Set([\n // Images (safe for img src)\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'image/avif',\n 'image/bmp',\n 'image/x-icon',\n // Audio (safe for audio src)\n 'audio/mpeg',\n 'audio/wav',\n 'audio/ogg',\n 'audio/flac',\n 'audio/mp4',\n 'audio/aac',\n // Video (safe for video src)\n 'video/mp4',\n 'video/webm',\n // Text/data (generally safe)\n 'text/plain',\n 'application/json',\n 'application/octet-stream',\n]);\n\n/** Potentially dangerous MIME types that should NOT be used in href/src */\nconst DANGEROUS_DATA_URI_MIME_TYPES = new Set([\n // Executable/script content\n 'text/html',\n 'text/javascript',\n 'application/javascript',\n 'application/x-javascript',\n 'text/css',\n 'image/svg+xml', // SVG can contain scripts\n 'application/xhtml+xml',\n 'application/xml',\n]);\n\n/**\n * Options for URI safety validation\n */\nexport interface URISafetyOptions {\n /** Allow http:// URIs (default: false) */\n allowHttp?: boolean;\n /** Allow file:// URIs (default: false) */\n allowFile?: boolean;\n /**\n * Allowed MIME types for data: URIs (default: all safe types).\n * Set to empty array to reject all data: URIs.\n * Set to undefined to use default safe list.\n */\n allowedDataMimes?: string[];\n}\n\n/**\n * Result of URI safety check with detailed information\n */\nexport interface URISafetyResult {\n /** Whether the URI is safe to use */\n safe: boolean;\n /** Reason if unsafe */\n reason?: string;\n /** Detected scheme */\n scheme: URIScheme;\n /** MIME type for data: URIs */\n mimeType?: string;\n}\n\n/**\n * Validate if a URI is safe to use (detailed version)\n *\n * @param uri - URI to validate\n * @param options - Safety options\n * @returns Detailed safety result\n */\nexport function checkURISafety(uri: string, options: URISafetyOptions = {}): URISafetyResult {\n const parsed = parseURI(uri);\n\n switch (parsed.scheme) {\n case 'embeded':\n case 'ccdefault':\n case 'internal':\n case 'https':\n case 'pngchunk':\n return { safe: true, scheme: parsed.scheme };\n\n case 'data': {\n const mimeType = parsed.mimeType || 'text/plain';\n\n // Check for explicitly dangerous MIME types\n if (DANGEROUS_DATA_URI_MIME_TYPES.has(mimeType)) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: `Data URI with potentially dangerous MIME type: ${mimeType}`,\n };\n }\n\n // If custom allowed list is provided, check against it\n if (options.allowedDataMimes !== undefined) {\n if (options.allowedDataMimes.length === 0) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: 'Data URIs are not allowed',\n };\n }\n if (!options.allowedDataMimes.includes(mimeType)) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: `Data URI MIME type not in allowed list: ${mimeType}`,\n };\n }\n }\n\n // Otherwise use default safe list\n if (!SAFE_DATA_URI_MIME_TYPES.has(mimeType)) {\n return {\n safe: false,\n scheme: parsed.scheme,\n mimeType,\n reason: `Unknown data URI MIME type: ${mimeType}`,\n };\n }\n\n return { safe: true, scheme: parsed.scheme, mimeType };\n }\n\n case 'http':\n if (options.allowHttp === true) {\n return { safe: true, scheme: parsed.scheme };\n }\n return { safe: false, scheme: parsed.scheme, reason: 'HTTP URIs are not allowed' };\n\n case 'file':\n if (options.allowFile === true) {\n return { safe: true, scheme: parsed.scheme };\n }\n return { safe: false, scheme: parsed.scheme, reason: 'File URIs are not allowed' };\n\n case 'unknown':\n default:\n return { safe: false, scheme: parsed.scheme, reason: 'Unknown URI scheme' };\n }\n}\n\n/**\n * Validate if a URI is safe to use (simple boolean version for backwards compatibility)\n *\n * @deprecated Use checkURISafety() for detailed safety information\n */\nexport function isURISafe(uri: string, options: { allowHttp?: boolean; allowFile?: boolean } = {}): boolean {\n return checkURISafety(uri, options).safe;\n}\n\n/**\n * Extract file extension from URI\n */\nexport function getExtensionFromURI(uri: string): string {\n const parsed = parseURI(uri);\n\n if (parsed.path) {\n const parts = parsed.path.split('.');\n if (parts.length > 1) {\n return parts[parts.length - 1]!.toLowerCase();\n }\n }\n\n if (parsed.url) {\n const urlParts = parsed.url.split('?')[0]!.split('.');\n if (urlParts.length > 1) {\n return urlParts[urlParts.length - 1]!.toLowerCase();\n }\n }\n\n if (parsed.mimeType) {\n return getExtFromMimeType(parsed.mimeType);\n }\n\n return 'unknown';\n}\n\n/**\n * Get MIME type from file extension\n */\nexport function getMimeTypeFromExt(ext: string): string {\n const extToMime: Record<string, string> = {\n // Images\n 'png': 'image/png',\n 'jpg': 'image/jpeg',\n 'jpeg': 'image/jpeg',\n 'webp': 'image/webp',\n 'gif': 'image/gif',\n 'avif': 'image/avif',\n 'svg': 'image/svg+xml',\n 'bmp': 'image/bmp',\n 'ico': 'image/x-icon',\n\n // Audio\n 'mp3': 'audio/mpeg',\n 'wav': 'audio/wav',\n 'ogg': 'audio/ogg',\n 'flac': 'audio/flac',\n 'm4a': 'audio/mp4',\n 'aac': 'audio/aac',\n\n // Video\n 'mp4': 'video/mp4',\n 'webm': 'video/webm',\n 'avi': 'video/x-msvideo',\n 'mov': 'video/quicktime',\n 'mkv': 'video/x-matroska',\n\n // Text/Data\n 'json': 'application/json',\n 'txt': 'text/plain',\n 'html': 'text/html',\n 'css': 'text/css',\n 'js': 'application/javascript',\n };\n\n return extToMime[ext.toLowerCase()] || 'application/octet-stream';\n}\n\n/**\n * Get file extension from MIME type\n */\nexport function getExtFromMimeType(mimeType: string): string {\n const mimeToExt: Record<string, string> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n 'image/avif': 'avif',\n 'image/svg+xml': 'svg',\n 'image/bmp': 'bmp',\n 'image/x-icon': 'ico',\n 'audio/mpeg': 'mp3',\n 'audio/wav': 'wav',\n 'audio/ogg': 'ogg',\n 'audio/flac': 'flac',\n 'audio/mp4': 'm4a',\n 'audio/aac': 'aac',\n 'video/mp4': 'mp4',\n 'video/webm': 'webm',\n 'video/x-msvideo': 'avi',\n 'video/quicktime': 'mov',\n 'video/x-matroska': 'mkv',\n 'application/json': 'json',\n 'text/plain': 'txt',\n 'text/html': 'html',\n 'text/css': 'css',\n 'application/javascript': 'js',\n };\n\n return mimeToExt[mimeType] || 'bin';\n}\n\n/**\n * Build a data URI from binary data and MIME type\n */\nexport function buildDataURI(data: string, mimeType: string, isBase64 = true): string {\n if (isBase64) {\n return `data:${mimeType};base64,${data}`;\n }\n return `data:${mimeType},${encodeURIComponent(data)}`;\n}\n","/**\n * Image Analysis Utilities\n *\n * Detect properties of image files from binary data.\n */\n\nimport {\n type BinaryData,\n indexOf,\n fromLatin1,\n} from './binary.js';\n\n/**\n * Check if an image buffer contains animation data.\n * Supports: APNG, WebP (Animated), GIF\n */\nexport function isAnimatedImage(data: BinaryData, _mimeType?: string): boolean {\n // 1. WebP Detection\n // RIFF .... WEBP\n if (\n data.length > 12 &&\n data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 && // RIFF\n data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50 // WEBP\n ) {\n // Check for VP8X chunk\n // VP8X chunk header: 'VP8X' (bytes 12-15)\n if (\n data[12] === 0x56 && data[13] === 0x50 && data[14] === 0x38 && data[15] === 0x58\n ) {\n // Flags byte is at offset 20 (16 + 4 bytes chunk size)\n // Animation bit is bit 1 (0x02)\n const flags = data[20];\n return (flags! & 0x02) !== 0;\n }\n return false;\n }\n\n // 2. PNG/APNG Detection\n // Signature: 89 50 4E 47 0D 0A 1A 0A\n if (\n data.length > 8 &&\n data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E && data[3] === 0x47\n ) {\n // Search for 'acTL' chunk (Animation Control)\n // It must appear before IDAT.\n // Simple search: indexOf('acTL')\n // Note: theoretically 'acTL' string could appear in other data, but highly unlikely in valid PNG structure before IDAT\n // We can iterate chunks to be safe, but indexOf is faster for a quick check\n const actlSig = fromLatin1('acTL');\n const idatSig = fromLatin1('IDAT');\n \n const actlIndex = indexOf(data, actlSig);\n if (actlIndex === -1) return false;\n\n const idatIndex = indexOf(data, idatSig);\n // If acTL exists and is before the first IDAT (or IDAT not found yet), it's APNG\n return idatIndex === -1 || actlIndex < idatIndex;\n }\n\n // 3. GIF Detection\n // Signature: GIF87a or GIF89a\n if (\n data.length > 6 &&\n data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 // GIF\n ) {\n // Check for NETSCAPE2.0 extension (looping animation)\n // This is a heuristic. Static GIFs are rare in this domain but possible.\n // Full frame counting is expensive. Presence of NETSCAPE block is a strong indicator.\n const netscape = fromLatin1('NETSCAPE2.0');\n return indexOf(data, netscape) !== -1;\n }\n\n return false;\n}\n","/**\n * UUID Generation Utilities\n *\n * Provides crypto-grade UUID v4 generation that works in Node.js,\n * browsers (secure contexts), and falls back gracefully.\n */\n\n/**\n * Format 16 random bytes as a UUID v4 string\n */\nfunction formatUUID(bytes: Uint8Array): string {\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n/**\n * Fallback UUID generation using Math.random()\n * Only used when crypto APIs are unavailable (rare)\n */\nfunction mathRandomUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generate a cryptographically secure UUID v4.\n *\n * Uses crypto.randomUUID() when available (Node.js 19+, modern browsers).\n * Falls back to crypto.getRandomValues() if randomUUID is unavailable.\n * Last resort uses Math.random() (non-secure, emits warning in dev).\n *\n * @returns A valid RFC 4122 UUID v4 string\n *\n * @example\n * ```typescript\n * const id = generateUUID();\n * // => \"550e8400-e29b-41d4-a716-446655440000\"\n * ```\n */\nexport function generateUUID(): string {\n // Node.js 19+ or browser with secure context\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n\n // Fallback using crypto.getRandomValues (older Node/browsers)\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n // Set version (4) and variant (RFC 4122)\n bytes[6] = (bytes[6]! & 0x0f) | 0x40; // Version 4\n bytes[8] = (bytes[8]! & 0x3f) | 0x80; // Variant 1\n return formatUUID(bytes);\n }\n\n // Last resort - non-secure fallback\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {\n console.warn('[character-foundry/core] generateUUID: Using insecure Math.random() fallback');\n }\n return mathRandomUUID();\n}\n\n/**\n * Validate if a string is a valid UUID v4\n *\n * @param uuid - String to validate\n * @returns true if valid UUID v4 format\n */\nexport function isValidUUID(uuid: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid);\n}\n","/**\n * Binary Data Utilities\n *\n * Universal binary data operations using Uint8Array.\n * Works in both Node.js and browser environments.\n */\n\n/**\n * Universal binary data type (works in both environments)\n */\nexport type BinaryData = Uint8Array;\n\n/**\n * Read a 32-bit big-endian unsigned integer\n */\nexport function readUInt32BE(data: BinaryData, offset: number): number {\n return (\n (data[offset]! << 24) |\n (data[offset + 1]! << 16) |\n (data[offset + 2]! << 8) |\n data[offset + 3]!\n ) >>> 0;\n}\n\n/**\n * Write a 32-bit big-endian unsigned integer\n */\nexport function writeUInt32BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 24) & 0xff;\n data[offset + 1] = (value >>> 16) & 0xff;\n data[offset + 2] = (value >>> 8) & 0xff;\n data[offset + 3] = value & 0xff;\n}\n\n/**\n * Read a 16-bit big-endian unsigned integer\n */\nexport function readUInt16BE(data: BinaryData, offset: number): number {\n return ((data[offset]! << 8) | data[offset + 1]!) >>> 0;\n}\n\n/**\n * Write a 16-bit big-endian unsigned integer\n */\nexport function writeUInt16BE(data: BinaryData, value: number, offset: number): void {\n data[offset] = (value >>> 8) & 0xff;\n data[offset + 1] = value & 0xff;\n}\n\n/**\n * Find a byte sequence in binary data\n */\nexport function indexOf(data: BinaryData, search: BinaryData, fromIndex = 0): number {\n outer: for (let i = fromIndex; i <= data.length - search.length; i++) {\n for (let j = 0; j < search.length; j++) {\n if (data[i + j] !== search[j]) continue outer;\n }\n return i;\n }\n return -1;\n}\n\n/**\n * Concatenate multiple binary arrays\n */\nexport function concat(...arrays: BinaryData[]): BinaryData {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * Slice binary data (returns a view, not a copy)\n */\nexport function slice(data: BinaryData, start: number, end?: number): BinaryData {\n return data.subarray(start, end);\n}\n\n/**\n * Copy a portion of binary data (returns a new array)\n */\nexport function copy(data: BinaryData, start: number, end?: number): BinaryData {\n return data.slice(start, end);\n}\n\n/**\n * Convert string to binary (UTF-8)\n */\nexport function fromString(str: string): BinaryData {\n return new TextEncoder().encode(str);\n}\n\n/**\n * Convert binary to string (UTF-8)\n */\nexport function toString(data: BinaryData): string {\n return new TextDecoder().decode(data);\n}\n\n/**\n * Convert string to binary (Latin1 - for PNG keywords and similar)\n */\nexport function fromLatin1(str: string): BinaryData {\n const result = new Uint8Array(str.length);\n for (let i = 0; i < str.length; i++) {\n result[i] = str.charCodeAt(i) & 0xff;\n }\n return result;\n}\n\n/**\n * Convert binary to string (Latin1)\n */\nexport function toLatin1(data: BinaryData): string {\n let result = '';\n for (let i = 0; i < data.length; i++) {\n result += String.fromCharCode(data[i]!);\n }\n return result;\n}\n\n/**\n * Compare two binary arrays for equality\n */\nexport function equals(a: BinaryData, b: BinaryData): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Create a new Uint8Array filled with zeros\n */\nexport function alloc(size: number): BinaryData {\n return new Uint8Array(size);\n}\n\n/**\n * Create a Uint8Array from an array of numbers\n */\nexport function from(data: number[] | ArrayBuffer | BinaryData): BinaryData {\n if (data instanceof Uint8Array) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n return new Uint8Array(data);\n}\n\n/**\n * Check if value is a Uint8Array\n */\nexport function isBinaryData(value: unknown): value is BinaryData {\n return value instanceof Uint8Array;\n}\n\n/**\n * Convert Node.js Buffer to Uint8Array (no-op if already Uint8Array)\n * This provides compatibility when interfacing with Node.js code\n */\nexport function toUint8Array(data: BinaryData | Buffer): BinaryData {\n if (data instanceof Uint8Array) {\n // Buffer extends Uint8Array, but we want a plain Uint8Array\n // This ensures we get a proper Uint8Array in all cases\n if (Object.getPrototypeOf(data).constructor.name === 'Buffer') {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n return data;\n }\n return new Uint8Array(data);\n}\n\n/**\n * Convert binary data to hex string\n */\nexport function toHex(data: BinaryData): string {\n return Array.from(data)\n .map(b => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Convert hex string to binary data\n */\nexport function fromHex(hex: string): BinaryData {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n }\n return bytes;\n}\n","/**\n * ZIP Utility Functions\n *\n * Handles ZIP format detection and SFX (self-extracting) archive support.\n * Uses Uint8Array for universal browser/Node.js compatibility.\n */\n\nimport { indexOf, concat, type BinaryData } from './binary.js';\nimport { Unzip, UnzipInflate, UnzipPassThrough, type Unzipped, type UnzipFile } from 'fflate';\n\n// ZIP local file header signature: PK\\x03\\x04\nexport const ZIP_SIGNATURE = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);\n\n// JPEG signatures\nexport const JPEG_SIGNATURE = new Uint8Array([0xff, 0xd8, 0xff]);\n\n/**\n * How to handle unsafe paths (with .. or absolute paths) during extraction.\n *\n * - 'skip': Silently skip unsafe files (default, backwards compatible)\n * - 'warn': Skip and call onUnsafePath callback for logging/monitoring\n * - 'reject': Throw ZipPreflightError immediately (strictest, recommended for untrusted input)\n */\nexport type UnsafePathHandling = 'skip' | 'warn' | 'reject';\n\n/**\n * Size limits for ZIP operations\n */\nexport interface ZipSizeLimits {\n /** Max size per file (default 50MB) */\n maxFileSize: number;\n /** Max total size (default 200MB) */\n maxTotalSize: number;\n /** Max number of files (default 1000) */\n maxFiles: number;\n /**\n * How to handle files with unsafe paths (path traversal attempts).\n *\n * Default: 'skip' (backwards compatible - silently ignores unsafe paths)\n * Recommended: 'reject' for untrusted input (throws on path traversal)\n *\n * @security Path traversal (../) in ZIP entries can lead to arbitrary file\n * overwrites when extracted to disk. While this library returns files in\n * memory, consumers may write to disk using the paths.\n */\n unsafePathHandling?: UnsafePathHandling;\n /**\n * Callback invoked when unsafe path is detected (only with 'warn' handling).\n * Use for logging/monitoring path traversal attempts.\n */\n onUnsafePath?: (path: string, reason: string) => void;\n}\n\nexport const DEFAULT_ZIP_LIMITS: ZipSizeLimits = {\n maxFileSize: 50 * 1024 * 1024, // 50MB per file (Risu standard)\n maxTotalSize: 500 * 1024 * 1024, // 500MB total (CharX can have many expression assets)\n maxFiles: 10000, // CharX cards can have 2k+ expression assets\n unsafePathHandling: 'skip', // Backwards compatible default\n};\n\n/**\n * Check if a buffer contains ZIP data (anywhere in the buffer).\n * This handles both regular ZIPs and SFX (self-extracting) archives.\n * @param data - Binary data to check\n * @returns true if ZIP signature found\n */\nexport function isZipBuffer(data: BinaryData): boolean {\n return indexOf(data, ZIP_SIGNATURE) >= 0;\n}\n\n/**\n * Check if a buffer starts with ZIP signature (standard ZIP detection).\n * @param data - Binary data to check\n * @returns true if data starts with PK\\x03\\x04\n */\nexport function startsWithZipSignature(data: BinaryData): boolean {\n return (\n data.length >= 4 &&\n data[0] === 0x50 &&\n data[1] === 0x4b &&\n data[2] === 0x03 &&\n data[3] === 0x04\n );\n}\n\n/**\n * Check if data starts with JPEG signature\n */\nexport function isJPEG(data: BinaryData): boolean {\n return (\n data.length >= 3 &&\n data[0] === 0xff &&\n data[1] === 0xd8 &&\n data[2] === 0xff\n );\n}\n\n/**\n * Check if data is a JPEG with appended ZIP (JPEG+CharX hybrid)\n */\nexport function isJpegCharX(data: BinaryData): boolean {\n if (!isJPEG(data)) return false;\n // Look for ZIP signature after JPEG data\n return indexOf(data, ZIP_SIGNATURE) > 0;\n}\n\n/**\n * Find ZIP data start in buffer (handles SFX/self-extracting archives).\n * SFX archives have an executable stub prepended to the ZIP data.\n *\n * @param data - Binary data that may contain ZIP data (possibly with SFX prefix)\n * @returns Binary data starting at ZIP signature, or original data if not found/already at start\n */\nexport function findZipStart(data: BinaryData): BinaryData {\n const index = indexOf(data, ZIP_SIGNATURE);\n\n if (index > 0) {\n // SFX archive detected - return data starting at ZIP signature\n return data.subarray(index);\n }\n\n // Either ZIP starts at 0, or no ZIP signature found - return original\n return data;\n}\n\n/**\n * Get the offset of ZIP data within a buffer.\n * @param data - Binary data to search\n * @returns Offset of ZIP signature, or -1 if not found\n */\nexport function getZipOffset(data: BinaryData): number {\n return indexOf(data, ZIP_SIGNATURE);\n}\n\n/**\n * Check if data is a valid ZIP archive (has signature at start or is SFX)\n * @param data - Binary data to check\n * @returns true if data contains valid ZIP structure\n */\nexport function isValidZip(data: BinaryData): boolean {\n const offset = getZipOffset(data);\n if (offset < 0) return false;\n\n // Check if there's enough data after the signature for a minimal ZIP\n // Minimum ZIP: local file header (30 bytes) + central directory (46 bytes) + end of central dir (22 bytes)\n return data.length - offset >= 98;\n}\n\n/**\n * Validate a path for directory traversal attacks\n * @param path - File path to validate\n * @returns true if path is safe\n */\nexport function isPathSafe(path: string): boolean {\n // Reject absolute paths\n if (path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {\n return false;\n }\n\n // Reject path traversal\n if (path.includes('..')) {\n return false;\n }\n\n // Reject backslashes (Windows-style paths that might be used for traversal)\n if (path.includes('\\\\')) {\n return false;\n }\n\n return true;\n}\n\n/**\n * ZIP Central Directory File Header structure\n */\nexport interface ZipCentralDirEntry {\n fileName: string;\n compressedSize: number;\n uncompressedSize: number;\n}\n\n/**\n * Result of preflight ZIP size check\n */\nexport interface ZipPreflightResult {\n entries: ZipCentralDirEntry[];\n totalUncompressedSize: number;\n fileCount: number;\n}\n\n/**\n * Error thrown when ZIP preflight fails due to size limits\n */\nexport class ZipPreflightError extends Error {\n constructor(\n message: string,\n public readonly totalSize?: number,\n public readonly maxSize?: number,\n public readonly oversizedEntry?: string,\n public readonly entrySize?: number,\n public readonly maxEntrySize?: number\n ) {\n super(message);\n this.name = 'ZipPreflightError';\n }\n}\n\n/**\n * Preflight check ZIP central directory to get uncompressed sizes BEFORE extraction.\n * This prevents zip bomb attacks by rejecting archives with dangerous compression ratios\n * or oversized entries without fully decompressing them.\n *\n * The ZIP format stores uncompressed sizes in the central directory at the end of the file.\n * This function reads that metadata without decompressing any actual data.\n *\n * @param data - ZIP file data (can be SFX/self-extracting, will find ZIP start)\n * @param limits - Size limits to enforce\n * @returns Preflight result with entry info and totals\n * @throws ZipPreflightError if limits would be exceeded\n */\nexport function preflightZipSizes(\n data: BinaryData,\n limits: ZipSizeLimits = DEFAULT_ZIP_LIMITS\n): ZipPreflightResult {\n // Find ZIP start (handles SFX/hybrid archives)\n const zipData = findZipStart(data);\n\n // Find End of Central Directory (EOCD) signature: PK\\x05\\x06\n // EOCD is at the end of the file, max comment size is 65535 bytes\n const eocdSignature = new Uint8Array([0x50, 0x4b, 0x05, 0x06]);\n const searchStart = Math.max(0, zipData.length - 65535 - 22);\n\n let eocdOffset = -1;\n for (let i = zipData.length - 22; i >= searchStart; i--) {\n if (\n zipData[i] === eocdSignature[0] &&\n zipData[i + 1] === eocdSignature[1] &&\n zipData[i + 2] === eocdSignature[2] &&\n zipData[i + 3] === eocdSignature[3]\n ) {\n // SECURITY: Validate comment length to avoid false positives\n // If the ZIP comment contains the EOCD signature bytes, we could\n // find the wrong location. Verify by checking comment length.\n // Comment length is at offset +20 (2 bytes, little-endian)\n if (i + 21 < zipData.length) {\n const commentLength = zipData[i + 20]! | (zipData[i + 21]! << 8);\n // True EOCD should be at: file_size - 22 - comment_length\n const expectedOffset = zipData.length - 22 - commentLength;\n if (i === expectedOffset) {\n eocdOffset = i;\n break;\n }\n // Otherwise, this is a false positive (signature in comment), continue searching\n }\n }\n }\n\n if (eocdOffset < 0) {\n throw new ZipPreflightError('Invalid ZIP: End of Central Directory not found');\n }\n\n // Parse EOCD\n // Offset 8: Total number of central directory records\n // Offset 12: Size of central directory\n // Offset 16: Offset of start of central directory\n const totalEntries = zipData[eocdOffset + 8]! | (zipData[eocdOffset + 9]! << 8);\n const _cdSize = readUInt32LEFromBytes(zipData, eocdOffset + 12);\n const cdOffset = readUInt32LEFromBytes(zipData, eocdOffset + 16);\n\n // Check file count limit\n if (totalEntries > limits.maxFiles) {\n throw new ZipPreflightError(\n `ZIP contains ${totalEntries} files, exceeds limit of ${limits.maxFiles}`,\n undefined,\n undefined,\n undefined,\n undefined,\n undefined\n );\n }\n\n // Parse Central Directory entries\n const entries: ZipCentralDirEntry[] = [];\n let totalUncompressedSize = 0;\n let offset = cdOffset;\n\n for (let i = 0; i < totalEntries && offset < eocdOffset; i++) {\n // Central Directory File Header signature: PK\\x01\\x02\n if (\n zipData[offset] !== 0x50 ||\n zipData[offset + 1] !== 0x4b ||\n zipData[offset + 2] !== 0x01 ||\n zipData[offset + 3] !== 0x02\n ) {\n throw new ZipPreflightError('Invalid ZIP: Central Directory header corrupted');\n }\n\n // Offset 20: Compressed size (4 bytes)\n const compressedSize = readUInt32LEFromBytes(zipData, offset + 20);\n // Offset 24: Uncompressed size (4 bytes)\n const uncompressedSize = readUInt32LEFromBytes(zipData, offset + 24);\n // Offset 28: File name length (2 bytes)\n const fileNameLength = zipData[offset + 28]! | (zipData[offset + 29]! << 8);\n // Offset 30: Extra field length (2 bytes)\n const extraLength = zipData[offset + 30]! | (zipData[offset + 31]! << 8);\n // Offset 32: File comment length (2 bytes)\n const commentLength = zipData[offset + 32]! | (zipData[offset + 33]! << 8);\n\n // Read file name\n const fileName = new TextDecoder().decode(\n zipData.subarray(offset + 46, offset + 46 + fileNameLength)\n );\n\n // Skip directories (names ending with /)\n if (!fileName.endsWith('/')) {\n // Check per-entry size limit\n if (uncompressedSize > limits.maxFileSize) {\n throw new ZipPreflightError(\n `File \"${fileName}\" uncompressed size ${uncompressedSize} exceeds limit ${limits.maxFileSize}`,\n undefined,\n undefined,\n fileName,\n uncompressedSize,\n limits.maxFileSize\n );\n }\n\n totalUncompressedSize += uncompressedSize;\n\n // Check total size limit early to fail fast\n if (totalUncompressedSize > limits.maxTotalSize) {\n throw new ZipPreflightError(\n `Total uncompressed size ${totalUncompressedSize} exceeds limit ${limits.maxTotalSize}`,\n totalUncompressedSize,\n limits.maxTotalSize\n );\n }\n\n entries.push({\n fileName,\n compressedSize,\n uncompressedSize,\n });\n }\n\n // Move to next entry\n offset += 46 + fileNameLength + extraLength + commentLength;\n }\n\n return {\n entries,\n totalUncompressedSize,\n fileCount: entries.length,\n };\n}\n\n/**\n * Read a 32-bit little-endian unsigned integer from bytes\n */\nfunction readUInt32LEFromBytes(data: BinaryData, offset: number): number {\n return (\n data[offset]! |\n (data[offset + 1]! << 8) |\n (data[offset + 2]! << 16) |\n (data[offset + 3]! << 24)\n ) >>> 0; // Convert to unsigned\n}\n\n/**\n * Streaming ZIP extraction with real-time byte limit enforcement.\n *\n * Unlike preflightZipSizes which only checks central directory metadata,\n * this function tracks ACTUAL decompressed bytes during extraction and\n * aborts immediately if limits are exceeded. This protects against\n * malicious archives that lie about sizes in their central directory.\n *\n * @security Path safety is enforced based on `limits.unsafePathHandling`:\n * - 'skip' (default): Silently ignores files with unsafe paths\n * - 'warn': Skips unsafe files and calls onUnsafePath callback\n * - 'reject': Throws ZipPreflightError on unsafe paths\n *\n * @performance SYNCHRONOUS BLOCKING OPERATION\n * This function runs synchronously and will block the event loop during\n * decompression. For large archives (>50MB), this can cause UI freezes\n * in browser environments or block the Node.js event loop.\n *\n * Recommendations:\n * - Browser: Use Web Workers for archives >10MB to avoid freezing the UI\n * - Node.js: Consider worker threads for archives >50MB\n * - All environments: Call preflightZipSizes() first to validate before extraction\n *\n * @param data - ZIP file data (can be SFX/self-extracting)\n * @param limits - Size limits to enforce\n * @returns Extracted files (synchronously)\n * @throws ZipPreflightError if limits are exceeded during extraction\n */\nexport function streamingUnzipSync(\n data: BinaryData,\n limits: ZipSizeLimits = DEFAULT_ZIP_LIMITS\n): Unzipped {\n // Find ZIP start (handles SFX/hybrid archives)\n const zipData = findZipStart(data);\n\n const result: Unzipped = {};\n let totalBytes = 0;\n let fileCount = 0;\n let error: Error | null = null;\n\n // Get path handling mode (default to 'skip' for backwards compatibility)\n const unsafePathHandling = limits.unsafePathHandling ?? 'skip';\n\n // Track chunks per file for concatenation\n const fileChunks = new Map<string, Uint8Array[]>();\n\n const unzipper = new Unzip((file: UnzipFile) => {\n if (error) return;\n\n // Skip directories\n if (file.name.endsWith('/')) {\n file.start();\n return;\n }\n\n // SECURITY: Check for path traversal attacks\n if (!isPathSafe(file.name)) {\n const reason = file.name.includes('..')\n ? 'path traversal (..)'\n : file.name.startsWith('/') || /^[a-zA-Z]:/.test(file.name)\n ? 'absolute path'\n : 'backslash in path';\n\n if (unsafePathHandling === 'reject') {\n error = new ZipPreflightError(\n `Unsafe path detected: \"${file.name}\" - ${reason}. ` +\n `This may be a path traversal attack.`\n );\n file.terminate();\n return;\n }\n\n if (unsafePathHandling === 'warn' && limits.onUnsafePath) {\n limits.onUnsafePath(file.name, reason);\n }\n\n // Skip this file (consume but don't store) for 'skip' and 'warn' modes\n // SECURITY: Still count bytes to prevent zip bombs hidden behind unsafe paths\n file.ondata = (err, chunk, _final) => {\n if (error) return;\n if (err) {\n error = err;\n return;\n }\n if (chunk && chunk.length > 0) {\n totalBytes += chunk.length;\n if (totalBytes > limits.maxTotalSize) {\n error = new ZipPreflightError(\n `Total actual size ${totalBytes} exceeds limit ${limits.maxTotalSize}`,\n totalBytes,\n limits.maxTotalSize\n );\n file.terminate();\n }\n }\n };\n file.start();\n return;\n }\n\n fileCount++;\n if (fileCount > limits.maxFiles) {\n error = new ZipPreflightError(\n `File count ${fileCount} exceeds limit ${limits.maxFiles}`\n );\n file.terminate();\n return;\n }\n\n const chunks: Uint8Array[] = [];\n fileChunks.set(file.name, chunks);\n let fileBytes = 0;\n\n file.ondata = (err, chunk, final) => {\n if (error) return;\n\n if (err) {\n error = err;\n return;\n }\n\n if (chunk && chunk.length > 0) {\n fileBytes += chunk.length;\n totalBytes += chunk.length;\n\n // Check per-file size limit (actual decompressed bytes)\n if (fileBytes > limits.maxFileSize) {\n error = new ZipPreflightError(\n `File \"${file.name}\" actual size ${fileBytes} exceeds limit ${limits.maxFileSize}`,\n undefined,\n undefined,\n file.name,\n fileBytes,\n limits.maxFileSize\n );\n file.terminate();\n return;\n }\n\n // Check total size limit (actual decompressed bytes)\n if (totalBytes > limits.maxTotalSize) {\n error = new ZipPreflightError(\n `Total actual size ${totalBytes} exceeds limit ${limits.maxTotalSize}`,\n totalBytes,\n limits.maxTotalSize\n );\n file.terminate();\n return;\n }\n\n chunks.push(chunk);\n }\n\n if (final && !error) {\n // Concatenate all chunks for this file\n result[file.name] = concat(...chunks);\n }\n };\n\n file.start();\n });\n\n // Register decompression handlers\n unzipper.register(UnzipInflate); // DEFLATE (compression method 8)\n unzipper.register(UnzipPassThrough); // Stored (compression method 0)\n\n // Push all data - fflate processes synchronously when given full buffer\n unzipper.push(zipData, true);\n\n // If an error occurred during processing, throw it\n if (error) {\n throw error;\n }\n\n return result;\n}\n\n/**\n * Re-export Unzipped type for convenience\n */\nexport type { Unzipped };\n","/**\n * Common Types\n *\n * Shared types used across all card formats.\n */\n\nimport { z } from 'zod';\n\n// ============================================================================\n// Zod Schemas\n// ============================================================================\n\n/**\n * ISO 8601 date string schema\n */\nexport const ISO8601Schema = z.string().datetime();\n\n/**\n * UUID string schema\n */\nexport const UUIDSchema = z.string().uuid();\n\n/**\n * Card specification version schema\n */\nexport const SpecSchema = z.enum(['v2', 'v3']);\n\n/**\n * Source format identifier schema\n */\nexport const SourceFormatSchema = z.enum([\n 'png_v2', // PNG with 'chara' chunk (v2)\n 'png_v3', // PNG with 'ccv3' chunk (v3)\n 'json_v2', // Raw JSON v2\n 'json_v3', // Raw JSON v3\n 'charx', // ZIP with card.json (v3 spec)\n 'charx_risu', // ZIP with card.json + module.risum\n 'charx_jpeg', // JPEG with appended ZIP (read-only)\n 'voxta', // VoxPkg format\n]);\n\n/**\n * Original JSON shape schema\n */\nexport const OriginalShapeSchema = z.enum(['wrapped', 'unwrapped', 'legacy']);\n\n/**\n * Asset type identifier schema\n */\nexport const AssetTypeSchema = z.enum([\n 'icon',\n 'background',\n 'emotion',\n 'user_icon',\n 'sound',\n 'video',\n 'custom',\n 'x-risu-asset',\n]);\n\n/**\n * Asset descriptor schema (v3 spec)\n */\nexport const AssetDescriptorSchema = z.object({\n type: AssetTypeSchema,\n uri: z.string(),\n name: z.string(),\n ext: z.string(),\n});\n\n/**\n * Extracted asset with binary data schema\n */\nexport const ExtractedAssetSchema = z.object({\n descriptor: AssetDescriptorSchema,\n data: z.instanceof(Uint8Array),\n mimeType: z.string(),\n});\n\n// ============================================================================\n// TypeScript Types (inferred from Zod schemas)\n// ============================================================================\n\n/**\n * ISO 8601 date string\n */\nexport type ISO8601 = z.infer<typeof ISO8601Schema>;\n\n/**\n * UUID string\n */\nexport type UUID = z.infer<typeof UUIDSchema>;\n\n/**\n * Card specification version\n */\nexport type Spec = z.infer<typeof SpecSchema>;\n\n/**\n * Source format identifier\n */\nexport type SourceFormat = z.infer<typeof SourceFormatSchema>;\n\n/**\n * Original JSON shape\n */\nexport type OriginalShape = z.infer<typeof OriginalShapeSchema>;\n\n/**\n * Asset type identifier\n */\nexport type AssetType = z.infer<typeof AssetTypeSchema>;\n\n/**\n * Asset descriptor (v3 spec)\n */\nexport type AssetDescriptor = z.infer<typeof AssetDescriptorSchema>;\n\n/**\n * Extracted asset with binary data\n */\nexport type ExtractedAsset = z.infer<typeof ExtractedAssetSchema>;\n","/**\n * Character Card v2 Types\n *\n * Based on: https://github.com/malfoyslastname/character-card-spec-v2\n */\n\nimport { z } from 'zod';\n\n// ============================================================================\n// Zod Schemas\n// ============================================================================\n\n/**\n * Lorebook entry schema for v2 cards\n */\nexport const CCv2LorebookEntrySchema = z.object({\n keys: z.array(z.string()).optional(), // Some tools use 'key' instead\n content: z.string(),\n enabled: z.boolean().default(true), // Default to enabled if missing\n insertion_order: z.preprocess((v) => v ?? 0, z.number().int()),\n // Optional fields - be lenient with nulls since wild data has them\n extensions: z.record(z.unknown()).optional(),\n case_sensitive: z.boolean().nullable().optional(),\n name: z.string().optional(),\n priority: z.number().int().nullable().optional(),\n id: z.number().int().nullable().optional(),\n comment: z.string().nullable().optional(),\n selective: z.boolean().nullable().optional(),\n secondary_keys: z.array(z.string()).nullable().optional(),\n constant: z.boolean().nullable().optional(),\n position: z.union([z.enum(['before_char', 'after_char', 'in_chat']), z.number().int(), z.literal('')]).nullable().optional(),\n}).passthrough(); // Allow SillyTavern extensions like depth, probability, etc.\n\n/**\n * Character book (lorebook) schema for v2 cards\n */\nexport const CCv2CharacterBookSchema = z.object({\n name: z.string().optional(),\n description: z.string().optional(),\n scan_depth: z.number().int().nonnegative().optional(),\n token_budget: z.number().int().nonnegative().optional(),\n recursive_scanning: z.boolean().optional(),\n extensions: z.record(z.unknown()).optional(),\n entries: z.array(CCv2LorebookEntrySchema),\n});\n\n/**\n * Character Card v2 data structure schema\n */\nexport const CCv2DataSchema = z.object({\n // Core fields - use .default('') to handle missing fields in malformed cards\n name: z.string().default(''),\n description: z.string().default(''),\n personality: z.string().nullable().default(''), // Can be null in wild (141 cards)\n scenario: z.string().default(''),\n first_mes: z.string().default(''),\n mes_example: z.string().nullable().default(''), // Can be null in wild (186 cards)\n // Optional fields\n creator_notes: z.string().optional(),\n system_prompt: z.string().optional(),\n post_history_instructions: z.string().optional(),\n alternate_greetings: z.array(z.string()).optional(),\n character_book: CCv2CharacterBookSchema.optional().nullable(),\n tags: z.array(z.string()).optional(),\n creator: z.string().optional(),\n character_version: z.string().optional(),\n extensions: z.record(z.unknown()).optional(),\n});\n\n/**\n * Wrapped v2 card format schema (modern tools)\n */\nexport const CCv2WrappedSchema = z.object({\n spec: z.literal('chara_card_v2'),\n spec_version: z.literal('2.0'),\n data: CCv2DataSchema,\n});\n\n// ============================================================================\n// TypeScript Types (inferred from Zod schemas)\n// ============================================================================\n\n/**\n * Lorebook entry for v2 cards\n */\nexport type CCv2LorebookEntry = z.infer<typeof CCv2LorebookEntrySchema>;\n\n/**\n * Character book (lorebook) for v2 cards\n */\nexport type CCv2CharacterBook = z.infer<typeof CCv2CharacterBookSchema>;\n\n/**\n * Character Card v2 data structure\n */\nexport type CCv2Data = z.infer<typeof CCv2DataSchema>;\n\n/**\n * Wrapped v2 card format (modern tools)\n */\nexport type CCv2Wrapped = z.infer<typeof CCv2WrappedSchema>;\n\n// ============================================================================\n// Type Guards & Parsers\n// ============================================================================\n\n/**\n * Check if data is a wrapped v2 card\n */\nexport function isWrappedV2(data: unknown): data is CCv2Wrapped {\n return CCv2WrappedSchema.safeParse(data).success;\n}\n\n/**\n * Check if data looks like v2 card data (wrapped or unwrapped)\n */\nexport function isV2CardData(data: unknown): data is CCv2Data | CCv2Wrapped {\n return (\n CCv2WrappedSchema.safeParse(data).success ||\n CCv2DataSchema.safeParse(data).success\n );\n}\n\n/**\n * Parse and validate a wrapped v2 card\n */\nexport function parseWrappedV2(data: unknown): CCv2Wrapped {\n return CCv2WrappedSchema.parse(data);\n}\n\n/**\n * Parse and validate v2 card data\n */\nexport function parseV2Data(data: unknown): CCv2Data {\n return CCv2DataSchema.parse(data);\n}\n\n/**\n * Check if data looks like a wrapped V2 card structurally (without strict validation).\n * This is more lenient than isWrappedV2 - it just checks structure, not full schema validity.\n */\nexport function looksLikeWrappedV2(data: unknown): data is { spec: string; data: Record<string, unknown> } {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n return (\n obj.spec === 'chara_card_v2' &&\n obj.data !== null &&\n typeof obj.data === 'object'\n );\n}\n\n/**\n * Get v2 card data from wrapped or unwrapped format.\n *\n * Uses structural check instead of strict Zod validation to handle\n * malformed cards that have the right structure but missing/invalid fields.\n * The caller (e.g., ccv2ToCCv3) handles defaulting missing fields.\n */\nexport function getV2Data(card: CCv2Data | CCv2Wrapped): CCv2Data {\n // Use structural check - more lenient than isWrappedV2 schema validation\n if (looksLikeWrappedV2(card)) {\n return card.data as CCv2Data;\n }\n return card;\n}\n","/**\n * Character Card v3 Types\n *\n * Based on: https://github.com/kwaroran/character-card-spec-v3\n */\n\nimport { z } from 'zod';\nimport { AssetDescriptorSchema } from './common.js';\n\n// ============================================================================\n// Zod Schemas\n// ============================================================================\n\n/**\n * Lorebook entry schema for v3 cards\n */\nexport const CCv3LorebookEntrySchema = z.object({\n keys: z.array(z.string()).optional(), // Some tools use 'key' instead\n content: z.string(),\n enabled: z.boolean().default(true), // Default to enabled if missing\n insertion_order: z.preprocess((v) => v ?? 0, z.number().int()),\n // Optional fields - be lenient with nulls since wild data has them\n case_sensitive: z.boolean().nullable().optional(),\n name: z.string().optional(),\n priority: z.number().int().nullable().optional(),\n id: z.number().int().nullable().optional(),\n comment: z.string().nullable().optional(),\n selective: z.boolean().nullable().optional(),\n secondary_keys: z.array(z.string()).nullable().optional(),\n constant: z.boolean().nullable().optional(),\n position: z.union([z.enum(['before_char', 'after_char', 'in_chat']), z.number().int(), z.literal('')]).nullable().optional(),\n extensions: z.record(z.unknown()).optional(),\n // v3 specific - also lenient with types since SillyTavern uses numbers for enums\n automation_id: z.string().optional(),\n role: z.union([z.enum(['system', 'user', 'assistant']), z.number().int()]).nullable().optional(),\n group: z.string().optional(),\n scan_frequency: z.number().int().nonnegative().optional(),\n probability: z.number().min(0).max(100).optional(), // Some tools use 0-100 instead of 0-1\n use_regex: z.boolean().optional(),\n depth: z.number().int().nonnegative().optional(),\n selective_logic: z.union([z.enum(['AND', 'NOT']), z.number().int()]).optional(),\n}).passthrough(); // Allow tool-specific extensions\n\n/**\n * Character book (lorebook) schema for v3 cards\n */\nexport const CCv3CharacterBookSchema = z.object({\n name: z.string().optional(),\n description: z.string().optional(),\n scan_depth: z.number().int().nonnegative().optional(),\n token_budget: z.number().int().nonnegative().optional(),\n recursive_scanning: z.boolean().optional(),\n extensions: z.record(z.unknown()).optional(),\n entries: z.array(CCv3LorebookEntrySchema),\n});\n\n/**\n * Character Card v3 inner data structure schema.\n *\n * Note: Fields like group_only_greetings, creator, character_version, and tags\n * are technically \"required\" per V3 spec but rarely present in wild cards.\n * We use .default() to make parsing lenient while still producing valid output.\n */\nexport const CCv3DataInnerSchema = z.object({\n // Core fields - use .default('') to handle missing fields in malformed cards\n name: z.string().default(''),\n description: z.string().default(''),\n personality: z.string().nullable().default(''), // Can be null in wild (141 cards)\n scenario: z.string().default(''),\n first_mes: z.string().default(''),\n mes_example: z.string().nullable().default(''), // Can be null in wild (186 cards)\n // \"Required\" per spec but often missing in wild - use defaults for leniency\n creator: z.string().default(''),\n character_version: z.string().default(''),\n tags: z.array(z.string()).default([]),\n group_only_greetings: z.array(z.string()).default([]),\n // Optional fields\n creator_notes: z.string().optional(),\n system_prompt: z.string().optional(),\n post_history_instructions: z.string().optional(),\n alternate_greetings: z.array(z.string()).optional(),\n character_book: CCv3CharacterBookSchema.optional().nullable(),\n extensions: z.record(z.unknown()).optional(),\n // v3 specific\n assets: z.array(AssetDescriptorSchema).optional(),\n nickname: z.string().optional(),\n creator_notes_multilingual: z.record(z.string()).optional(),\n source: z.array(z.string()).optional(),\n creation_date: z.number().int().nonnegative().optional(), // Unix timestamp in seconds\n modification_date: z.number().int().nonnegative().optional(), // Unix timestamp in seconds\n});\n\n/**\n * Character Card v3 full structure schema\n */\nexport const CCv3DataSchema = z.object({\n spec: z.literal('chara_card_v3'),\n spec_version: z.literal('3.0'),\n data: CCv3DataInnerSchema,\n});\n\n// ============================================================================\n// TypeScript Types (inferred from Zod schemas)\n// ============================================================================\n\n/**\n * Lorebook entry for v3 cards\n */\nexport type CCv3LorebookEntry = z.infer<typeof CCv3LorebookEntrySchema>;\n\n/**\n * Character book (lorebook) for v3 cards\n */\nexport type CCv3CharacterBook = z.infer<typeof CCv3CharacterBookSchema>;\n\n/**\n * Character Card v3 inner data structure\n */\nexport type CCv3DataInner = z.infer<typeof CCv3DataInnerSchema>;\n\n/**\n * Character Card v3 full structure\n */\nexport type CCv3Data = z.infer<typeof CCv3DataSchema>;\n\n// ============================================================================\n// Type Guards & Parsers\n// ============================================================================\n\n/**\n * Check if data is a v3 card\n */\nexport function isV3Card(data: unknown): data is CCv3Data {\n return CCv3DataSchema.safeParse(data).success;\n}\n\n/**\n * Parse and validate a v3 card\n */\nexport function parseV3Card(data: unknown): CCv3Data {\n return CCv3DataSchema.parse(data);\n}\n\n/**\n * Parse and validate v3 card inner data\n */\nexport function parseV3DataInner(data: unknown): CCv3DataInner {\n return CCv3DataInnerSchema.parse(data);\n}\n\n/**\n * Get v3 card inner data\n */\nexport function getV3Data(card: CCv3Data): CCv3DataInner {\n return card.data;\n}\n\n/**\n * Check if data looks like a V3 card structurally (without strict validation).\n * More lenient than isV3Card - just checks structure, not full schema validity.\n */\nexport function looksLikeV3Card(data: unknown): data is { spec: string; data: Record<string, unknown> } {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n return (\n obj.spec === 'chara_card_v3' &&\n obj.data !== null &&\n typeof obj.data === 'object'\n );\n}\n","/**\n * RisuAI Extension Types\n *\n * These extensions are preserved as opaque blobs.\n * We do NOT interpret or transform the script contents.\n */\n\n/**\n * Risu emotions mapping (v2 style)\n * Format: [name, uri][]\n */\nexport type RisuEmotions = [string, string][];\n\n/**\n * Risu additional assets (v3 style)\n * Format: [name, uri, type][]\n */\nexport type RisuAdditionalAssets = [string, string, string][];\n\n/**\n * Risu depth prompt configuration\n */\nexport interface RisuDepthPrompt {\n depth: number;\n prompt: string;\n}\n\n/**\n * Risu extensions in card.extensions.risuai\n * Preserved as opaque - we don't interpret script contents\n */\nexport interface RisuExtensions {\n // Emotion assets\n emotions?: RisuEmotions;\n additionalAssets?: RisuAdditionalAssets;\n\n // Script data - OPAQUE, do not parse\n triggerscript?: unknown;\n customScripts?: unknown;\n\n // Voice/TTS settings\n vits?: Record<string, string>;\n\n // Depth prompt\n depth_prompt?: RisuDepthPrompt;\n\n // Other Risu-specific fields\n [key: string]: unknown;\n}\n\n/**\n * CharX x_meta entry (PNG chunk metadata preservation)\n */\nexport interface CharxMetaEntry {\n type?: string; // e.g., 'WEBP', 'PNG', 'JPEG'\n [key: string]: unknown;\n}\n\n/**\n * Check if card has Risu extensions\n */\nexport function hasRisuExtensions(extensions?: Record<string, unknown>): boolean {\n if (!extensions) return false;\n return 'risuai' in extensions || 'risu' in extensions;\n}\n\n/**\n * Check if card has Risu scripts (triggerscript or customScripts)\n */\nexport function hasRisuScripts(extensions?: Record<string, unknown>): boolean {\n if (!extensions) return false;\n const risu = extensions.risuai as RisuExtensions | undefined;\n if (!risu) return false;\n return !!risu.triggerscript || !!risu.customScripts;\n}\n\n/**\n * Check if card has depth prompt\n * Checks both SillyTavern style (extensions.depth_prompt) and Risu style (extensions.risuai.depth_prompt)\n */\nexport function hasDepthPrompt(extensions?: Record<string, unknown>): boolean {\n if (!extensions) return false;\n // SillyTavern top-level depth_prompt\n if ('depth_prompt' in extensions && extensions.depth_prompt) return true;\n // Risu-style depth_prompt\n const risu = extensions.risuai as RisuExtensions | undefined;\n return !!risu?.depth_prompt;\n}\n","/**\n * Normalized Card Types\n *\n * Unified view of card data regardless of source format.\n * This is a computed/virtual representation, not stored.\n */\n\nimport type { CCv3CharacterBook } from './ccv3.js';\n\n/**\n * Normalized card representation\n * Provides unified access to card data from any format\n */\nexport interface NormalizedCard {\n // Core fields (always present)\n name: string;\n description: string;\n personality: string;\n scenario: string;\n firstMes: string;\n mesExample: string;\n\n // Optional prompts\n systemPrompt?: string;\n postHistoryInstructions?: string;\n\n // Arrays\n alternateGreetings: string[];\n groupOnlyGreetings: string[];\n tags: string[];\n\n // Metadata\n creator?: string;\n creatorNotes?: string;\n characterVersion?: string;\n\n // Character book (v3 format)\n characterBook?: CCv3CharacterBook;\n\n // Extensions (preserved as-is)\n extensions: Record<string, unknown>;\n}\n\n/**\n * Create empty normalized card with defaults\n */\nexport function createEmptyNormalizedCard(): NormalizedCard {\n return {\n name: '',\n description: '',\n personality: '',\n scenario: '',\n firstMes: '',\n mesExample: '',\n alternateGreetings: [],\n groupOnlyGreetings: [],\n tags: [],\n extensions: {},\n };\n}\n\n/**\n * Derived features extracted from card (not stored in card)\n */\nexport interface DerivedFeatures {\n // Content flags\n hasAlternateGreetings: boolean;\n alternateGreetingsCount: number;\n /** Total greetings = first_mes (1) + alternate_greetings */\n totalGreetingsCount: number;\n hasLorebook: boolean;\n lorebookEntriesCount: number;\n hasEmbeddedImages: boolean;\n embeddedImagesCount: number;\n hasGallery: boolean;\n\n // Format-specific\n hasRisuExtensions: boolean;\n hasRisuScripts: boolean;\n hasDepthPrompt: boolean;\n hasVoxtaAppearance: boolean;\n\n // Token counts (estimated)\n tokens: {\n description: number;\n personality: number;\n scenario: number;\n firstMes: number;\n mesExample: number;\n systemPrompt: number;\n total: number;\n };\n}\n\n/**\n * Create empty derived features\n */\nexport function createEmptyFeatures(): DerivedFeatures {\n return {\n hasAlternateGreetings: false,\n alternateGreetingsCount: 0,\n totalGreetingsCount: 1, // first_mes always counts as 1\n hasLorebook: false,\n lorebookEntriesCount: 0,\n hasEmbeddedImages: false,\n embeddedImagesCount: 0,\n hasGallery: false,\n hasRisuExtensions: false,\n hasRisuScripts: false,\n hasDepthPrompt: false,\n hasVoxtaAppearance: false,\n tokens: {\n description: 0,\n personality: 0,\n scenario: 0,\n firstMes: 0,\n mesExample: 0,\n systemPrompt: 0,\n total: 0,\n },\n };\n}\n","/**\n * Feature Derivation\n *\n * Canonical feature extraction from character cards.\n * Eliminates duplicate implementations across Archive, Federation, and Architect.\n */\n\nimport type { CCv2Data } from './ccv2.js';\nimport type { CCv3DataInner } from './ccv3.js';\nimport type { DerivedFeatures } from './normalized.js';\nimport { hasRisuExtensions, hasRisuScripts, hasDepthPrompt } from './risu.js';\n\n/**\n * Derive features from a character card (V2 or V3 format).\n *\n * This is the canonical implementation - all apps should use this\n * rather than implementing their own feature detection.\n *\n * @param card - Either CCv2Data or CCv3DataInner (unwrapped)\n * @returns DerivedFeatures with all feature flags populated\n *\n * @example\n * ```typescript\n * import { deriveFeatures, parseV3Card } from '@character-foundry/schemas';\n *\n * const card = parseV3Card(data);\n * const features = deriveFeatures(card.data);\n *\n * if (features.hasLorebook) {\n * console.log(`Found ${features.lorebookEntriesCount} lorebook entries`);\n * }\n * ```\n */\nexport function deriveFeatures(card: CCv2Data | CCv3DataInner): DerivedFeatures {\n // Detect format by checking for V3-specific field\n const isV3 = 'assets' in card;\n\n // Alternate greetings\n const altGreetings = card.alternate_greetings ?? [];\n const hasAlternateGreetings = altGreetings.length > 0;\n const alternateGreetingsCount = altGreetings.length;\n // Total = first_mes (1) + alternate_greetings\n const totalGreetingsCount = 1 + alternateGreetingsCount;\n\n // Lorebook\n const characterBook = card.character_book;\n const hasLorebook = !!characterBook && characterBook.entries.length > 0;\n const lorebookEntriesCount = characterBook?.entries.length ?? 0;\n\n // Assets (V3 only) - check for visual asset types\n const assets = isV3 ? (card as CCv3DataInner).assets ?? [] : [];\n const imageAssetTypes = ['icon', 'background', 'emotion', 'custom'];\n const imageAssets = assets.filter(\n (a) =>\n imageAssetTypes.includes(a.type) ||\n ['png', 'jpg', 'jpeg', 'webp', 'gif'].includes(a.ext.toLowerCase()),\n );\n const hasGallery = imageAssets.length > 0;\n\n // Embedded images - check for data URLs in text fields\n const embeddedImageCount = countEmbeddedImages(card);\n const hasEmbeddedImages = embeddedImageCount > 0;\n\n // Extensions\n const extensions = card.extensions ?? {};\n const hasRisu = hasRisuExtensions(extensions);\n const hasScripts = hasRisuScripts(extensions);\n const hasDepth = hasDepthPrompt(extensions);\n const hasVoxta = checkVoxtaAppearance(extensions);\n\n // Token counts - initialize to zero (actual counting happens in tokenizers package)\n const tokens = {\n description: 0,\n personality: 0,\n scenario: 0,\n firstMes: 0,\n mesExample: 0,\n systemPrompt: 0,\n total: 0,\n };\n\n return {\n hasAlternateGreetings,\n alternateGreetingsCount,\n totalGreetingsCount,\n hasLorebook,\n lorebookEntriesCount,\n hasEmbeddedImages,\n embeddedImagesCount: embeddedImageCount,\n hasGallery,\n hasRisuExtensions: hasRisu,\n hasRisuScripts: hasScripts,\n hasDepthPrompt: hasDepth,\n hasVoxtaAppearance: hasVoxta,\n tokens,\n };\n}\n\n/**\n * Count embedded images (data URLs) in card text fields.\n * Looks for base64-encoded images in description, personality, scenario, etc.\n */\nfunction countEmbeddedImages(card: CCv2Data | CCv3DataInner): number {\n const textFields = [\n card.description,\n card.personality,\n card.scenario,\n card.first_mes,\n card.mes_example,\n card.creator_notes,\n card.system_prompt,\n card.post_history_instructions,\n ...(card.alternate_greetings ?? []),\n ].filter((field): field is string => typeof field === 'string');\n\n // Add group_only_greetings if V3\n if ('group_only_greetings' in card) {\n textFields.push(...(card.group_only_greetings ?? []));\n }\n\n let count = 0;\n const dataUrlPattern = /data:image\\/[^;]+;base64,/g;\n\n for (const text of textFields) {\n const matches = text.match(dataUrlPattern);\n if (matches) {\n count += matches.length;\n }\n }\n\n return count;\n}\n\n/**\n * Check if card has Voxta appearance data.\n * Voxta stores appearance in extensions.voxta.appearance\n */\nfunction checkVoxtaAppearance(extensions: Record<string, unknown>): boolean {\n if (!extensions.voxta) return false;\n const voxta = extensions.voxta as Record<string, unknown>;\n return !!voxta.appearance;\n}\n","/**\n * Format Detection\n *\n * Detect card specification version from JSON data.\n */\n\nimport type { Spec } from './common.js';\n\n/**\n * V3-only fields that indicate a V3 card\n */\nconst V3_ONLY_FIELDS = ['group_only_greetings', 'creation_date', 'modification_date', 'assets'] as const;\n\n/**\n * Result from detailed spec detection\n */\nexport interface SpecDetectionResult {\n /** Detected spec version */\n spec: Spec | null;\n /** Confidence level of detection */\n confidence: 'high' | 'medium' | 'low';\n /** What fields/values indicated this spec */\n indicators: string[];\n /** Anomalies or inconsistencies detected */\n warnings: string[];\n}\n\n/**\n * Detect card spec version from parsed JSON\n * Returns 'v2', 'v3', or null if not recognized\n */\nexport function detectSpec(data: unknown): Spec | null {\n return detectSpecDetailed(data).spec;\n}\n\n/**\n * Detailed spec detection with confidence and reasoning.\n * Useful for debugging and logging.\n */\nexport function detectSpecDetailed(data: unknown): SpecDetectionResult {\n const result: SpecDetectionResult = {\n spec: null,\n confidence: 'low',\n indicators: [],\n warnings: [],\n };\n\n if (!data || typeof data !== 'object') {\n result.indicators.push('Input is not an object');\n return result;\n }\n\n const obj = data as Record<string, unknown>;\n const dataObj = (obj.data && typeof obj.data === 'object' ? obj.data : null) as Record<\n string,\n unknown\n > | null;\n\n // Check for explicit spec markers (HIGH confidence)\n\n // Explicit v3 spec marker\n if (obj.spec === 'chara_card_v3') {\n result.spec = 'v3';\n result.confidence = 'high';\n result.indicators.push('spec field is \"chara_card_v3\"');\n\n // Check for inconsistencies\n if (obj.spec_version && obj.spec_version !== '3.0') {\n result.warnings.push(`spec_version \"${obj.spec_version}\" inconsistent with v3 spec`);\n }\n\n return result;\n }\n\n // Explicit v2 spec marker\n if (obj.spec === 'chara_card_v2') {\n result.spec = 'v2';\n result.confidence = 'high';\n result.indicators.push('spec field is \"chara_card_v2\"');\n\n // Check for inconsistencies - V3-only fields in V2 card\n if (dataObj) {\n for (const field of V3_ONLY_FIELDS) {\n if (field in dataObj) {\n result.warnings.push(`V3-only field \"${field}\" found in V2 card`);\n }\n }\n }\n\n if (obj.spec_version && obj.spec_version !== '2.0') {\n result.warnings.push(`spec_version \"${obj.spec_version}\" inconsistent with v2 spec`);\n }\n\n return result;\n }\n\n // Check spec_version field (HIGH confidence)\n if (typeof obj.spec_version === 'string') {\n if (obj.spec_version.startsWith('3')) {\n result.spec = 'v3';\n result.confidence = 'high';\n result.indicators.push(`spec_version \"${obj.spec_version}\" starts with \"3\"`);\n return result;\n }\n if (obj.spec_version.startsWith('2')) {\n result.spec = 'v2';\n result.confidence = 'high';\n result.indicators.push(`spec_version \"${obj.spec_version}\" starts with \"2\"`);\n return result;\n }\n }\n\n if (obj.spec_version === 2.0 || obj.spec_version === 2) {\n result.spec = 'v2';\n result.confidence = 'high';\n result.indicators.push(`spec_version is numeric ${obj.spec_version}`);\n return result;\n }\n\n // Check for V3-only fields (MEDIUM confidence)\n if (dataObj) {\n const v3Fields: string[] = [];\n for (const field of V3_ONLY_FIELDS) {\n if (field in dataObj) {\n v3Fields.push(field);\n }\n }\n\n if (v3Fields.length > 0) {\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push(`Has V3-only fields: ${v3Fields.join(', ')}`);\n return result;\n }\n }\n\n // Check root level for V3-only fields (also MEDIUM confidence)\n const rootV3Fields: string[] = [];\n for (const field of V3_ONLY_FIELDS) {\n if (field in obj) {\n rootV3Fields.push(field);\n }\n }\n if (rootV3Fields.length > 0) {\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push(`Has V3-only fields at root: ${rootV3Fields.join(', ')}`);\n result.warnings.push('V3 fields found at root level instead of data object');\n return result;\n }\n\n // Wrapped format with data object (MEDIUM confidence)\n if (obj.spec && dataObj) {\n const dataName = dataObj.name;\n if (dataName && typeof dataName === 'string') {\n // Infer from spec string\n if (typeof obj.spec === 'string') {\n if (obj.spec.includes('v3') || obj.spec.includes('3')) {\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push(`spec field \"${obj.spec}\" contains \"v3\" or \"3\"`);\n return result;\n }\n if (obj.spec.includes('v2') || obj.spec.includes('2')) {\n result.spec = 'v2';\n result.confidence = 'medium';\n result.indicators.push(`spec field \"${obj.spec}\" contains \"v2\" or \"2\"`);\n return result;\n }\n }\n // Default wrapped format to v3 (modern)\n result.spec = 'v3';\n result.confidence = 'medium';\n result.indicators.push('Has wrapped format with spec and data.name');\n return result;\n }\n }\n\n // Unwrapped format - V1/V2 like structure (MEDIUM confidence)\n if (obj.name && typeof obj.name === 'string') {\n if ('description' in obj || 'personality' in obj || 'scenario' in obj) {\n result.spec = 'v2';\n result.confidence = 'medium';\n result.indicators.push('Unwrapped format with name, description/personality/scenario');\n return result;\n }\n }\n\n // Check if data object has card-like structure without spec (LOW confidence)\n if (dataObj && typeof dataObj.name === 'string') {\n if ('description' in dataObj || 'personality' in dataObj) {\n result.spec = 'v2';\n result.confidence = 'low';\n result.indicators.push('Has data object with name and card fields, but no spec');\n result.warnings.push('Missing spec field');\n return result;\n }\n }\n\n result.indicators.push('No card structure detected');\n return result;\n}\n\n/**\n * Check if card has a lorebook\n */\nexport function hasLorebook(data: unknown): boolean {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n\n // Check wrapped format\n const wrapped = obj.data as Record<string, unknown> | undefined;\n if (wrapped?.character_book) {\n const book = wrapped.character_book as Record<string, unknown>;\n if (Array.isArray(book.entries) && book.entries.length > 0) return true;\n }\n\n // Check unwrapped format\n if (obj.character_book) {\n const book = obj.character_book as Record<string, unknown>;\n if (Array.isArray(book.entries) && book.entries.length > 0) return true;\n }\n\n return false;\n}\n\n/**\n * Check if data looks like a valid card structure\n */\nexport function looksLikeCard(data: unknown): boolean {\n if (!data || typeof data !== 'object') return false;\n const obj = data as Record<string, unknown>;\n\n // Has explicit spec marker\n if (obj.spec === 'chara_card_v2' || obj.spec === 'chara_card_v3') {\n return true;\n }\n\n // Has wrapped data with name\n if (obj.data && typeof obj.data === 'object') {\n const dataObj = obj.data as Record<string, unknown>;\n if (typeof dataObj.name === 'string' && dataObj.name.length > 0) {\n return true;\n }\n }\n\n // Has unwrapped card-like structure\n if (typeof obj.name === 'string' && obj.name.length > 0) {\n if ('description' in obj || 'personality' in obj || 'first_mes' in obj) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Card Normalizer\n *\n * Handles normalization of malformed card data from various sources.\n * Fixes common issues like wrong spec values, misplaced fields, missing required fields.\n */\n\nimport type { CCv2Data, CCv2Wrapped, CCv2CharacterBook, CCv2LorebookEntry } from './ccv2.js';\nimport type { CCv3Data, CCv3CharacterBook, CCv3LorebookEntry } from './ccv3.js';\nimport { detectSpec } from './detection.js';\n\n/**\n * Position values as numbers (non-standard) and their string equivalents\n */\nconst POSITION_MAP: Record<number, 'before_char' | 'after_char'> = {\n 0: 'before_char',\n 1: 'after_char',\n};\n\n/**\n * V3-only lorebook entry fields that should be moved to extensions for V2\n */\nconst V3_ONLY_ENTRY_FIELDS = [\n 'probability',\n 'depth',\n 'group',\n 'scan_frequency',\n 'use_regex',\n 'selective_logic',\n 'role',\n 'automation_id',\n] as const;\n\n/**\n * Required V2 card fields with their defaults\n */\nconst V2_REQUIRED_DEFAULTS: Partial<CCv2Data> = {\n name: '',\n description: '',\n personality: '',\n scenario: '',\n first_mes: '',\n mes_example: '',\n};\n\n/**\n * Required V3 card fields with their defaults\n */\nconst V3_REQUIRED_DEFAULTS: Partial<CCv3Data['data']> = {\n name: '',\n description: '',\n personality: '',\n scenario: '',\n first_mes: '',\n mes_example: '',\n creator: '',\n character_version: '1.0',\n tags: [],\n group_only_greetings: [],\n};\n\n/**\n * Fields that belong at root level for wrapped format\n */\nconst _ROOT_FIELDS = ['spec', 'spec_version', 'data'] as const;\n\n/**\n * Fields that belong in the data object\n */\nconst DATA_FIELDS = [\n 'name',\n 'description',\n 'personality',\n 'scenario',\n 'first_mes',\n 'mes_example',\n 'creator_notes',\n 'system_prompt',\n 'post_history_instructions',\n 'alternate_greetings',\n 'character_book',\n 'tags',\n 'creator',\n 'character_version',\n 'extensions',\n 'assets',\n 'nickname',\n 'creator_notes_multilingual',\n 'source',\n 'creation_date',\n 'modification_date',\n 'group_only_greetings',\n] as const;\n\n/**\n * Deep clone an object without mutating the original\n */\nfunction deepClone<T>(obj: T): T {\n if (obj === null || obj === undefined) {\n return obj;\n }\n if (Array.isArray(obj)) {\n return obj.map((item) => deepClone(item)) as T;\n }\n if (typeof obj === 'object') {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n result[key] = deepClone(value);\n }\n return result as T;\n }\n return obj;\n}\n\n/**\n * Check if a timestamp is in milliseconds (13+ digits)\n */\nfunction isMilliseconds(timestamp: number): boolean {\n // Timestamps before year 2001 in seconds: < 1000000000\n // Timestamps in milliseconds are typically 13 digits: 1000000000000+\n return timestamp > 10000000000;\n}\n\n/**\n * CardNormalizer - handles normalization of malformed card data\n */\nexport const CardNormalizer = {\n /**\n * Normalize card data to valid schema format.\n *\n * Handles:\n * - Fixing spec/spec_version values\n * - Moving misplaced fields to correct locations\n * - Adding missing required fields with defaults\n * - Handling hybrid formats (fields at root AND in data object)\n *\n * @param data - Raw card data (potentially malformed)\n * @param spec - Target spec version\n * @returns Normalized card data (does not mutate input)\n */\n normalize(data: unknown, spec: 'v2' | 'v3'): CCv2Wrapped | CCv3Data {\n if (!data || typeof data !== 'object') {\n // Return minimal valid card\n if (spec === 'v3') {\n return {\n spec: 'chara_card_v3',\n spec_version: '3.0',\n data: { ...V3_REQUIRED_DEFAULTS } as CCv3Data['data'],\n };\n }\n return {\n spec: 'chara_card_v2',\n spec_version: '2.0',\n data: { ...V2_REQUIRED_DEFAULTS } as CCv2Data,\n };\n }\n\n const obj = data as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n // Build merged data object from root fields + existing data object\n const existingData = (obj.data && typeof obj.data === 'object' ? obj.data : {}) as Record<\n string,\n unknown\n >;\n const mergedData: Record<string, unknown> = {};\n\n // Copy existing data first\n for (const [key, value] of Object.entries(existingData)) {\n mergedData[key] = deepClone(value);\n }\n\n // Move any misplaced root-level data fields into data object\n // (ChubAI hybrid format fix)\n for (const field of DATA_FIELDS) {\n if (field in obj && !(field in mergedData)) {\n mergedData[field] = deepClone(obj[field]);\n }\n }\n\n // Handle character_book: null -> remove entirely\n if (mergedData.character_book === null) {\n delete mergedData.character_book;\n }\n\n // Normalize character_book if present\n if (mergedData.character_book && typeof mergedData.character_book === 'object') {\n mergedData.character_book = CardNormalizer.normalizeCharacterBook(\n mergedData.character_book as Record<string, unknown>,\n spec\n );\n }\n\n // Apply defaults for required fields\n const defaults = spec === 'v3' ? V3_REQUIRED_DEFAULTS : V2_REQUIRED_DEFAULTS;\n for (const [key, defaultValue] of Object.entries(defaults)) {\n if (!(key in mergedData) || mergedData[key] === undefined) {\n mergedData[key] = Array.isArray(defaultValue) ? [...defaultValue] : defaultValue;\n }\n }\n\n // Ensure arrays are actually arrays\n if (mergedData.tags && !Array.isArray(mergedData.tags)) {\n mergedData.tags = [];\n }\n if (mergedData.alternate_greetings && !Array.isArray(mergedData.alternate_greetings)) {\n mergedData.alternate_greetings = [];\n }\n if (spec === 'v3') {\n if (\n mergedData.group_only_greetings &&\n !Array.isArray(mergedData.group_only_greetings)\n ) {\n mergedData.group_only_greetings = [];\n }\n }\n\n // Build result with correct spec\n if (spec === 'v3') {\n result.spec = 'chara_card_v3';\n result.spec_version = '3.0';\n result.data = CardNormalizer.fixTimestampsInner(mergedData);\n } else {\n result.spec = 'chara_card_v2';\n result.spec_version = '2.0';\n result.data = mergedData;\n }\n\n return result as unknown as CCv2Wrapped | CCv3Data;\n },\n\n /**\n * Normalize a character book (lorebook).\n *\n * Handles:\n * - Ensuring required fields exist\n * - Converting numeric position values to string enums\n * - Moving V3-only fields to extensions for V2 compatibility\n *\n * @param book - Raw character book data\n * @param spec - Target spec version\n * @returns Normalized character book\n */\n normalizeCharacterBook(\n book: Record<string, unknown>,\n spec: 'v2' | 'v3'\n ): CCv2CharacterBook | CCv3CharacterBook {\n const result: Record<string, unknown> = {};\n\n // Copy book-level fields\n if (book.name !== undefined) result.name = book.name;\n if (book.description !== undefined) result.description = book.description;\n if (book.scan_depth !== undefined) result.scan_depth = book.scan_depth;\n if (book.token_budget !== undefined) result.token_budget = book.token_budget;\n if (book.recursive_scanning !== undefined)\n result.recursive_scanning = book.recursive_scanning;\n if (book.extensions !== undefined) result.extensions = deepClone(book.extensions);\n\n // Normalize entries\n const entries = Array.isArray(book.entries) ? book.entries : [];\n result.entries = entries.map((entry) =>\n CardNormalizer.normalizeEntry(entry as Record<string, unknown>, spec)\n );\n\n return result as unknown as CCv2CharacterBook | CCv3CharacterBook;\n },\n\n /**\n * Normalize a single lorebook entry.\n *\n * Handles:\n * - Converting numeric position to string enum\n * - Moving V3-only fields to extensions for V2\n * - Ensuring required fields exist\n *\n * @param entry - Raw entry data\n * @param spec - Target spec version\n * @returns Normalized entry\n */\n normalizeEntry(\n entry: Record<string, unknown>,\n spec: 'v2' | 'v3'\n ): CCv2LorebookEntry | CCv3LorebookEntry {\n const result: Record<string, unknown> = {};\n\n // Required fields with defaults\n result.keys = Array.isArray(entry.keys) ? [...entry.keys] : [];\n result.content = typeof entry.content === 'string' ? entry.content : '';\n result.enabled = entry.enabled !== false; // default true\n result.insertion_order =\n typeof entry.insertion_order === 'number' ? entry.insertion_order : 0;\n\n // For V2, extensions is required\n if (spec === 'v2') {\n result.extensions =\n entry.extensions && typeof entry.extensions === 'object'\n ? deepClone(entry.extensions)\n : {};\n }\n\n // Optional fields\n if (entry.case_sensitive !== undefined) result.case_sensitive = entry.case_sensitive;\n if (entry.name !== undefined) result.name = entry.name;\n if (entry.priority !== undefined) result.priority = entry.priority;\n if (entry.id !== undefined) result.id = entry.id;\n if (entry.comment !== undefined) result.comment = entry.comment;\n if (entry.selective !== undefined) result.selective = entry.selective;\n if (entry.secondary_keys !== undefined) {\n result.secondary_keys = Array.isArray(entry.secondary_keys)\n ? [...entry.secondary_keys]\n : [];\n }\n if (entry.constant !== undefined) result.constant = entry.constant;\n\n // Position: convert numeric to string enum\n if (entry.position !== undefined) {\n if (typeof entry.position === 'number') {\n result.position = POSITION_MAP[entry.position] || 'before_char';\n } else if (entry.position === 'before_char' || entry.position === 'after_char') {\n result.position = entry.position;\n }\n }\n\n // Handle V3-only fields\n if (spec === 'v3') {\n // Copy V3 fields directly\n if (entry.extensions !== undefined) result.extensions = deepClone(entry.extensions);\n for (const field of V3_ONLY_ENTRY_FIELDS) {\n if (entry[field] !== undefined) {\n result[field] = entry[field];\n }\n }\n } else {\n // V2: Move V3-only fields to extensions\n const ext = (result.extensions || {}) as Record<string, unknown>;\n for (const field of V3_ONLY_ENTRY_FIELDS) {\n if (entry[field] !== undefined) {\n ext[field] = entry[field];\n }\n }\n result.extensions = ext;\n }\n\n return result as unknown as CCv2LorebookEntry | CCv3LorebookEntry;\n },\n\n /**\n * Fix CharacterTavern timestamp format (milliseconds -> seconds).\n *\n * CCv3 spec requires timestamps in seconds (Unix epoch).\n * CharacterTavern exports timestamps in milliseconds.\n *\n * @param data - V3 card data\n * @returns Card data with fixed timestamps (does not mutate input)\n */\n fixTimestamps(data: CCv3Data): CCv3Data {\n const result = deepClone(data);\n result.data = CardNormalizer.fixTimestampsInner(\n result.data as unknown as Record<string, unknown>\n ) as unknown as CCv3Data['data'];\n return result;\n },\n\n /**\n * Internal: fix timestamps in data object\n */\n fixTimestampsInner(data: Record<string, unknown>): Record<string, unknown> {\n const result = { ...data };\n\n if (typeof result.creation_date === 'number') {\n if (isMilliseconds(result.creation_date)) {\n result.creation_date = Math.floor(result.creation_date / 1000);\n }\n // Sanitize negative timestamps (.NET default dates like 0001-01-01)\n if ((result.creation_date as number) < 0) {\n delete result.creation_date;\n }\n }\n\n if (typeof result.modification_date === 'number') {\n if (isMilliseconds(result.modification_date)) {\n result.modification_date = Math.floor(result.modification_date / 1000);\n }\n // Sanitize negative timestamps (.NET default dates like 0001-01-01)\n if ((result.modification_date as number) < 0) {\n delete result.modification_date;\n }\n }\n\n return result;\n },\n\n /**\n * Auto-detect spec and normalize.\n *\n * @param data - Raw card data\n * @returns Normalized card data, or null if not a valid card\n */\n autoNormalize(data: unknown): CCv2Wrapped | CCv3Data | null {\n const spec = detectSpec(data);\n if (!spec) return null;\n\n // V1 cards get upgraded to V2\n const targetSpec = spec === 'v3' ? 'v3' : 'v2';\n return CardNormalizer.normalize(data, targetSpec);\n },\n};\n\nexport type { CCv2Wrapped, CCv3Data };\n","/**\n * Validation Utilities\n *\n * Helper functions for Zod validation with Foundry error integration.\n */\n\nimport { z } from 'zod';\n\n/**\n * Convert Zod error to human-readable message\n */\nexport function zodErrorToMessage(zodError: z.ZodError, context?: string): string {\n const messages = zodError.errors.map((err) => {\n const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';\n return `${path}${err.message}`;\n });\n\n const message = messages.join('; ');\n return context ? `${context} - ${message}` : message;\n}\n\n/**\n * Get the first error field from Zod error\n */\nexport function getFirstErrorField(zodError: z.ZodError): string | undefined {\n return zodError.errors[0]?.path[0]?.toString();\n}\n\n/**\n * Safe parse with detailed error information\n */\nexport function safeParse<T>(\n schema: z.ZodSchema<T>,\n data: unknown\n): { success: true; data: T } | { success: false; error: string; field?: string } {\n const result = schema.safeParse(data);\n\n if (result.success) {\n return { success: true, data: result.data };\n }\n\n return {\n success: false,\n error: zodErrorToMessage(result.error),\n field: getFirstErrorField(result.error),\n };\n}\n","/**\n * CharX Reader\n *\n * Extracts and parses .charx (ZIP-based character card) files.\n * Supports standard CharX, Risu CharX, and JPEG+ZIP hybrid formats.\n */\n\nimport {\n type BinaryData,\n toString,\n base64Decode,\n parseURI,\n ParseError,\n SizeLimitError,\n} from '@character-foundry/core';\nimport {\n type Unzipped,\n isJpegCharX,\n getZipOffset,\n streamingUnzipSync,\n ZipPreflightError,\n} from '@character-foundry/core/zip';\nimport type { CCv3Data, AssetDescriptor } from '@character-foundry/schemas';\nimport { hasRisuExtensions } from '@character-foundry/schemas';\nimport type {\n CharxData,\n CharxAssetInfo,\n CharxMetaEntry,\n CharxReadOptions,\n AssetFetcher,\n} from './types.js';\n\nconst DEFAULT_OPTIONS: Required<Omit<CharxReadOptions, 'assetFetcher'>> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxAssetSize: 50 * 1024 * 1024, // 50MB (Risu standard)\n maxTotalSize: 200 * 1024 * 1024, // 200MB\n preserveXMeta: true,\n preserveModuleRisum: true,\n};\n\n/**\n * Check if data is a CharX file (ZIP with card.json)\n *\n * Scans the END of the file (ZIP central directory) since JPEG+ZIP hybrids\n * can have large JPEG data at the front. The central directory listing\n * all filenames is always at the tail of the ZIP.\n */\nexport function isCharX(data: BinaryData): boolean {\n const zipOffset = getZipOffset(data);\n if (zipOffset < 0) return false;\n\n const zipData = data.subarray(zipOffset);\n const cardJsonMarker = new TextEncoder().encode('card.json');\n\n // Scan the last 64KB where the central directory lives\n // (covers ZIP with up to ~1000 files in the directory)\n const scanSize = Math.min(zipData.length, 65536);\n const startOffset = zipData.length - scanSize;\n\n for (let i = startOffset; i < zipData.length - cardJsonMarker.length; i++) {\n let found = true;\n for (let j = 0; j < cardJsonMarker.length; j++) {\n if (zipData[i + j] !== cardJsonMarker[j]) {\n found = false;\n break;\n }\n }\n if (found) return true;\n }\n\n return false;\n}\n\n/**\n * Check if data is a JPEG+ZIP hybrid (JPEG with appended CharX)\n */\nexport { isJpegCharX };\n\n/**\n * Extract and parse a CharX buffer\n */\nexport function readCharX(\n data: BinaryData,\n options: CharxReadOptions = {}\n): CharxData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n // SECURITY: Streaming unzip with real-time byte limit enforcement\n // This tracks ACTUAL decompressed bytes and aborts if limits exceeded,\n // protecting against malicious archives that lie about sizes in central directory\n let unzipped: Unzipped;\n try {\n unzipped = streamingUnzipSync(data, {\n maxFileSize: opts.maxAssetSize,\n maxTotalSize: opts.maxTotalSize,\n maxFiles: 10000, // CharX can have many assets\n });\n } catch (err) {\n if (err instanceof ZipPreflightError) {\n throw new SizeLimitError(\n err.totalSize || err.entrySize || 0,\n err.maxSize || err.maxEntrySize || opts.maxTotalSize,\n err.oversizedEntry || 'CharX archive'\n );\n }\n throw new ParseError(\n `Failed to unzip CharX: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n\n let cardJson: CCv3Data | null = null;\n const assets: CharxAssetInfo[] = [];\n const metadata = new Map<number, CharxMetaEntry>();\n let moduleRisum: BinaryData | undefined;\n\n // Process entries (size limits already enforced by streamingUnzipSync)\n for (const [fileName, fileData] of Object.entries(unzipped)) {\n // Skip directories (empty or ends with /)\n if (fileName.endsWith('/') || fileData.length === 0) continue;\n\n // Handle card.json\n if (fileName === 'card.json') {\n if (fileData.length > opts.maxFileSize) {\n throw new SizeLimitError(fileData.length, opts.maxFileSize, 'card.json');\n }\n try {\n const content = toString(fileData);\n cardJson = JSON.parse(content) as CCv3Data;\n } catch (err) {\n throw new ParseError(\n `Failed to parse card.json: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n continue;\n }\n\n // Handle x_meta/*.json\n if (opts.preserveXMeta) {\n const metaMatch = fileName.match(/^x_meta\\/(\\d+)\\.json$/);\n if (metaMatch) {\n const index = parseInt(metaMatch[1]!, 10);\n try {\n const content = toString(fileData);\n const meta = JSON.parse(content) as CharxMetaEntry;\n metadata.set(index, meta);\n } catch {\n // Ignore invalid metadata\n }\n continue;\n }\n }\n\n // Handle module.risum (Risu scripts)\n if (fileName === 'module.risum' && opts.preserveModuleRisum) {\n moduleRisum = fileData;\n continue;\n }\n\n // Handle assets/** files\n if (fileName.startsWith('assets/')) {\n const name = fileName.split('/').pop() || 'unknown';\n const ext = name.split('.').pop() || 'bin';\n\n assets.push({\n path: fileName,\n descriptor: {\n type: 'custom',\n name: name.replace(/\\.[^.]+$/, ''), // Remove extension\n uri: `embeded://${fileName}`,\n ext,\n },\n buffer: fileData,\n });\n continue;\n }\n\n // Unknown files are ignored (readme.txt, etc.)\n }\n\n if (!cardJson) {\n throw new ParseError('CharX file does not contain card.json', 'charx');\n }\n\n // Validate that it's a CCv3 card\n if (cardJson.spec !== 'chara_card_v3') {\n throw new ParseError(\n `Invalid card spec: expected \"chara_card_v3\", got \"${cardJson.spec}\"`,\n 'charx'\n );\n }\n\n // Match assets to their descriptors from card.json\n const matchedAssets = matchAssetsToDescriptors(assets, cardJson.data.assets || []);\n\n // Determine if this is a Risu-format CharX\n const isRisuFormat = !!moduleRisum || hasRisuExtensions(cardJson.data.extensions);\n\n return {\n card: cardJson,\n assets: matchedAssets,\n metadata: metadata.size > 0 ? metadata : undefined,\n moduleRisum,\n isRisuFormat,\n };\n}\n\n/**\n * Match extracted asset files to their descriptors from card.json\n *\n * @performance Uses O(1) Map lookup instead of O(n) linear search per descriptor.\n * This reduces complexity from O(n*m) to O(n+m) for large asset packs.\n */\nfunction matchAssetsToDescriptors(\n extractedAssets: CharxAssetInfo[],\n descriptors: AssetDescriptor[]\n): CharxAssetInfo[] {\n // Build O(1) lookup map for extracted assets by path\n const assetsByPath = new Map<string, CharxAssetInfo>();\n for (const asset of extractedAssets) {\n assetsByPath.set(asset.path, asset);\n }\n\n const matched: CharxAssetInfo[] = [];\n\n for (const descriptor of descriptors) {\n const parsed = parseURI(descriptor.uri);\n\n if (parsed.scheme === 'embeded' && parsed.path) {\n // O(1) lookup instead of O(n) find\n const asset = assetsByPath.get(parsed.path);\n\n if (asset) {\n matched.push({\n ...asset,\n descriptor,\n });\n } else {\n // Asset referenced but not found in ZIP\n matched.push({\n path: parsed.path,\n descriptor,\n buffer: undefined,\n });\n }\n } else if (parsed.scheme === 'ccdefault') {\n // Default asset, no file needed\n matched.push({\n path: 'ccdefault:',\n descriptor,\n buffer: undefined,\n });\n } else if (parsed.scheme === 'https' || parsed.scheme === 'http') {\n // Remote asset, no file needed\n matched.push({\n path: descriptor.uri,\n descriptor,\n buffer: undefined,\n });\n } else if (parsed.scheme === 'data') {\n // Data URI, extract the data\n if (parsed.data && parsed.encoding === 'base64') {\n const buffer = base64Decode(parsed.data);\n matched.push({\n path: 'data:',\n descriptor,\n buffer,\n });\n } else {\n matched.push({\n path: 'data:',\n descriptor,\n buffer: undefined,\n });\n }\n }\n }\n\n return matched;\n}\n\n/**\n * Extract just the card.json from a CharX buffer (quick validation)\n */\nexport function readCardJsonOnly(data: BinaryData): CCv3Data {\n // Use streaming unzip with conservative limits for card.json extraction\n // This protects against zip bombs even for the \"quick validation\" path\n let unzipped: Unzipped;\n try {\n unzipped = streamingUnzipSync(data, {\n maxFileSize: DEFAULT_OPTIONS.maxFileSize, // 10MB for card.json\n maxTotalSize: DEFAULT_OPTIONS.maxTotalSize, // 200MB total\n maxFiles: 10000,\n });\n } catch (err) {\n throw new ParseError(\n `Failed to unzip CharX: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n\n const cardData = unzipped['card.json'];\n if (!cardData) {\n throw new ParseError('card.json not found in CharX file', 'charx');\n }\n\n try {\n const content = toString(cardData);\n return JSON.parse(content) as CCv3Data;\n } catch (err) {\n throw new ParseError(\n `Failed to parse card.json: ${err instanceof Error ? err.message : String(err)}`,\n 'charx'\n );\n }\n}\n\n/**\n * Async version of readCharX with optional remote asset fetching\n */\nexport async function readCharXAsync(\n data: BinaryData,\n options: CharxReadOptions & { fetchRemoteAssets?: boolean; assetFetcher?: AssetFetcher } = {}\n): Promise<CharxData> {\n // First do the sync extraction\n const result = readCharX(data, options);\n\n // If remote fetching is disabled or no fetcher provided, return as-is\n if (!options.fetchRemoteAssets || !options.assetFetcher) {\n return result;\n }\n\n // Fetch remote assets\n const fetchedAssets = await Promise.all(\n result.assets.map(async (asset) => {\n // Only fetch assets that don't have buffers and have remote URLs\n if (asset.buffer) {\n return asset;\n }\n\n const parsed = parseURI(asset.descriptor.uri);\n\n if ((parsed.scheme === 'https' || parsed.scheme === 'http') && parsed.url) {\n try {\n const buffer = await options.assetFetcher!(parsed.url);\n if (buffer) {\n return { ...asset, buffer };\n }\n } catch {\n // Failed to fetch, leave buffer undefined\n }\n }\n\n return asset;\n })\n );\n\n return {\n ...result,\n assets: fetchedAssets,\n };\n}\n","/**\n * CharX Writer\n *\n * Creates .charx (ZIP-based character card) files.\n */\n\nimport { zipSync, type Zippable } from 'fflate';\nimport {\n fromString,\n getMimeTypeFromExt,\n} from '@character-foundry/core';\nimport type { CCv3Data, AssetDescriptor } from '@character-foundry/schemas';\nimport type {\n CharxWriteAsset,\n CharxWriteOptions,\n CharxBuildResult,\n CompressionLevel,\n} from './types.js';\n\n/** Safe asset types for CharX path construction (whitelist) */\nconst SAFE_ASSET_TYPES = new Set([\n 'icon', 'user_icon', 'emotion', 'background', 'sound', 'video',\n 'custom', 'x-risu-asset', 'data', 'unknown',\n]);\n\n/**\n * Get CharX category from MIME type\n */\nfunction getCharxCategory(mimetype: string): string {\n if (mimetype.startsWith('image/')) return 'images';\n if (mimetype.startsWith('audio/')) return 'audio';\n if (mimetype.startsWith('video/')) return 'video';\n return 'other';\n}\n\n/**\n * Sanitize an asset type for safe use in file paths.\n * Only allows whitelisted types to prevent path traversal.\n */\nfunction sanitizeAssetType(type: string): string {\n // Normalize to lowercase\n const normalized = type.toLowerCase().replace(/[^a-z0-9-_]/g, '-');\n\n // Use whitelist - if not in whitelist, default to 'custom'\n if (SAFE_ASSET_TYPES.has(normalized)) {\n return normalized;\n }\n\n // For unknown types, sanitize strictly\n const sanitized = normalized.replace(/[^a-z0-9]/g, '');\n return sanitized || 'custom';\n}\n\n/**\n * Sanitize a file extension for safe use in file paths.\n *\n * @remarks\n * CharX assets may be arbitrary file types (including scripts/text). We validate\n * for path-safety and normalize minimally, rather than coercing unknown\n * extensions to `.bin`.\n */\nfunction sanitizeExtension(ext: string): string {\n const normalized = ext.trim().replace(/^\\./, '').toLowerCase();\n\n if (!normalized) {\n throw new Error('Invalid asset extension: empty extension');\n }\n\n if (normalized.length > 64) {\n throw new Error(`Invalid asset extension: too long (${normalized.length} chars)`);\n }\n\n // Prevent zip path traversal / separators\n if (normalized.includes('/') || normalized.includes('\\\\') || normalized.includes('\\0')) {\n throw new Error('Invalid asset extension: path separators are not allowed');\n }\n\n // Conservative filename safety while still allowing common multi-part extensions (e.g. tar.gz)\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {\n throw new Error(`Invalid asset extension: \"${ext}\"`);\n }\n\n return normalized;\n}\n\n/**\n * Sanitize a name for use in file paths\n */\nfunction sanitizeName(name: string, ext: string): string {\n let safeName = name;\n\n // Strip extension if present\n if (safeName.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {\n safeName = safeName.substring(0, safeName.length - (ext.length + 1));\n }\n\n // Replace dots and underscores with hyphens, remove special chars, collapse dashes\n safeName = safeName\n .replace(/[._]/g, '-')\n .replace(/[^a-zA-Z0-9-]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n\n if (!safeName) safeName = 'asset';\n\n return safeName;\n}\n\n/**\n * Build a CharX ZIP from card data and assets\n */\nexport function writeCharX(\n card: CCv3Data,\n assets: CharxWriteAsset[],\n options: CharxWriteOptions = {}\n): CharxBuildResult {\n const {\n spec = 'v3',\n compressionLevel = 6,\n emitXMeta = spec === 'risu',\n emitReadme = false,\n moduleRisum,\n } = options;\n\n // Transform card to use embeded:// URIs\n const transformedCard = transformAssetUris(card, assets);\n\n // Create ZIP entries\n const zipEntries: Zippable = {};\n\n // Add card.json\n const cardJson = JSON.stringify(transformedCard, null, 2);\n zipEntries['card.json'] = [fromString(cardJson), { level: compressionLevel as CompressionLevel }];\n\n // Add readme.txt if requested\n if (emitReadme) {\n const readme = `Character: ${card.data.name}\nCreated with Character Foundry\n\nThis is a CharX character card package.\nImport this file into SillyTavern, RisuAI, or other compatible applications.\n`;\n zipEntries['readme.txt'] = [fromString(readme), { level: compressionLevel as CompressionLevel }];\n }\n\n // Add assets\n let assetCount = 0;\n\n for (let i = 0; i < assets.length; i++) {\n const asset = assets[i]!;\n // SECURITY: Sanitize all path components to prevent path traversal\n const safeType = sanitizeAssetType(asset.type);\n const safeExt = sanitizeExtension(asset.ext);\n const mimetype = getMimeTypeFromExt(safeExt);\n const category = getCharxCategory(mimetype);\n const safeName = sanitizeName(asset.name, safeExt);\n\n const assetPath = `assets/${safeType}/${category}/${safeName}.${safeExt}`;\n\n zipEntries[assetPath] = [asset.data, { level: compressionLevel as CompressionLevel }];\n assetCount++;\n\n // Add x_meta if enabled and it's an image\n if (emitXMeta && mimetype.startsWith('image/')) {\n const metaJson = JSON.stringify({\n type: mimetype.split('/')[1]?.toUpperCase() || 'PNG',\n });\n zipEntries[`x_meta/${i}.json`] = [fromString(metaJson), { level: compressionLevel as CompressionLevel }];\n }\n }\n\n // Add module.risum for Risu format (opaque preservation)\n if (moduleRisum) {\n zipEntries['module.risum'] = [moduleRisum, { level: compressionLevel as CompressionLevel }];\n }\n\n // Create ZIP\n const buffer = zipSync(zipEntries);\n\n return {\n buffer,\n assetCount,\n totalSize: buffer.length,\n };\n}\n\n/**\n * Transform asset URIs in card to use embeded:// format\n */\nfunction transformAssetUris(card: CCv3Data, assets: CharxWriteAsset[]): CCv3Data {\n // Clone the card to avoid mutations\n // Note: Using structuredClone where available for better performance and preserving undefined\n const transformed: CCv3Data = typeof structuredClone === 'function'\n ? structuredClone(card)\n : JSON.parse(JSON.stringify(card));\n\n // Generate assets array from provided assets\n transformed.data.assets = assets.map((asset): AssetDescriptor => {\n // SECURITY: Sanitize all path components to prevent path traversal\n const safeType = sanitizeAssetType(asset.type);\n const safeExt = sanitizeExtension(asset.ext);\n const mimetype = getMimeTypeFromExt(safeExt);\n const category = getCharxCategory(mimetype);\n const safeName = sanitizeName(asset.name, safeExt);\n\n return {\n type: asset.type as AssetDescriptor['type'],\n uri: `embeded://assets/${safeType}/${category}/${safeName}.${safeExt}`,\n name: safeName,\n ext: safeExt,\n };\n });\n\n return transformed;\n}\n\n/**\n * Async version of writeCharX\n */\nexport async function writeCharXAsync(\n card: CCv3Data,\n assets: CharxWriteAsset[],\n options: CharxWriteOptions = {}\n): Promise<CharxBuildResult> {\n // For now, just wrap sync version\n return writeCharX(card, assets, options);\n}\n"],"mappings":"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","names":["z","z"]}
|