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
42 changes: 42 additions & 0 deletions examples/ota/src/__tests__/multi-search-sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Unit tests for multi-search error sanitization (QA fix-forward candidate).
*/

import { describe, it, expect } from 'vitest';
import type { DistributionAdapter, SearchRequest, SearchResponse } from '@otaip/core';
import { MultiSearchService } from '../services/multi-search-service.js';

function failingAdapter(name: string, message: string): DistributionAdapter {
return {
name,
async isAvailable() {
return true;
},
async search(_req: SearchRequest): Promise<SearchResponse> {
throw new Error(message);
},
};
}

describe('MultiSearchService — error sanitization', () => {
it('redacts credential hints from per-adapter error metadata', async () => {
const service = new MultiSearchService({
adapters: new Map([
[
'leaky',
failingAdapter('leaky', 'Adapter failed with key hint: sk_live_secret123'),
],
]),
});

const result = await service.search({
segments: [{ origin: 'JFK', destination: 'LAX', departure_date: '2026-07-15' }],
passengers: [{ type: 'ADT', count: 1 }],
cabin_class: 'economy',
currency: 'USD',
});

expect(result.sources[0]?.error).not.toContain('sk_live_secret123');
expect(result.sources[0]?.error).toContain('[redacted]');
});
});
15 changes: 10 additions & 5 deletions examples/ota/src/services/multi-search-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ export interface AggregatedSearchResult {
readonly sources: readonly SourceStatus[];
}

// ---------------------------------------------------------------------------
// Timeout helper
// ---------------------------------------------------------------------------
/** Strip credential-like substrings from adapter errors before exposing to clients. */
function sanitizeAdapterError(message: string): string {
return message
.replace(/(?:api[_-]?key|secret|token|Bearer)\s*[:=]?\s*\S+/gi, '[redacted]')
.replace(/\b(sk|pk)_(live|test)_[A-Za-z0-9]+\b/g, '[redacted]')
.replace(/key hint:\s*\S+/gi, 'key hint: [redacted]');
}

function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
Expand Down Expand Up @@ -116,10 +120,11 @@ export class MultiSearchService {
durationMs,
});
} else {
const errorMessage =
const errorMessage = sanitizeAdapterError(
result.reason instanceof Error
? result.reason.message
: String(result.reason);
: String(result.reason),
);
sources.push({
adapter: adapterName,
success: false,
Expand Down
Loading