From ca4e9645f9e80793f62c5e016234fbdd973c0230 Mon Sep 17 00:00:00 2001 From: scthornton Date: Tue, 12 May 2026 09:02:05 -0400 Subject: [PATCH 1/2] fix: download template crash (getToken undefined) The downloadTemplate workaround accesses SDK internals (customAttacks.oauthClient.getToken) that no longer exist in the current SDK version. Replace with a direct OAuth token fetch using the client credentials from the constructor options. --- src/airs/promptsets.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/airs/promptsets.ts b/src/airs/promptsets.ts index 995bd20..e9f6720 100644 --- a/src/airs/promptsets.ts +++ b/src/airs/promptsets.ts @@ -38,9 +38,11 @@ function normalizePrompt(raw: Record): PromptDetail { */ export class SdkPromptSetService implements PromptSetService { private client: RedTeamClient; + private opts?: RedTeamClientOptions; constructor(opts?: RedTeamClientOptions) { this.client = new RedTeamClient(opts); + this.opts = opts; } async createPromptSet( @@ -106,14 +108,26 @@ 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. + // Fetch a fresh OAuth token and make a raw request 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 tokenEndpoint = + this.opts?.tokenEndpoint ?? 'https://auth.apps.paloaltonetworks.com/oauth2/access_token'; + const tokenRes = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: this.opts?.clientId ?? '', + client_secret: this.opts?.clientSecret ?? '', + scope: `tsg_id:${this.opts?.tsgId ?? ''}`, + }), + }); + if (!tokenRes.ok) { + throw new Error(`OAuth token request failed (${tokenRes.status})`); + } + const { access_token: token } = (await tokenRes.json()) as { access_token: string }; + + const base = 'https://api.sase.paloaltonetworks.com/aisec'; const url = `${base}/v1/custom-attack/download-template/${uuid}`; const res = await fetch(url, { method: 'GET', From 260a686f66afce082cf4852f049ef38c251e4f51 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Wed, 13 May 2026 13:47:42 -0500 Subject: [PATCH 2/2] fix: bump SDK to 0.8.3, delete downloadTemplate workaround SDK 0.8.3 fixes downloadTemplate to return CSV string directly. Drop the 30-line OAuth+raw-fetch workaround, delegate straight to the SDK. Update test to mock the SDK call instead of the removed internals. --- .changeset/0017-fix-download-template.md | 5 ++++ package.json | 2 +- pnpm-lock.yaml | 10 +++---- src/airs/promptsets.ts | 35 +----------------------- tests/unit/airs/promptsets.spec.ts | 30 ++++---------------- 5 files changed, 17 insertions(+), 65 deletions(-) create mode 100644 .changeset/0017-fix-download-template.md 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 e9f6720..13da09a 100644 --- a/src/airs/promptsets.ts +++ b/src/airs/promptsets.ts @@ -38,11 +38,9 @@ function normalizePrompt(raw: Record): PromptDetail { */ export class SdkPromptSetService implements PromptSetService { private client: RedTeamClient; - private opts?: RedTeamClientOptions; constructor(opts?: RedTeamClientOptions) { this.client = new RedTeamClient(opts); - this.opts = opts; } async createPromptSet( @@ -106,38 +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. - // Fetch a fresh OAuth token and make a raw request instead. - // Tracked upstream: https://github.com/cdot65/prisma-airs-sdk/issues/77 - const tokenEndpoint = - this.opts?.tokenEndpoint ?? 'https://auth.apps.paloaltonetworks.com/oauth2/access_token'; - const tokenRes = await fetch(tokenEndpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: this.opts?.clientId ?? '', - client_secret: this.opts?.clientSecret ?? '', - scope: `tsg_id:${this.opts?.tsgId ?? ''}`, - }), - }); - if (!tokenRes.ok) { - throw new Error(`OAuth token request failed (${tokenRes.status})`); - } - const { access_token: token } = (await tokenRes.json()) as { access_token: string }; - - const base = 'https://api.sase.paloaltonetworks.com/aisec'; - 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(); }); });