|
| 1 | +import { sanitizeFlowDataForPublicEndpoint } from '../../src/utils/sanitizeFlowData' |
| 2 | + |
| 3 | +const makeFlowData = (nodes: object[], edges: object[] = []) => JSON.stringify({ nodes, edges, viewport: { x: 0, y: 0, zoom: 1 } }) |
| 4 | + |
| 5 | +const makeNode = (inputs: Record<string, unknown>, inputParams: object[], extra: object = {}) => ({ |
| 6 | + id: 'node_0', |
| 7 | + position: { x: 0, y: 0 }, |
| 8 | + type: 'customNode', |
| 9 | + data: { |
| 10 | + id: 'node_0', |
| 11 | + name: 'testNode', |
| 12 | + label: 'Test Node', |
| 13 | + inputs, |
| 14 | + inputParams, |
| 15 | + ...extra |
| 16 | + } |
| 17 | +}) |
| 18 | + |
| 19 | +export function sanitizeFlowDataTest() { |
| 20 | + describe('sanitizeFlowDataForPublicEndpoint', () => { |
| 21 | + it('strips password-type inputs', () => { |
| 22 | + const flowData = makeFlowData([ |
| 23 | + makeNode({ apiKey: 'sk-secret', model: 'gpt-4' }, [ |
| 24 | + { name: 'apiKey', type: 'password' }, |
| 25 | + { name: 'model', type: 'string' } |
| 26 | + ]) |
| 27 | + ]) |
| 28 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 29 | + expect(result.nodes[0].data.inputs).not.toHaveProperty('apiKey') |
| 30 | + expect(result.nodes[0].data.inputs.model).toBe('gpt-4') |
| 31 | + }) |
| 32 | + |
| 33 | + it('strips file-type inputs', () => { |
| 34 | + const flowData = makeFlowData([ |
| 35 | + makeNode({ filePath: '/data/secret.pdf', label: 'loader' }, [ |
| 36 | + { name: 'filePath', type: 'file' }, |
| 37 | + { name: 'label', type: 'string' } |
| 38 | + ]) |
| 39 | + ]) |
| 40 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 41 | + expect(result.nodes[0].data.inputs).not.toHaveProperty('filePath') |
| 42 | + expect(result.nodes[0].data.inputs.label).toBe('loader') |
| 43 | + }) |
| 44 | + |
| 45 | + it('strips folder-type inputs', () => { |
| 46 | + const flowData = makeFlowData([ |
| 47 | + makeNode({ folderPath: '/home/user/docs', name: 'ingest' }, [ |
| 48 | + { name: 'folderPath', type: 'folder' }, |
| 49 | + { name: 'name', type: 'string' } |
| 50 | + ]) |
| 51 | + ]) |
| 52 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 53 | + expect(result.nodes[0].data.inputs).not.toHaveProperty('folderPath') |
| 54 | + expect(result.nodes[0].data.inputs.name).toBe('ingest') |
| 55 | + }) |
| 56 | + |
| 57 | + it('removes credential field from node data', () => { |
| 58 | + const flowData = makeFlowData([ |
| 59 | + makeNode({ model: 'gpt-4' }, [{ name: 'model', type: 'string' }], { credential: 'cred-uuid-123' }) |
| 60 | + ]) |
| 61 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 62 | + expect(result.nodes[0].data).not.toHaveProperty('credential') |
| 63 | + }) |
| 64 | + |
| 65 | + it('removes Authorization header from headers input, preserves other headers', () => { |
| 66 | + const headers = JSON.stringify({ Authorization: 'Bearer secret-token', 'Content-Type': 'application/json' }) |
| 67 | + const flowData = makeFlowData([ |
| 68 | + makeNode({ headers, url: 'https://example.com' }, [ |
| 69 | + { name: 'headers', type: 'json' }, |
| 70 | + { name: 'url', type: 'string' } |
| 71 | + ]) |
| 72 | + ]) |
| 73 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 74 | + const sanitizedHeaders = JSON.parse(result.nodes[0].data.inputs.headers) |
| 75 | + expect(sanitizedHeaders).not.toHaveProperty('Authorization') |
| 76 | + expect(sanitizedHeaders['Content-Type']).toBe('application/json') |
| 77 | + }) |
| 78 | + |
| 79 | + it('removes x-api-key header case-insensitively', () => { |
| 80 | + const headers = JSON.stringify({ 'X-API-Key': 'my-key', Accept: 'application/json' }) |
| 81 | + const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'json' }])]) |
| 82 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 83 | + const sanitizedHeaders = JSON.parse(result.nodes[0].data.inputs.headers) |
| 84 | + expect(sanitizedHeaders).not.toHaveProperty('X-API-Key') |
| 85 | + expect(sanitizedHeaders.Accept).toBe('application/json') |
| 86 | + }) |
| 87 | + |
| 88 | + it('removes Authorization from array-format headers (HTTP agentflow node)', () => { |
| 89 | + const headers = [ |
| 90 | + { key: 'Authorization', value: 'Bearer secret-token' }, |
| 91 | + { key: 'Content-Type', value: 'application/json' } |
| 92 | + ] |
| 93 | + const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'array' }])]) |
| 94 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 95 | + const sanitizedHeaders = result.nodes[0].data.inputs.headers |
| 96 | + expect(sanitizedHeaders).toEqual([{ key: 'Content-Type', value: 'application/json' }]) |
| 97 | + }) |
| 98 | + |
| 99 | + it('removes x-api-key case-insensitively from array-format headers', () => { |
| 100 | + const headers = [ |
| 101 | + { key: 'X-API-Key', value: 'secret' }, |
| 102 | + { key: 'Accept', value: 'application/json' } |
| 103 | + ] |
| 104 | + const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'array' }])]) |
| 105 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 106 | + const sanitizedHeaders = result.nodes[0].data.inputs.headers |
| 107 | + expect(sanitizedHeaders).toEqual([{ key: 'Accept', value: 'application/json' }]) |
| 108 | + }) |
| 109 | + |
| 110 | + it('preserves non-sensitive string inputs unchanged', () => { |
| 111 | + const flowData = makeFlowData([ |
| 112 | + makeNode({ temperature: '0.7', maxTokens: '1024' }, [ |
| 113 | + { name: 'temperature', type: 'string' }, |
| 114 | + { name: 'maxTokens', type: 'number' } |
| 115 | + ]) |
| 116 | + ]) |
| 117 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 118 | + expect(result.nodes[0].data.inputs.temperature).toBe('0.7') |
| 119 | + expect(result.nodes[0].data.inputs.maxTokens).toBe('1024') |
| 120 | + }) |
| 121 | + |
| 122 | + it('preserves startAgentflow node inputs used by the embed widget', () => { |
| 123 | + const formInputTypes = [{ name: 'email', type: 'string', label: 'Email' }] |
| 124 | + const flowData = makeFlowData([ |
| 125 | + makeNode( |
| 126 | + { startInputType: 'formInput', formTitle: 'Contact Us', formDescription: 'Fill out the form', formInputTypes }, |
| 127 | + [ |
| 128 | + { name: 'startInputType', type: 'options' }, |
| 129 | + { name: 'formTitle', type: 'string' }, |
| 130 | + { name: 'formDescription', type: 'string' }, |
| 131 | + { name: 'formInputTypes', type: 'datagrid' } |
| 132 | + ], |
| 133 | + { name: 'startAgentflow' } |
| 134 | + ) |
| 135 | + ]) |
| 136 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 137 | + const inputs = result.nodes[0].data.inputs |
| 138 | + expect(inputs.startInputType).toBe('formInput') |
| 139 | + expect(inputs.formTitle).toBe('Contact Us') |
| 140 | + expect(inputs.formDescription).toBe('Fill out the form') |
| 141 | + expect(inputs.formInputTypes).toEqual(formInputTypes) |
| 142 | + }) |
| 143 | + |
| 144 | + it('returns empty nodes/edges structure for malformed flowData', () => { |
| 145 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint('not-valid-json')) |
| 146 | + expect(result).toEqual({ nodes: [], edges: [] }) |
| 147 | + }) |
| 148 | + |
| 149 | + it('returns the original string unchanged when flowDataString is empty', () => { |
| 150 | + expect(sanitizeFlowDataForPublicEndpoint('')).toBe('') |
| 151 | + }) |
| 152 | + |
| 153 | + it('does not crash when a node has no inputParams', () => { |
| 154 | + const flowData = makeFlowData([{ id: 'n0', type: 'x', data: { inputs: { foo: 'bar' } } }]) |
| 155 | + expect(() => sanitizeFlowDataForPublicEndpoint(flowData)).not.toThrow() |
| 156 | + const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData)) |
| 157 | + expect(result.nodes[0].data.inputs.foo).toBe('bar') |
| 158 | + }) |
| 159 | + }) |
| 160 | +} |
0 commit comments