diff --git a/.changeset/0017-fix-download-template.md b/.changeset/0017-fix-download-template.md new file mode 100644 index 0000000..36bf341 --- /dev/null +++ b/.changeset/0017-fix-download-template.md @@ -0,0 +1,5 @@ +--- +"@cdot65/prisma-airs-cli": patch +--- + +Fix `airs redteam prompt-sets download` crashing with `Cannot read properties of undefined (reading 'getToken')`. The previous workaround reached into SDK internals that no longer exist. With `@cdot65/prisma-airs-sdk` 0.8.3 the SDK now returns CSV correctly, so the workaround is removed and the method delegates straight to `customAttacks.downloadTemplate()`. diff --git a/package.json b/package.json index 5fa0d08..aeb37af 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "license": "MIT", "dependencies": { "@anthropic-ai/vertex-sdk": "^0.14.4", - "@cdot65/prisma-airs-sdk": "^0.8.1", + "@cdot65/prisma-airs-sdk": "^0.8.3", "@inquirer/prompts": "^8.3.0", "@langchain/anthropic": "^1.3.25", "@langchain/aws": "^1.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2243b..10c9f83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.14.4 version: 0.14.4(zod@3.25.76) '@cdot65/prisma-airs-sdk': - specifier: ^0.8.1 - version: 0.8.1 + specifier: ^0.8.3 + version: 0.8.3 '@inquirer/prompts': specifier: ^8.3.0 version: 8.3.0(@types/node@22.19.13) @@ -313,8 +313,8 @@ packages: cpu: [x64] os: [win32] - '@cdot65/prisma-airs-sdk@0.8.1': - resolution: {integrity: sha512-14I2+6Vy1ARLTEripdFUVsfNvll6zu8CkIeru42UzeJQnysEHYFp7+W5j+4cU4YjUFFooxQoGqs1/GYfTEoXAQ==} + '@cdot65/prisma-airs-sdk@0.8.3': + resolution: {integrity: sha512-q6iNeaG/sdFBj7PguOk98TraS/YXfUmafFvYCIxhJ5fESt/Jjc1FACTZtCb6h0dJ+xdwRHLZRLSHv+4bk773jg==} engines: {node: '>=18'} '@cfworker/json-schema@4.1.1': @@ -2439,7 +2439,7 @@ snapshots: '@biomejs/cli-win32-x64@2.4.5': optional: true - '@cdot65/prisma-airs-sdk@0.8.1': + '@cdot65/prisma-airs-sdk@0.8.3': dependencies: zod: 3.25.76 diff --git a/src/airs/promptsets.ts b/src/airs/promptsets.ts index 995bd20..13da09a 100644 --- a/src/airs/promptsets.ts +++ b/src/airs/promptsets.ts @@ -104,26 +104,7 @@ export class SdkPromptSetService implements PromptSetService { } async downloadTemplate(uuid: string): Promise { - // WORKAROUND: Bypass SDK's downloadTemplate — it routes through managementHttpRequest - // which unconditionally JSON.parse()s the response, but this endpoint returns text/csv. - // Make a raw fetch using the SDK's OAuth client instead. - // Tracked upstream: https://github.com/cdot65/prisma-airs-sdk/issues/77 - const internals = this.client.customAttacks as unknown as { - baseUrl: string; - oauthClient: { getToken(): Promise }; - }; - const token = await internals.oauthClient.getToken(); - const base = internals.baseUrl.replace(/\/+$/, ''); - const url = `${base}/v1/custom-attack/download-template/${uuid}`; - const res = await fetch(url, { - method: 'GET', - headers: { Authorization: `Bearer ${token}` }, - }); - if (!res.ok) { - const body = await res.text(); - throw new Error(`Download template failed (${res.status}): ${body}`); - } - return res.text(); + return this.client.customAttacks.downloadTemplate(uuid); } async uploadPromptsCsv(uuid: string, file: Blob): Promise<{ message: string; status: number }> { diff --git a/tests/unit/airs/promptsets.spec.ts b/tests/unit/airs/promptsets.spec.ts index 369c30a..7d3e96f 100644 --- a/tests/unit/airs/promptsets.spec.ts +++ b/tests/unit/airs/promptsets.spec.ts @@ -22,8 +22,6 @@ const mockCreatePropertyValue = vi.fn(); vi.mock('@cdot65/prisma-airs-sdk', () => ({ RedTeamClient: vi.fn().mockImplementation(() => ({ customAttacks: { - baseUrl: 'https://api.example.com', - oauthClient: { getToken: vi.fn().mockResolvedValue('mock-token') }, createPromptSet: mockCreatePromptSet, createPrompt: mockCreatePrompt, listPromptSets: mockListPromptSets, @@ -261,36 +259,18 @@ describe('SdkPromptSetService', () => { }); describe('downloadTemplate', () => { - it('returns CSV template string via raw fetch', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve('prompt,goal\n"test","test goal"'), - }); - vi.stubGlobal('fetch', mockFetch); - + it('delegates to SDK and returns CSV string', async () => { + mockDownloadTemplate.mockResolvedValue('prompt,goal\n"test","test goal"'); const result = await service.downloadTemplate('ps-1'); expect(result).toBe('prompt,goal\n"test","test goal"'); - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining('/v1/custom-attack/download-template/ps-1'), - expect.objectContaining({ method: 'GET' }), - ); - - vi.unstubAllGlobals(); + expect(mockDownloadTemplate).toHaveBeenCalledWith('ps-1'); }); - it('throws on non-OK response', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: false, - status: 404, - text: () => Promise.resolve('Not found'), - }); - vi.stubGlobal('fetch', mockFetch); - + it('propagates SDK errors', async () => { + mockDownloadTemplate.mockRejectedValue(new Error('Download template failed (404)')); await expect(service.downloadTemplate('ps-1')).rejects.toThrow( 'Download template failed (404)', ); - - vi.unstubAllGlobals(); }); });