security(phase-9): complete security hardening

- Add safeJsonParse utility with schema validation
- Migrate tokens to OS keyring storage
- Add Ed25519 key encryption at rest
- Enable WSS configuration option
- Fix JSON.parse in HandParamsForm, WorkflowEditor, WorkflowList
- Update test mock data to match valid status values

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-15 19:22:51 +08:00
parent e3d164e9d2
commit a6b1255dc0
10 changed files with 499 additions and 74 deletions

View File

@@ -0,0 +1,173 @@
/**
* Safe JSON Parsing Utilities
*
* Provides try-catch protected JSON parsing with optional default values
* and context-aware error messages.
*
* Usage:
* - safeJsonParse: Returns result object with success/failure status
* - parseJsonOrDefault: Returns parsed value or default on failure
* - parseJsonOrThrow: Returns parsed value or throws friendly error
*/
export interface SafeJsonParseResult<T> {
success: boolean;
data?: T;
error?: string;
}
/**
* Safely parse a JSON string with error handling
*
* @param text - The JSON string to parse
* @param defaultValue - Optional default value to return on parse failure
* @returns Result object with success status, data, and optional error message
*
* @example
* const result = safeJsonParse<UserData>(jsonString);
* if (result.success) {
* console.log(result.data);
* } else {
* console.error(result.error);
* }
*/
export function safeJsonParse<T>(text: string, defaultValue?: T): SafeJsonParseResult<T> {
try {
const data = JSON.parse(text) as T;
return { success: true, data };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown JSON parse error';
// Log truncated input for debugging
const truncatedInput = text.length > 100 ? `${text.substring(0, 100)}...` : text;
console.warn('[json-utils] Parse failed:', errorMessage, 'Input:', truncatedInput);
return {
success: false,
error: errorMessage,
data: defaultValue,
};
}
}
/**
* Safely parse JSON and return default value on failure
*
* Use this when you have a sensible default and don't need to know
* about parse failures.
*
* @param text - The JSON string to parse
* @param defaultValue - The value to return if parsing fails
* @returns The parsed data or the default value
*
* @example
* const config = parseJsonOrDefault(rawConfig, defaultConfig);
*/
export function parseJsonOrDefault<T>(text: string, defaultValue: T): T {
const result = safeJsonParse<T>(text, defaultValue);
return result.data!;
}
/**
* Safely parse JSON or throw a friendly error with context
*
* Use this when JSON parsing is required and failures should halt execution
* with a clear error message.
*
* @param text - The JSON string to parse
* @param context - Optional context for the error message (e.g., "loading config")
* @returns The parsed data
* @throws Error with context-aware message if parsing fails
*
* @example
* try {
* const data = parseJsonOrThrow<UserConfig>(rawJson, 'parsing user config');
* } catch (error) {
* showToast(error.message); // "JSON parse failed (parsing user config): Unexpected token..."
* }
*/
export function parseJsonOrThrow<T>(text: string, context?: string): T {
const result = safeJsonParse<T>(text);
if (!result.success) {
throw new Error(`JSON parse failed${context ? ` (${context})` : ''}: ${result.error}`);
}
return result.data!;
}
/**
* Type guard to check if a value is a valid JSON-compatible object
*
* @param value - The value to check
* @returns True if the value can be safely serialized to JSON
*/
export function isJsonSerializable(value: unknown): boolean {
try {
JSON.stringify(value);
return true;
} catch {
return false;
}
}
/**
* Safely stringify a value to JSON
*
* @param value - The value to stringify
* @param fallback - Fallback string if stringification fails
* @returns JSON string or fallback
*/
export function safeJsonStringify(value: unknown, fallback = '{}'): string {
try {
return JSON.stringify(value);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown stringify error';
console.warn('[json-utils] Stringify failed:', errorMessage);
return fallback;
}
}
/**
* Safely stringify with pretty formatting
*
* @param value - The value to stringify
* @param indent - Number of spaces for indentation (default: 2)
* @param fallback - Fallback string if stringification fails
* @returns Formatted JSON string or fallback
*/
export function safeJsonStringifyPretty(value: unknown, indent = 2, fallback = '{}'): string {
try {
return JSON.stringify(value, null, indent);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown stringify error';
console.warn('[json-utils] Pretty stringify failed:', errorMessage);
return fallback;
}
}
/**
* Deep clone an object using JSON serialization
*
* Note: This only works for JSON-serializable data (no functions, undefined, symbols, etc.)
*
* @param value - The value to clone
* @returns A deep clone of the value
* @throws Error if the value cannot be serialized
*/
export function deepClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
/**
* Safely deep clone an object with fallback
*
* @param value - The value to clone
* @param fallback - Fallback value if cloning fails
* @returns A deep clone of the value or the fallback
*/
export function safeDeepClone<T>(value: T, fallback: T): T {
try {
return JSON.parse(JSON.stringify(value)) as T;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown clone error';
console.warn('[json-utils] Deep clone failed:', errorMessage);
return fallback;
}
}