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 type { GenerateInput, GenerateOptions, HealthResult, InferenceProvider } from './InferenceProvider.js';
|
||||||
import { DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL } from '../../shared/constants.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 {
|
export interface LocalOllamaOptions {
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -119,7 +127,8 @@ export class LocalOllamaProvider implements InferenceProvider {
|
|||||||
return found ? { ok: true, model: this.model }
|
return found ? { ok: true, model: this.model }
|
||||||
: { ok: false, reason: `${this.model} not installed` };
|
: { ok: false, reason: `${this.model} not installed` };
|
||||||
} catch (err) {
|
} 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');
|
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)', () => {
|
describe('vision path (v0.3.1 Cut F)', () => {
|
||||||
it('visionModel + images → body.images + model=visionModel + buildVisionPrompt', async () => {
|
it('visionModel + images → body.images + model=visionModel + buildVisionPrompt', async () => {
|
||||||
let capturedBody: string = '';
|
let capturedBody: string = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user