@signalvaultio/node 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@signalvaultio/node",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SignalVault SDK - AI audit logs and guardrails for safe production AI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist",
9
+ "!dist/__tests__",
9
10
  "README.md"
10
11
  ],
11
12
  "scripts": {
@@ -39,20 +40,20 @@
39
40
  },
40
41
  "homepage": "https://signalvault.io/docs",
41
42
  "peerDependencies": {
42
- "openai": "^4.0.0",
43
- "@anthropic-ai/sdk": ">=0.20.0"
43
+ "@anthropic-ai/sdk": ">=0.20.0",
44
+ "openai": "^4.0.0"
44
45
  },
45
46
  "peerDependenciesMeta": {
46
- "openai": { "optional": true },
47
- "@anthropic-ai/sdk": { "optional": true }
48
- },
49
- "dependencies": {
50
- "uuid": "^9.0.0"
47
+ "openai": {
48
+ "optional": true
49
+ },
50
+ "@anthropic-ai/sdk": {
51
+ "optional": true
52
+ }
51
53
  },
52
54
  "devDependencies": {
53
55
  "@types/jest": "^30.0.0",
54
56
  "@types/node": "^20.0.0",
55
- "@types/uuid": "^9.0.0",
56
57
  "jest": "^30.2.0",
57
58
  "openai": "^4.0.0",
58
59
  "ts-jest": "^29.4.6",
@@ -1 +0,0 @@
1
- export {};
@@ -1,99 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const index_1 = require("../index");
4
- describe('SignalVaultClient', () => {
5
- describe('construction', () => {
6
- it('constructs with openaiApiKey', () => {
7
- const client = new index_1.SignalVaultClient({ apiKey: 'sk_test_abc', openaiApiKey: 'sk-fake' });
8
- expect(client).toBeDefined();
9
- expect(client.chat).toBeDefined();
10
- expect(client.chat.completions).toBeDefined();
11
- expect(typeof client.chat.completions.create).toBe('function');
12
- });
13
- it('throws when neither openaiApiKey nor anthropicApiKey is provided', () => {
14
- expect(() => new index_1.SignalVaultClient({ apiKey: 'sk_test_abc' })).toThrow('At least one of openaiApiKey or anthropicApiKey is required');
15
- });
16
- it('defaults to localhost and production', () => {
17
- const client = new index_1.SignalVaultClient({ apiKey: 'sk_test_abc', openaiApiKey: 'sk-fake' });
18
- expect(client.svBaseUrl).toBe('http://localhost:4000');
19
- expect(client.svEnvironment).toBe('production');
20
- expect(client.debugMode).toBe(false);
21
- expect(client.mirrorMode).toBe(false);
22
- });
23
- it('accepts custom config', () => {
24
- const client = new index_1.SignalVaultClient({
25
- apiKey: 'sk_test_abc',
26
- openaiApiKey: 'sk-fake',
27
- baseUrl: 'https://api.signalvault.io/',
28
- environment: 'staging',
29
- debug: true,
30
- mirrorMode: true,
31
- });
32
- expect(client.svBaseUrl).toBe('https://api.signalvault.io');
33
- expect(client.svEnvironment).toBe('staging');
34
- expect(client.debugMode).toBe(true);
35
- expect(client.mirrorMode).toBe(true);
36
- });
37
- it('strips trailing slashes from baseUrl', () => {
38
- const client = new index_1.SignalVaultClient({
39
- apiKey: 'sk_test_abc',
40
- openaiApiKey: 'sk-fake',
41
- baseUrl: 'https://example.com///',
42
- });
43
- expect(client.svBaseUrl).toBe('https://example.com');
44
- });
45
- });
46
- describe('timeout configuration', () => {
47
- it('defaults to 2000ms preflight and 10000ms background', () => {
48
- const client = new index_1.SignalVaultClient({ apiKey: 'sk_test_abc', openaiApiKey: 'sk-fake' });
49
- expect(client.preflightTimeout).toBe(2000);
50
- expect(client.bgTimeout).toBe(10000);
51
- });
52
- it('accepts custom timeouts', () => {
53
- const client = new index_1.SignalVaultClient({
54
- apiKey: 'sk_test_abc',
55
- openaiApiKey: 'sk-fake',
56
- preflightTimeout: 500,
57
- timeout: 5000,
58
- });
59
- expect(client.preflightTimeout).toBe(500);
60
- expect(client.bgTimeout).toBe(5000);
61
- });
62
- });
63
- describe('metadata', () => {
64
- it('defaults to empty metadata', () => {
65
- const client = new index_1.SignalVaultClient({ apiKey: 'sk_test_abc', openaiApiKey: 'sk-fake' });
66
- expect(client.defaultMetadata).toEqual({});
67
- });
68
- it('accepts metadata in config', () => {
69
- const client = new index_1.SignalVaultClient({
70
- apiKey: 'sk_test_abc',
71
- openaiApiKey: 'sk-fake',
72
- metadata: { user_id: 'u_123', feature: 'chat' },
73
- });
74
- expect(client.defaultMetadata).toEqual({ user_id: 'u_123', feature: 'chat' });
75
- });
76
- });
77
- describe('Anthropic', () => {
78
- it('throws on messages access when anthropicApiKey not provided', () => {
79
- const client = new index_1.SignalVaultClient({ apiKey: 'sk_test_abc', openaiApiKey: 'sk-fake' });
80
- expect(() => client.messages).toThrow('anthropicApiKey not provided');
81
- });
82
- it('throws when @anthropic-ai/sdk is not installed', () => {
83
- expect(() => new index_1.SignalVaultClient({ apiKey: 'sk_test_abc', anthropicApiKey: 'sk-ant-fake' })).toThrow('@anthropic-ai/sdk is not installed');
84
- });
85
- });
86
- describe('fail-open on preflight timeout', () => {
87
- it('sendRequest returns allow decision on fetch error', async () => {
88
- const client = new index_1.SignalVaultClient({
89
- apiKey: 'sk_test_abc',
90
- openaiApiKey: 'sk-fake',
91
- baseUrl: 'http://127.0.0.1:1', // unreachable
92
- preflightTimeout: 100,
93
- });
94
- const decision = await client.sendRequest('req-1', 'gpt-4', [], {}, 'openai');
95
- expect(decision.decision).toBe('allow');
96
- expect(decision.violations).toEqual([]);
97
- });
98
- });
99
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,243 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const index_1 = require("../index");
4
- describe('Agent tool-use capture', () => {
5
- let fetchMock;
6
- let client;
7
- beforeEach(() => {
8
- fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify({ event_id: 'evt_123' }), { status: 200 }));
9
- client = new index_1.SignalVaultClient({
10
- apiKey: 'sk_test_abc',
11
- openaiApiKey: 'sk-fake',
12
- baseUrl: 'https://api.example.com',
13
- });
14
- });
15
- afterEach(() => {
16
- fetchMock.mockRestore();
17
- });
18
- // Helper: lets the fire-and-forget tool record promise settle.
19
- const flushAsync = () => new Promise((resolve) => setImmediate(resolve));
20
- describe('client.tool() wrapper', () => {
21
- it('records a successful tool invocation with input/output and timing', async () => {
22
- const fetchWeather = client.tool('fetch_weather', async (city) => {
23
- return { temp: 12.3, conditions: 'rain', for: city };
24
- });
25
- const result = await fetchWeather('London');
26
- expect(result).toEqual({ temp: 12.3, conditions: 'rain', for: 'London' });
27
- await flushAsync();
28
- expect(fetchMock).toHaveBeenCalledTimes(1);
29
- const [url, init] = fetchMock.mock.calls[0];
30
- expect(url).toBe('https://api.example.com/v1/events');
31
- const body = JSON.parse(init.body);
32
- expect(body.type).toBe('agent.tool_call');
33
- expect(body.payload.tool_name).toBe('fetch_weather');
34
- expect(body.payload.tool_input).toBe('London');
35
- expect(body.payload.tool_output).toEqual({ temp: 12.3, conditions: 'rain', for: 'London' });
36
- expect(body.payload.duration_ms).toBeGreaterThanOrEqual(0);
37
- expect(body.payload.started_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
38
- });
39
- it('records an errored invocation and rethrows', async () => {
40
- const failingTool = client.tool('fetch_external', async () => {
41
- throw new Error('connection refused');
42
- });
43
- await expect(failingTool()).rejects.toThrow('connection refused');
44
- await flushAsync();
45
- expect(fetchMock).toHaveBeenCalledTimes(1);
46
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
47
- expect(body.payload.error).toBe('connection refused');
48
- expect(body.payload.tool_output).toBeNull();
49
- });
50
- it('serializes a single object arg as the value, not as an array', async () => {
51
- const search = client.tool('search', async (query) => ({ matches: query.k }));
52
- await search({ q: 'embeddings', k: 5 });
53
- await flushAsync();
54
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
55
- expect(body.payload.tool_input).toEqual({ q: 'embeddings', k: 5 });
56
- });
57
- it('serializes multiple args as an array', async () => {
58
- const add = client.tool('add', async (a, b) => a + b);
59
- await add(2, 3);
60
- await flushAsync();
61
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
62
- expect(body.payload.tool_input).toEqual([2, 3]);
63
- });
64
- it('does not block on the audit network call (fire-and-forget)', async () => {
65
- // Make fetch hang so we can verify the wrapper returns before the audit completes
66
- fetchMock.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve(new Response('{}', { status: 200 })), 5000)));
67
- const start = Date.now();
68
- const tool = client.tool('fast_tool', async () => 42);
69
- const result = await tool();
70
- const elapsed = Date.now() - start;
71
- expect(result).toBe(42);
72
- expect(elapsed).toBeLessThan(100); // should NOT wait the 5s audit timeout
73
- });
74
- it('supports synchronous functions', async () => {
75
- const sum = client.tool('sum', (a, b) => a + b);
76
- const result = await sum(2, 3);
77
- expect(result).toBe(5);
78
- });
79
- });
80
- describe('client.tools.record() manual API', () => {
81
- it('posts a tool_call event with the given fields', async () => {
82
- await client.tools.record({
83
- toolName: 'manual_tool',
84
- toolInput: { x: 1 },
85
- toolOutput: { y: 2 },
86
- durationMs: 50,
87
- requestId: 'req-explicit',
88
- });
89
- expect(fetchMock).toHaveBeenCalledTimes(1);
90
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
91
- expect(body.type).toBe('agent.tool_call');
92
- expect(body.request_id).toBe('req-explicit');
93
- expect(body.payload.tool_name).toBe('manual_tool');
94
- expect(body.payload.duration_ms).toBe(50);
95
- });
96
- it('omits request_id when not provided (orphan tool call)', async () => {
97
- await client.tools.record({
98
- toolName: 'orphan',
99
- durationMs: 10,
100
- });
101
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
102
- expect(body.request_id).toBeUndefined();
103
- });
104
- });
105
- describe('input sanitization', () => {
106
- it('replaces circular references with a marker instead of throwing', async () => {
107
- const circular = { name: 'root' };
108
- circular.self = circular;
109
- const tool = client.tool('circular_tool', async (_arg) => 'ok');
110
- await tool(circular);
111
- await flushAsync();
112
- expect(fetchMock).toHaveBeenCalledTimes(1);
113
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
114
- expect(body.payload.tool_input).toEqual({ name: 'root', self: '[Circular]' });
115
- });
116
- it('serializes BigInt arguments as a string with n suffix', async () => {
117
- const tool = client.tool('bigint_tool', async (_n) => 'ok');
118
- await tool(BigInt(42));
119
- await flushAsync();
120
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
121
- expect(body.payload.tool_input).toBe('42n');
122
- });
123
- it('truncates oversized tool_output with a marker', async () => {
124
- // ~300 KB — over the 256 KB cap.
125
- const big = 'x'.repeat(300 * 1024);
126
- const tool = client.tool('big_output', async () => ({ blob: big }));
127
- await tool();
128
- await flushAsync();
129
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
130
- expect(body.payload.tool_output._signalvault_truncated).toBe(true);
131
- expect(body.payload.tool_output._signalvault_original_bytes).toBeGreaterThan(256 * 1024);
132
- expect(typeof body.payload.tool_output.preview).toBe('string');
133
- });
134
- it('truncates a long error message to ~1900 bytes', async () => {
135
- const longMsg = 'a'.repeat(5000);
136
- const failing = client.tool('long_error', async () => {
137
- throw new Error(longMsg);
138
- });
139
- await expect(failing()).rejects.toThrow();
140
- await flushAsync();
141
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
142
- const err = body.payload.error;
143
- // Allow a generous upper bound to account for the truncation marker.
144
- expect(Buffer.byteLength(err, 'utf8')).toBeLessThanOrEqual(1900);
145
- expect(err).toMatch(/truncated/);
146
- });
147
- it('truncates an oversized tool_name and still records the call', async () => {
148
- const longName = 'n'.repeat(500);
149
- await client.tools.record({
150
- toolName: longName,
151
- durationMs: 1,
152
- });
153
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
154
- expect(Buffer.byteLength(body.payload.tool_name, 'utf8')).toBeLessThanOrEqual(200);
155
- expect(body.payload.tool_name).toMatch(/truncated/);
156
- });
157
- it('rejects an empty tool name from the manual API', async () => {
158
- await expect(client.tools.record({ toolName: '', durationMs: 1 })).rejects.toThrow(/non-empty string/);
159
- expect(fetchMock).not.toHaveBeenCalled();
160
- });
161
- it('replaces undefined tool_output with null in the body', async () => {
162
- const tool = client.tool('void_tool', async () => undefined);
163
- await tool();
164
- await flushAsync();
165
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
166
- expect(body.payload.tool_output).toBeNull();
167
- });
168
- it('replaces a function value with a [Function] marker (not a crash)', async () => {
169
- const tool = client.tool('weird_input', async (_fn) => 'ok');
170
- await tool(() => 'hello');
171
- await flushAsync();
172
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
173
- expect(body.payload.tool_input).toBe('[Function]');
174
- });
175
- it('falls back to the serialization-error envelope for top-level undefined', async () => {
176
- // A bare `undefined` is JSON-serializable as nothing — the replacer leaves
177
- // it alone and JSON.stringify returns undefined. We expose this as a
178
- // marker so the audit isn't silently dropped.
179
- await client.tools.record({
180
- toolName: 'undef',
181
- toolInput: undefined,
182
- durationMs: 1,
183
- });
184
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
185
- // sanitizePayload(undefined) returns null per the function contract.
186
- expect(body.payload.tool_input).toBeNull();
187
- });
188
- });
189
- describe('error logging', () => {
190
- let warnSpy;
191
- let errorSpy;
192
- beforeEach(() => {
193
- warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
194
- errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
195
- });
196
- afterEach(() => {
197
- warnSpy.mockRestore();
198
- errorSpy.mockRestore();
199
- });
200
- it('surfaces 4xx responses to console.warn even with debug=false', async () => {
201
- fetchMock.mockResolvedValue(new Response('bad request', { status: 422 }));
202
- await client.tools.record({ toolName: 'will_fail', durationMs: 1 });
203
- expect(warnSpy).toHaveBeenCalledTimes(1);
204
- expect(warnSpy.mock.calls[0][0]).toMatch(/422/);
205
- expect(warnSpy.mock.calls[0][0]).toMatch(/Check your apiKey/);
206
- });
207
- it('does NOT log 5xx responses unless debug is true', async () => {
208
- fetchMock.mockResolvedValue(new Response('boom', { status: 503 }));
209
- await client.tools.record({ toolName: 'svc_down', durationMs: 1 });
210
- expect(warnSpy).not.toHaveBeenCalled();
211
- expect(errorSpy).not.toHaveBeenCalled();
212
- });
213
- });
214
- describe('client.withContext() correlation', () => {
215
- it('auto-links wrapped tool calls to the surrounding requestId', async () => {
216
- const tool = client.tool('search', async (q) => ({ q }));
217
- await client.withContext({ requestId: 'turn-abc' }, async () => {
218
- await tool('embeddings');
219
- });
220
- await flushAsync();
221
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
222
- expect(body.request_id).toBe('turn-abc');
223
- });
224
- it('explicit requestId on record() overrides context', async () => {
225
- await client.withContext({ requestId: 'context-id' }, async () => {
226
- await client.tools.record({
227
- toolName: 'override',
228
- durationMs: 1,
229
- requestId: 'explicit-id',
230
- });
231
- });
232
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
233
- expect(body.request_id).toBe('explicit-id');
234
- });
235
- it('tools called outside any context are orphans', async () => {
236
- const tool = client.tool('orphan_tool', async () => 'ok');
237
- await tool();
238
- await flushAsync();
239
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
240
- expect(body.request_id).toBeUndefined();
241
- });
242
- });
243
- });