Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,14 @@ try {
} catch (error) {
if (error instanceof DataSanitizationError) {
console.error(error.message); // 'Invalid data type'
console.error(error.details); // { originalData: 123 }
console.error(error.details); // { inputType: 'number' }
}
}
```

Error details are limited to safe diagnostic metadata and do not include the
original input payload.

## How it works

1. **String input** is sanitized directly via regex replacement.
Expand Down
44 changes: 44 additions & 0 deletions docs/plans/003-sanitization-error-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Sanitization Error Details

## Approach

Update `sanitizeData` so wrapped sanitization failures expose only safe
diagnostic metadata instead of retaining caller payloads. Keep the public
`DataSanitizationError` shape intact while replacing raw `originalData` entries
with input type and wrapped error metadata that callers can log without leaking
sensitive values.

## Pre-implementation

Create issue branch `fix/279/sanitization_error_details` for GitHub issue #279.

## Steps

1. Update `src/index.ts` to build safe error details for invalid input and
wrapped failures.
2. Update `test/index-errors.test.ts` with regression coverage for malformed
sanitized JSON and sensitive payload values.
3. Update `README.md` so the error handling example logs safe fields instead of
raw details.

## Relevant Files

- `docs/plans/003-sanitization-error-details.md` - new plan for issue #279.
- `src/index.ts` - updated sanitizer error details.
- `test/index-errors.test.ts` - updated regression coverage for safe error
details.
- `README.md` - updated error handling guidance.

## Verification

Run the full test suite, linting, and formatting checks through package scripts.

## Decisions

- Preserve `DataSanitizationError.details` as a structured object so existing
callers keep a stable error shape.
- Use safe metadata instead of sanitized payload summaries because parse failures
happen when the sanitized payload may be malformed, and summaries can still
reveal application data shape or user content.
- Include the wrapped error name for debugging while excluding raw input, error
objects, messages, and stack traces from public details.
4 changes: 2 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* @example
* const error = new DataSanitizationError('Invalid data type', {
* originalData: 123,
* inputType: 'number',
* });
* error.name;
* // => 'DataSanitizationError'
Expand All @@ -20,7 +20,7 @@ class DataSanitizationError extends Error {
*
* @example
* const error = new DataSanitizationError('Invalid data type', {
* originalData: 123,
* inputType: 'number',
* });
*/
constructor(message: string, details: unknown = {}) {
Expand Down
55 changes: 52 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,56 @@ import { DataSanitizationError } from './errors';
import { stringReplacer } from './replacers';
import { DataSanitizationReplacer } from './types';

/**
* Returns a safe type label for data passed to the sanitizer.
*
* @param data - Input value being sanitized.
* @returns A type label that does not expose the input value.
* @throws Does not throw.
*
* @example
* getInputType({ password: 'secret' })
* // => 'object'
*/
const getInputType = (data: unknown): string => {
if (data === null) {
return 'null';
}

if (Array.isArray(data)) {
return 'array';
}

return typeof data;
};

/**
* Builds public error details that do not include caller payloads.
*
* @param data - Input value being sanitized.
* @param error - Optional wrapped error from the failed operation.
* @returns Structured diagnostics safe for logging.
* @throws Does not throw.
*
* @example
* createSafeErrorDetails({ password: 'secret' }, new SyntaxError('Bad JSON'))
* // => { inputType: 'object', errorName: 'SyntaxError' }
*/
const createSafeErrorDetails = (
data: unknown,
error?: unknown,
): Record<string, string> => {
const details: Record<string, string> = {
inputType: getInputType(data),
};

if (error instanceof Error) {
details.errorName = error.name;
}

return details;
};

/**
* Sanitizes data in an object/string to make it safe for logging
* or other purposes of a sensitive nature
Expand Down Expand Up @@ -49,15 +99,14 @@ const sanitizeData: DataSanitizationReplacer = (data, options = {}) => {
}

throw new DataSanitizationError('Invalid data type', {
originalData: data,
...createSafeErrorDetails(data),
});
} catch (error) {
if (error instanceof DataSanitizationError) {
throw error;
}
throw new DataSanitizationError('Error parsing data', {
error,
originalData: data,
...createSafeErrorDetails(data, error),
});
}
};
Expand Down
121 changes: 119 additions & 2 deletions test/index-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,24 @@ describe('DataSanitizationIndexAndErrors', () => {
it('should throw DataSanitizationError for invalid data type', () => {
// Arrange
const input = 123 as unknown as Record<string, unknown>;
let thrownError: unknown;

// Act
const act = (): void => {
sanitizeData(input);
try {
sanitizeData(input);
} catch (error) {
thrownError = error;
throw error;
}
};

// Assert
expect(act).toThrowError(DataSanitizationError);
expect(act).toThrowError('Invalid data type');
expect((thrownError as DataSanitizationError).details).toEqual({
inputType: 'number',
});
});

it('should throw DataSanitizationError for boolean input', () => {
Expand Down Expand Up @@ -156,15 +165,123 @@ describe('DataSanitizationIndexAndErrors', () => {
// Arrange
const input: Record<string, unknown> = {};
input.self = input;
let thrownError: unknown;

// Act
const act = (): void => {
sanitizeData(input);
try {
sanitizeData(input);
} catch (error) {
thrownError = error;
throw error;
}
};

// Assert
expect(act).toThrowError(DataSanitizationError);
expect(act).toThrowError('Error parsing data');
expect((thrownError as DataSanitizationError).details).toEqual({
errorName: 'TypeError',
inputType: 'object',
});
});

it('should report null input type in wrapped error details', () => {
// Arrange
const input = null as unknown as Record<string, unknown>;
const failingMatcher = (): RegExp => {
throw new Error('matcher failed');
};
let thrownError: unknown;

// Act
const act = (): void => {
try {
sanitizeData(input, {
customMatchers: [failingMatcher],
customPatterns: ['password'],
useDefaultMatchers: false,
useDefaultPatterns: false,
});
} catch (error) {
thrownError = error;
throw error;
}
};

// Assert
expect(act).toThrowError(DataSanitizationError);
expect(act).toThrowError('Error parsing data');
expect(thrownError).toBeInstanceOf(DataSanitizationError);
expect((thrownError as DataSanitizationError).details).toEqual({
errorName: 'Error',
inputType: 'null',
});
});

it('should report array input type in wrapped error details', () => {
// Arrange
const input: unknown[] = [];
input.push(input);
let thrownError: unknown;

// Act
const act = (): void => {
try {
sanitizeData(input as unknown as Record<string, unknown>);
} catch (error) {
thrownError = error;
throw error;
}
};

// Assert
expect(act).toThrowError(DataSanitizationError);
expect(act).toThrowError('Error parsing data');
expect(thrownError).toBeInstanceOf(DataSanitizationError);
expect((thrownError as DataSanitizationError).details).toEqual({
errorName: 'TypeError',
inputType: 'array',
});
});

it('should not expose sensitive object input in parse error details', () => {
// Arrange
const input = {
password: 'abc"def',
username: 'mark',
};
let thrownError: unknown;

// Act
const act = (): void => {
try {
sanitizeData(input);
} catch (error) {
thrownError = error;
throw error;
}
};

// Assert
expect(act).toThrowError(DataSanitizationError);
expect(act).toThrowError('Error parsing data');
expect(thrownError).toBeInstanceOf(DataSanitizationError);

const details = (thrownError as DataSanitizationError).details as Record<
string,
unknown
>;
const serializedDetails = JSON.stringify(details);

expect(details).toEqual({
errorName: 'SyntaxError',
inputType: 'object',
});
expect(details).not.toHaveProperty('originalData');
expect(details).not.toHaveProperty('error');
expect(serializedDetails).not.toContain(input.password);
expect(serializedDetails).not.toContain(input.username);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

Expand Down
Loading