diff --git a/packages/core/src/__tests__/escape.test.ts b/packages/core/src/__tests__/escape.test.ts
new file mode 100644
index 0000000..1b7c5e5
--- /dev/null
+++ b/packages/core/src/__tests__/escape.test.ts
@@ -0,0 +1,63 @@
+import { describe, it, expect } from 'vitest';
+import { safeStringify, isValidJson } from '../escape.js';
+
+describe('safeStringify', () => {
+ it('returns valid JSON for simple objects', () => {
+ const result = safeStringify({ ok: true, data: { id: 'p_123' } });
+ expect(() => JSON.parse(result)).not.toThrow();
+ expect(JSON.parse(result)).toEqual({ ok: true, data: { id: 'p_123' } });
+ });
+
+ it('escapes double quotes in string values', () => {
+ const result = safeStringify({ text: 'He said "hello"' });
+ const parsed = JSON.parse(result);
+ expect(parsed.text).toBe('He said "hello"');
+ });
+
+ it('escapes backslashes in string values', () => {
+ const result = safeStringify({ path: 'C:\\Users\\test' });
+ const parsed = JSON.parse(result);
+ expect(parsed.path).toBe('C:\\Users\\test');
+ });
+
+ it('escapes control characters (newline, tab)', () => {
+ const result = safeStringify({ text: 'line1\nline2\tindented' });
+ const parsed = JSON.parse(result);
+ expect(parsed.text).toBe('line1\nline2\tindented');
+ });
+
+ it('handles unicode characters', () => {
+ const result = safeStringify({ text: '你好世界 🌍' });
+ const parsed = JSON.parse(result);
+ expect(parsed.text).toBe('你好世界 🌍');
+ });
+
+ it('handles HTML-like content without breaking JSON', () => {
+ const html = '
Hello
';
+ const result = safeStringify({ html });
+ const parsed = JSON.parse(result);
+ expect(parsed.html).toBe(html);
+ });
+
+ it('handles empty string and null', () => {
+ expect(JSON.parse(safeStringify({ a: '' }))).toEqual({ a: '' });
+ expect(JSON.parse(safeStringify({ a: null }))).toEqual({ a: null });
+ });
+
+ it('handles arrays with special characters', () => {
+ const result = safeStringify({ items: ['a"b', 'c\\d', 'e\nf'] });
+ const parsed = JSON.parse(result);
+ expect(parsed.items).toEqual(['a"b', 'c\\d', 'e\nf']);
+ });
+});
+
+describe('isValidJson', () => {
+ it('returns true for valid JSON', () => {
+ expect(isValidJson('{"ok":true}')).toBe(true);
+ });
+
+ it('returns false for invalid JSON', () => {
+ expect(isValidJson('{ok:true}')).toBe(false);
+ expect(isValidJson('')).toBe(false);
+ });
+});
diff --git a/packages/core/src/escape.ts b/packages/core/src/escape.ts
new file mode 100644
index 0000000..8eafd54
--- /dev/null
+++ b/packages/core/src/escape.ts
@@ -0,0 +1,15 @@
+// JSON safe serialization / JSON 安全序列化
+// Always use this instead of manual string concatenation for JSON output
+
+export function safeStringify(obj: unknown): string {
+ return JSON.stringify(obj);
+}
+
+export function isValidJson(str: string): boolean {
+ try {
+ JSON.parse(str);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 9d0d3bd..39c3620 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -8,3 +8,4 @@ export {
type CanvasNoiseConfig,
type PermissionState,
} from './types/fingerprint.js';
+export { safeStringify, isValidJson } from './escape.js';