fix(v032): healthCheck reason PII 마스킹 (#39)
err.message 안에 LAN endpoint URL (예: 192.168.x.x:11434) 이 포함될 수
있어 telemetry 파일에 PII 우회 노출. v0.2.3.1 in-app endpoint UI 가 LAN
사용을 흔하게 만들어 노출 경로 확대.
classifyFetchError 로 error class 분류 (network/timeout/dns/other) 후
reason: 'unreachable:{class}' 형태만 emit. host/IP 노출 0.
This commit is contained in:
@@ -5,6 +5,14 @@ import { buildVisionPrompt } from './visionPrompt.js';
|
||||
import type { GenerateInput, GenerateOptions, HealthResult, InferenceProvider } from './InferenceProvider.js';
|
||||
import { DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL } from '../../shared/constants.js';
|
||||
|
||||
function classifyFetchError(e: unknown): 'network' | 'timeout' | 'dns' | 'other' {
|
||||
const msg = ((e as Error)?.message ?? '').toLowerCase();
|
||||
if (msg.includes('aborted') || msg.includes('timeout')) return 'timeout';
|
||||
if (msg.includes('econnrefused') || msg.includes('econnreset')) return 'network';
|
||||
if (msg.includes('enotfound') || msg.includes('eai_again')) return 'dns';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
export interface LocalOllamaOptions {
|
||||
endpoint?: string;
|
||||
model?: string;
|
||||
@@ -119,7 +127,8 @@ export class LocalOllamaProvider implements InferenceProvider {
|
||||
return found ? { ok: true, model: this.model }
|
||||
: { ok: false, reason: `${this.model} not installed` };
|
||||
} catch (err) {
|
||||
return { ok: false, reason: `unreachable: ${(err as Error).message}` };
|
||||
const cls = classifyFetchError(err);
|
||||
return { ok: false, reason: `unreachable:${cls}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,44 @@ describe('LocalOllamaProvider', () => {
|
||||
expect(provider.name).toBe('local-ollama/gemma4:26b');
|
||||
});
|
||||
|
||||
describe('healthCheck PII reason masking', () => {
|
||||
it('classifies ECONNREFUSED as network', async () => {
|
||||
mock.get('http://192.168.1.5:11434').intercept({ path: '/api/tags', method: 'GET' })
|
||||
.replyWithError(new Error('connect ECONNREFUSED 192.168.1.5:11434'));
|
||||
const provider = new LocalOllamaProvider({ endpoint: 'http://192.168.1.5:11434' });
|
||||
const h = await provider.healthCheck();
|
||||
expect(h.ok).toBe(false);
|
||||
expect(h.reason).toBe('unreachable:network');
|
||||
expect(h.reason).not.toContain('192.168.1.5');
|
||||
});
|
||||
|
||||
it('classifies AbortError/timeout as timeout', async () => {
|
||||
mock.get('http://localhost:11434').intercept({ path: '/api/tags', method: 'GET' })
|
||||
.replyWithError(new Error('The operation was aborted due to timeout'));
|
||||
const h = await new LocalOllamaProvider().healthCheck();
|
||||
expect(h.ok).toBe(false);
|
||||
expect(h.reason).toBe('unreachable:timeout');
|
||||
});
|
||||
|
||||
it('classifies ENOTFOUND as dns', async () => {
|
||||
mock.get('http://nonexistent.local:11434').intercept({ path: '/api/tags', method: 'GET' })
|
||||
.replyWithError(new Error('getaddrinfo ENOTFOUND nonexistent.local'));
|
||||
const provider = new LocalOllamaProvider({ endpoint: 'http://nonexistent.local:11434' });
|
||||
const h = await provider.healthCheck();
|
||||
expect(h.ok).toBe(false);
|
||||
expect(h.reason).toBe('unreachable:dns');
|
||||
expect(h.reason).not.toContain('nonexistent.local');
|
||||
});
|
||||
|
||||
it('falls back to other for unknown errors', async () => {
|
||||
mock.get('http://localhost:11434').intercept({ path: '/api/tags', method: 'GET' })
|
||||
.replyWithError(new Error('something weird happened'));
|
||||
const h = await new LocalOllamaProvider().healthCheck();
|
||||
expect(h.ok).toBe(false);
|
||||
expect(h.reason).toBe('unreachable:other');
|
||||
});
|
||||
});
|
||||
|
||||
describe('vision path (v0.3.1 Cut F)', () => {
|
||||
it('visionModel + images → body.images + model=visionModel + buildVisionPrompt', async () => {
|
||||
let capturedBody: string = '';
|
||||
|
||||
Reference in New Issue
Block a user