From 186223647c2b2840461de21b9a2658531854d33b Mon Sep 17 00:00:00 2001 From: MarkXian Date: Tue, 28 Jul 2026 11:24:10 +0800 Subject: [PATCH] fix(node-sdk): validate catalog payloads --- .changeset/clean-catalog-payloads.md | 5 +++++ packages/node-sdk/src/catalog.ts | 14 +++++++++++--- packages/node-sdk/test/catalog.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .changeset/clean-catalog-payloads.md diff --git a/.changeset/clean-catalog-payloads.md b/.changeset/clean-catalog-payloads.md new file mode 100644 index 0000000000..fb3c2a1ff8 --- /dev/null +++ b/.changeset/clean-catalog-payloads.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code-sdk": patch +--- + +Reject invalid catalog payloads consistently. diff --git a/packages/node-sdk/src/catalog.ts b/packages/node-sdk/src/catalog.ts index 78cb2afd31..24df5c7e75 100644 --- a/packages/node-sdk/src/catalog.ts +++ b/packages/node-sdk/src/catalog.ts @@ -49,10 +49,13 @@ export async function fetchCatalog( if (!res.ok) { throw new CatalogFetchError(`Failed to fetch catalog (HTTP ${res.status}).`, res.status); } - const payload: unknown = await res.json(); - if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) { + let payload: unknown; + try { + payload = await res.json(); + } catch { throw new Error(`Unexpected catalog response from ${url}.`); } + if (!isCatalogPayload(payload)) throw new Error(`Unexpected catalog response from ${url}.`); return payload as Catalog; } @@ -109,12 +112,17 @@ export interface ApplyCatalogProviderOptions { export function loadBuiltInCatalog(text?: string): Catalog | undefined { if (typeof text !== 'string' || text.length === 0) return undefined; try { - return JSON.parse(text) as Catalog; + const payload: unknown = JSON.parse(text); + return isCatalogPayload(payload) ? (payload as Catalog) : undefined; } catch { return undefined; } } +function isCatalogPayload(payload: unknown): boolean { + return typeof payload === 'object' && payload !== null && !Array.isArray(payload); +} + /** * Writes a catalog-selected provider and its model aliases into `config` and * marks it the default. Model metadata (context, output limit, capabilities) diff --git a/packages/node-sdk/test/catalog.test.ts b/packages/node-sdk/test/catalog.test.ts index d1bc5a6ea9..a0444bdded 100644 --- a/packages/node-sdk/test/catalog.test.ts +++ b/packages/node-sdk/test/catalog.test.ts @@ -7,6 +7,7 @@ import { catalogProviderModels, CatalogFetchError, fetchCatalog, + loadBuiltInCatalog, type CatalogModel, } from '../src/catalog'; @@ -17,6 +18,13 @@ function catalogResponse(body: unknown, status = 200): Response { }); } +function rawCatalogResponse(body: string, status = 200): Response { + return new Response(body, { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + const model: CatalogModel = { id: 'm1', name: 'M1', @@ -55,6 +63,13 @@ describe('fetchCatalog', () => { ).rejects.toThrow(/Unexpected catalog response/); }); + it('throws on invalid JSON', async () => { + const fetchMock = vi.fn(async () => rawCatalogResponse('{')); + await expect( + fetchCatalog('https://x', { fetchImpl: fetchMock as unknown as typeof fetch }), + ).rejects.toThrow(/Unexpected catalog response/); + }); + it('sends the given User-Agent, and none by default', async () => { const fetchMock = vi.fn(async () => catalogResponse({})); @@ -79,6 +94,13 @@ describe('fetchCatalog', () => { }); }); +describe('loadBuiltInCatalog', () => { + it('returns undefined for invalid or non-object payloads', () => { + expect(loadBuiltInCatalog('{')).toBeUndefined(); + expect(loadBuiltInCatalog('[1, 2]')).toBeUndefined(); + }); +}); + describe('catalogModelToAlias', () => { it('flattens a catalog model capability into alias fields', () => { expect(catalogModelToAlias('anthropic', model)).toEqual({