@signalvaultio/node 0.2.0 → 0.3.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/README.md +68 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +243 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +146 -0
- package/dist/tools.d.ts +69 -0
- package/dist/tools.js +207 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
AI audit logs and guardrails for your OpenAI and Anthropic applications.
|
|
4
4
|
|
|
5
|
+
> **Server-side only.** This SDK uses Node APIs (`async_hooks`, `fetch`) and
|
|
6
|
+
> requires Node.js **18+**. Do **not** bundle it for the browser — your
|
|
7
|
+
> `apiKey`, `openaiApiKey`, and `anthropicApiKey` would be exposed to end
|
|
8
|
+
> users. Always call SignalVault from your backend.
|
|
9
|
+
|
|
5
10
|
## Installation
|
|
6
11
|
|
|
7
12
|
```bash
|
|
@@ -86,6 +91,69 @@ for await (const event of stream) {
|
|
|
86
91
|
}
|
|
87
92
|
```
|
|
88
93
|
|
|
94
|
+
## Agent Tool-Use Capture
|
|
95
|
+
|
|
96
|
+
Log every tool invocation made by your agents — name, input, output, duration, and any error — as auditable events alongside your LLM calls.
|
|
97
|
+
|
|
98
|
+
### Wrapper API (recommended)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const fetchWeather = client.tool('fetch_weather', async (city: string) => {
|
|
102
|
+
const res = await fetch(`https://api.example.com/weather?city=${city}`);
|
|
103
|
+
return res.json();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Call it like the original — SignalVault auto-times and audits
|
|
107
|
+
const weather = await fetchWeather('London');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The wrapper records the call asynchronously (no impact on your tool's latency) and passes the result through unchanged. Errors are recorded and rethrown.
|
|
111
|
+
|
|
112
|
+
### Manual API
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
await client.tools.record({
|
|
116
|
+
toolName: 'fetch_weather',
|
|
117
|
+
toolInput: { city: 'London' },
|
|
118
|
+
toolOutput: { temp: 12.3 },
|
|
119
|
+
durationMs: 142,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### What gets captured (and what to keep out)
|
|
124
|
+
|
|
125
|
+
When you wrap a tool or call `tools.record()`, SignalVault captures:
|
|
126
|
+
|
|
127
|
+
- `tool_name` (truncated to 200 bytes)
|
|
128
|
+
- `tool_input` — the function arguments, JSON-serialized (capped at 256 KB; oversize values are truncated with a marker)
|
|
129
|
+
- `tool_output` — the function return value, JSON-serialized (same 256 KB cap)
|
|
130
|
+
- `error.message` if the tool throws (truncated to 1900 bytes)
|
|
131
|
+
- `duration_ms`, `started_at`, and any `metadata` you attach
|
|
132
|
+
|
|
133
|
+
These fields are stored encrypted at rest server-side, but they go on the wire
|
|
134
|
+
to SignalVault's API. **If you pass user PII, secrets, or API keys as tool
|
|
135
|
+
arguments, those values will leave your process and be stored in SignalVault.**
|
|
136
|
+
Recommendations:
|
|
137
|
+
|
|
138
|
+
- Sanitize sensitive arguments before invoking the wrapped tool, or use the
|
|
139
|
+
manual `tools.record()` API and pass a redacted copy.
|
|
140
|
+
- Don't put secrets in error messages — they end up in `error` verbatim.
|
|
141
|
+
- Use `metadata` for non-sensitive identifiers (`user_id`, `feature`,
|
|
142
|
+
`workspace_id`); avoid putting raw user content in metadata.
|
|
143
|
+
|
|
144
|
+
### Linking tool calls to a parent LLM turn
|
|
145
|
+
|
|
146
|
+
Wrap your agent loop in `withContext` and tool calls inside auto-correlate to the given `requestId`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
await client.withContext({ requestId: 'agent-turn-abc' }, async () => {
|
|
150
|
+
const llmResponse = await client.chat.completions.create({...});
|
|
151
|
+
await fetchWeather('London'); // auto-linked to 'agent-turn-abc'
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Without `withContext`, tool calls are recorded as orphans (no parent request).
|
|
156
|
+
|
|
89
157
|
## Metadata
|
|
90
158
|
|
|
91
159
|
Attach contextual metadata to every event — perfect for audit trails, user attribution, and analytics:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
+
import { ToolsAPI } from './tools';
|
|
3
|
+
export type { ToolRecordOptions, ToolsAPI } from './tools';
|
|
2
4
|
export interface SignalVaultConfig {
|
|
3
5
|
apiKey: string;
|
|
4
6
|
/** Required for OpenAI (chat.completions). */
|
|
@@ -81,6 +83,60 @@ export declare class SignalVaultClient {
|
|
|
81
83
|
private readonly openai;
|
|
82
84
|
private readonly anthropic;
|
|
83
85
|
constructor(config: SignalVaultConfig);
|
|
86
|
+
/**
|
|
87
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
88
|
+
* timing, input, output, and any error. The wrapper auto-times, runs the
|
|
89
|
+
* inner function, fires the audit event in the background, and returns the
|
|
90
|
+
* inner result (or rethrows the inner error).
|
|
91
|
+
*
|
|
92
|
+
* Tool calls invoked inside a `withContext({ requestId })` block auto-pick
|
|
93
|
+
* up the requestId for correlation with the parent ai.request.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const fetchWeather = signalvault.tool('fetch_weather', async (city: string) => {
|
|
98
|
+
* const res = await fetch(`https://api.example.com/weather?city=${city}`);
|
|
99
|
+
* return res.json();
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* const weather = await fetchWeather('London');
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
tool<TArgs extends readonly unknown[], TReturn>(name: string, fn: (...args: TArgs) => TReturn | Promise<TReturn>, options?: {
|
|
106
|
+
metadata?: Record<string, unknown>;
|
|
107
|
+
}): (...args: TArgs) => Promise<TReturn>;
|
|
108
|
+
/**
|
|
109
|
+
* Manual tool recording API. Use when the wrapper isn't a fit (streaming
|
|
110
|
+
* tools, custom timing, post-hoc capture from logs).
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* await signalvault.tools.record({
|
|
115
|
+
* toolName: 'fetch_weather',
|
|
116
|
+
* toolInput: { city: 'London' },
|
|
117
|
+
* toolOutput: { temp: 12.3 },
|
|
118
|
+
* durationMs: 142,
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
get tools(): ToolsAPI;
|
|
123
|
+
/**
|
|
124
|
+
* Runs the given async function inside a context where tool calls are
|
|
125
|
+
* automatically correlated to the provided `requestId`. Useful for grouping
|
|
126
|
+
* a multi-step agent loop under one logical turn.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* await signalvault.withContext({ requestId: 'agent-turn-abc' }, async () => {
|
|
131
|
+
* const llm = await signalvault.chat.completions.create({...});
|
|
132
|
+
* await fetchWeather('London'); // tool call auto-linked to 'agent-turn-abc'
|
|
133
|
+
* });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
withContext<T>(ctx: {
|
|
137
|
+
requestId?: string;
|
|
138
|
+
}, fn: () => Promise<T>): Promise<T>;
|
|
139
|
+
private sendToolCallEvent;
|
|
84
140
|
get chat(): {
|
|
85
141
|
completions: {
|
|
86
142
|
create(params: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming, options?: CreateOptions): Promise<OpenAI.Chat.ChatCompletion>;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.SignalVaultClient = void 0;
|
|
7
7
|
const openai_1 = __importDefault(require("openai"));
|
|
8
8
|
const uuid_1 = require("uuid");
|
|
9
|
+
const tools_1 = require("./tools");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Environment guard
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Hard-fails if the SDK is loaded in a browser context. The SDK relies on
|
|
15
|
+
* `async_hooks` (Node-only) and is intended for server-side use; bundling it
|
|
16
|
+
* for the browser would expose `apiKey`, `openaiApiKey`, and `anthropicApiKey`
|
|
17
|
+
* to end users — which is exactly the kind of leak this product is meant to
|
|
18
|
+
* prevent.
|
|
19
|
+
*/
|
|
20
|
+
function assertNodeEnvironment() {
|
|
21
|
+
// `process` exists on Node and not in plain browsers. `window` is the
|
|
22
|
+
// canonical browser global. We refuse to run if we look browser-like.
|
|
23
|
+
const hasProcess = typeof process !== 'undefined' && process?.versions?.node;
|
|
24
|
+
const hasWindow = typeof globalThis.window !== 'undefined';
|
|
25
|
+
if (!hasProcess || hasWindow) {
|
|
26
|
+
throw new Error('[SignalVault] This SDK is server-side only. Do not bundle it for browser use — ' +
|
|
27
|
+
'it will leak your API keys to end users. Call SignalVault from your backend instead.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
9
30
|
// ---------------------------------------------------------------------------
|
|
10
31
|
// SignalVaultClient
|
|
11
32
|
// ---------------------------------------------------------------------------
|
|
@@ -35,6 +56,7 @@ const uuid_1 = require("uuid");
|
|
|
35
56
|
*/
|
|
36
57
|
class SignalVaultClient {
|
|
37
58
|
constructor(config) {
|
|
59
|
+
assertNodeEnvironment();
|
|
38
60
|
if (!config.openaiApiKey && !config.anthropicApiKey) {
|
|
39
61
|
throw new Error('[SignalVault] At least one of openaiApiKey or anthropicApiKey is required.');
|
|
40
62
|
}
|
|
@@ -65,6 +87,130 @@ class SignalVaultClient {
|
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
// ---------------------------------------------------------------------------
|
|
90
|
+
// Agent tool-use capture
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
94
|
+
* timing, input, output, and any error. The wrapper auto-times, runs the
|
|
95
|
+
* inner function, fires the audit event in the background, and returns the
|
|
96
|
+
* inner result (or rethrows the inner error).
|
|
97
|
+
*
|
|
98
|
+
* Tool calls invoked inside a `withContext({ requestId })` block auto-pick
|
|
99
|
+
* up the requestId for correlation with the parent ai.request.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const fetchWeather = signalvault.tool('fetch_weather', async (city: string) => {
|
|
104
|
+
* const res = await fetch(`https://api.example.com/weather?city=${city}`);
|
|
105
|
+
* return res.json();
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* const weather = await fetchWeather('London');
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
tool(name, fn, options) {
|
|
112
|
+
return (0, tools_1.wrapTool)({
|
|
113
|
+
recordFn: (opts) => this.sendToolCallEvent(opts),
|
|
114
|
+
debug: this.debugMode,
|
|
115
|
+
}, name, fn, options);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Manual tool recording API. Use when the wrapper isn't a fit (streaming
|
|
119
|
+
* tools, custom timing, post-hoc capture from logs).
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* await signalvault.tools.record({
|
|
124
|
+
* toolName: 'fetch_weather',
|
|
125
|
+
* toolInput: { city: 'London' },
|
|
126
|
+
* toolOutput: { temp: 12.3 },
|
|
127
|
+
* durationMs: 142,
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
get tools() {
|
|
132
|
+
return {
|
|
133
|
+
record: (opts) => this.sendToolCallEvent(opts),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Runs the given async function inside a context where tool calls are
|
|
138
|
+
* automatically correlated to the provided `requestId`. Useful for grouping
|
|
139
|
+
* a multi-step agent loop under one logical turn.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* await signalvault.withContext({ requestId: 'agent-turn-abc' }, async () => {
|
|
144
|
+
* const llm = await signalvault.chat.completions.create({...});
|
|
145
|
+
* await fetchWeather('London'); // tool call auto-linked to 'agent-turn-abc'
|
|
146
|
+
* });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
withContext(ctx, fn) {
|
|
150
|
+
return (0, tools_1.runWithContext)(ctx, fn);
|
|
151
|
+
}
|
|
152
|
+
async sendToolCallEvent(opts) {
|
|
153
|
+
const metadata = { ...this.defaultMetadata, ...(opts.metadata ?? {}) };
|
|
154
|
+
// Validate + sanitize before constructing the body. validateToolName throws
|
|
155
|
+
// on missing/empty name; surfacing that to the caller is intentional —
|
|
156
|
+
// the manual API should refuse malformed input rather than silently fail.
|
|
157
|
+
const safeName = (0, tools_1.validateToolName)(opts.toolName);
|
|
158
|
+
const payload = {
|
|
159
|
+
tool_name: safeName,
|
|
160
|
+
tool_input: (0, tools_1.sanitizePayload)(opts.toolInput),
|
|
161
|
+
tool_output: (0, tools_1.sanitizePayload)(opts.toolOutput),
|
|
162
|
+
duration_ms: opts.durationMs,
|
|
163
|
+
};
|
|
164
|
+
const safeError = (0, tools_1.sanitizeError)(opts.error);
|
|
165
|
+
if (safeError !== undefined)
|
|
166
|
+
payload.error = safeError;
|
|
167
|
+
if (opts.startedAt)
|
|
168
|
+
payload.started_at = opts.startedAt;
|
|
169
|
+
const body = {
|
|
170
|
+
type: 'agent.tool_call',
|
|
171
|
+
environment: this.svEnvironment,
|
|
172
|
+
metadata,
|
|
173
|
+
payload,
|
|
174
|
+
};
|
|
175
|
+
if (opts.requestId)
|
|
176
|
+
body.request_id = opts.requestId;
|
|
177
|
+
let serialized;
|
|
178
|
+
try {
|
|
179
|
+
serialized = JSON.stringify(body);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// sanitizePayload should make this impossible, but guard the outer
|
|
183
|
+
// serialize too — we'd rather drop the audit than crash the app.
|
|
184
|
+
console.warn('[SignalVault] tool_call serialization failed, event dropped:', error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(`${this.svBaseUrl}/v1/events`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: this.authHeaders(),
|
|
191
|
+
signal: AbortSignal.timeout(this.bgTimeout),
|
|
192
|
+
body: serialized,
|
|
193
|
+
});
|
|
194
|
+
// Surface 4xx unconditionally — those are usually misconfigurations
|
|
195
|
+
// (bad key, malformed payload) the user must know about. 5xx are noisy
|
|
196
|
+
// and only logged in debug mode.
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
if (response.status >= 400 && response.status < 500) {
|
|
199
|
+
console.warn(`[SignalVault] tool_call rejected with ${response.status}. ` +
|
|
200
|
+
`Check your apiKey and event payload.`);
|
|
201
|
+
}
|
|
202
|
+
else if (this.debugMode) {
|
|
203
|
+
console.error('[SignalVault] tool_call event failed:', response.status);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (this.debugMode) {
|
|
209
|
+
console.error('[SignalVault] tool_call event error:', error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
68
214
|
// OpenAI: chat.completions
|
|
69
215
|
// ---------------------------------------------------------------------------
|
|
70
216
|
get chat() {
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
interface ToolContext {
|
|
2
|
+
requestId?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface ToolRecordOptions {
|
|
5
|
+
/** Logical name of the tool. Required. Truncated to 200 bytes. */
|
|
6
|
+
toolName: string;
|
|
7
|
+
/** Arguments passed to the tool. Any JSON-serializable value. */
|
|
8
|
+
toolInput?: unknown;
|
|
9
|
+
/** What the tool returned. Any JSON-serializable value. */
|
|
10
|
+
toolOutput?: unknown;
|
|
11
|
+
/** How long the tool took to run, in milliseconds. */
|
|
12
|
+
durationMs: number;
|
|
13
|
+
/** Error message if the tool threw. Truncated to 1900 bytes. */
|
|
14
|
+
error?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Optional ISO8601 timestamp of when the tool started.
|
|
17
|
+
* Falls back to server-side `inserted_at` when omitted.
|
|
18
|
+
*/
|
|
19
|
+
startedAt?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Parent ai.request correlation. If omitted, the SDK uses the requestId from
|
|
22
|
+
* the surrounding `withContext` block (if any), else null (orphan tool call).
|
|
23
|
+
*/
|
|
24
|
+
requestId?: string | null;
|
|
25
|
+
/** Per-call metadata, merged over client-level defaults. */
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
export interface ToolsAPI {
|
|
29
|
+
/**
|
|
30
|
+
* Records a tool call manually. Use when the wrapper isn't a fit
|
|
31
|
+
* (streaming tools, custom timing, post-hoc capture).
|
|
32
|
+
*/
|
|
33
|
+
record(opts: ToolRecordOptions): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Returns the requestId from the surrounding withContext block, or undefined. */
|
|
36
|
+
export declare function getCurrentRequestId(): string | undefined;
|
|
37
|
+
/** Runs `fn` inside an async context with the given requestId attached. */
|
|
38
|
+
export declare function runWithContext<T>(ctx: ToolContext, fn: () => Promise<T>): Promise<T>;
|
|
39
|
+
export interface ToolWrapperConfig {
|
|
40
|
+
recordFn: (opts: ToolRecordOptions) => Promise<void>;
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
45
|
+
* timing, args, result, and any error.
|
|
46
|
+
*
|
|
47
|
+
* The wrapper auto-times, JSON-serializes args/result, and captures the
|
|
48
|
+
* surrounding `withContext` requestId (if any). Errors propagate to the caller
|
|
49
|
+
* after recording — wrapping is observation, not error swallowing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function wrapTool<TArgs extends readonly unknown[], TReturn>(config: ToolWrapperConfig, toolName: string, fn: (...args: TArgs) => TReturn | Promise<TReturn>, options?: {
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}): (...args: TArgs) => Promise<TReturn>;
|
|
54
|
+
/** Validates and normalizes a tool name. Throws if missing or non-string. */
|
|
55
|
+
export declare function validateToolName(name: unknown): string;
|
|
56
|
+
/** Truncates an error string to the server-accepted byte limit. */
|
|
57
|
+
export declare function sanitizeError(err: unknown): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Returns a JSON-safe version of `value`, capped at MAX_PAYLOAD_BYTES.
|
|
60
|
+
*
|
|
61
|
+
* - Circular references, BigInt, functions, Symbols → replaced with a marker
|
|
62
|
+
* object so the audit event is still useful instead of dropped silently.
|
|
63
|
+
* - Oversized values → truncated; the marker `_signalvault_truncated` is set
|
|
64
|
+
* so the receiver can detect partial captures.
|
|
65
|
+
*
|
|
66
|
+
* Never throws — sanitization failures degrade gracefully.
|
|
67
|
+
*/
|
|
68
|
+
export declare function sanitizePayload(value: unknown): unknown;
|
|
69
|
+
export {};
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCurrentRequestId = getCurrentRequestId;
|
|
4
|
+
exports.runWithContext = runWithContext;
|
|
5
|
+
exports.wrapTool = wrapTool;
|
|
6
|
+
exports.validateToolName = validateToolName;
|
|
7
|
+
exports.sanitizeError = sanitizeError;
|
|
8
|
+
exports.sanitizePayload = sanitizePayload;
|
|
9
|
+
const async_hooks_1 = require("async_hooks");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Hard limit for serialized tool_input/tool_output JSON, in UTF-8 bytes. */
|
|
14
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024; // 256 KB
|
|
15
|
+
/** Hard limit for tool error string, in UTF-8 bytes. */
|
|
16
|
+
const MAX_ERROR_BYTES = 1900;
|
|
17
|
+
/** Hard limit for tool_name, in UTF-8 bytes. */
|
|
18
|
+
const MAX_TOOL_NAME_BYTES = 200;
|
|
19
|
+
const toolContext = new async_hooks_1.AsyncLocalStorage();
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Internal: context-aware request_id resolution
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** Returns the requestId from the surrounding withContext block, or undefined. */
|
|
24
|
+
function getCurrentRequestId() {
|
|
25
|
+
return toolContext.getStore()?.requestId;
|
|
26
|
+
}
|
|
27
|
+
/** Runs `fn` inside an async context with the given requestId attached. */
|
|
28
|
+
function runWithContext(ctx, fn) {
|
|
29
|
+
return toolContext.run(ctx, fn);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
33
|
+
* timing, args, result, and any error.
|
|
34
|
+
*
|
|
35
|
+
* The wrapper auto-times, JSON-serializes args/result, and captures the
|
|
36
|
+
* surrounding `withContext` requestId (if any). Errors propagate to the caller
|
|
37
|
+
* after recording — wrapping is observation, not error swallowing.
|
|
38
|
+
*/
|
|
39
|
+
function wrapTool(config, toolName, fn, options) {
|
|
40
|
+
return async (...args) => {
|
|
41
|
+
const startedAt = new Date().toISOString();
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
let result;
|
|
44
|
+
let error = null;
|
|
45
|
+
try {
|
|
46
|
+
result = await Promise.resolve(fn(...args));
|
|
47
|
+
const durationMs = Date.now() - start;
|
|
48
|
+
// Fire-and-forget recording so the wrapped function's latency
|
|
49
|
+
// isn't affected by SignalVault's network call.
|
|
50
|
+
void config
|
|
51
|
+
.recordFn({
|
|
52
|
+
toolName,
|
|
53
|
+
toolInput: serializeArgs(args),
|
|
54
|
+
toolOutput: result,
|
|
55
|
+
durationMs,
|
|
56
|
+
startedAt,
|
|
57
|
+
requestId: getCurrentRequestId() ?? null,
|
|
58
|
+
metadata: options?.metadata,
|
|
59
|
+
})
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
if (config.debug)
|
|
62
|
+
console.error('[SignalVault] tool record failed:', err);
|
|
63
|
+
});
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
const durationMs = Date.now() - start;
|
|
68
|
+
error = e instanceof Error ? e.message : String(e);
|
|
69
|
+
void config
|
|
70
|
+
.recordFn({
|
|
71
|
+
toolName,
|
|
72
|
+
toolInput: serializeArgs(args),
|
|
73
|
+
toolOutput: null,
|
|
74
|
+
durationMs,
|
|
75
|
+
error,
|
|
76
|
+
startedAt,
|
|
77
|
+
requestId: getCurrentRequestId() ?? null,
|
|
78
|
+
metadata: options?.metadata,
|
|
79
|
+
})
|
|
80
|
+
.catch((err) => {
|
|
81
|
+
if (config.debug)
|
|
82
|
+
console.error('[SignalVault] tool record failed:', err);
|
|
83
|
+
});
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Sanitization helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* Returns the byte length of a string when encoded as UTF-8.
|
|
93
|
+
* Used to enforce server-side byte limits accurately on the client.
|
|
94
|
+
*/
|
|
95
|
+
function byteLength(s) {
|
|
96
|
+
return Buffer.byteLength(s, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Truncates a string to `max` UTF-8 bytes, preserving multi-byte boundaries.
|
|
100
|
+
* Appends a "(…N bytes truncated)" marker so the receiver knows truncation
|
|
101
|
+
* happened rather than seeing a silently shortened string.
|
|
102
|
+
*/
|
|
103
|
+
function truncateString(s, max) {
|
|
104
|
+
if (byteLength(s) <= max)
|
|
105
|
+
return s;
|
|
106
|
+
const marker = (n) => ` …(truncated ${n} bytes)`;
|
|
107
|
+
// Reserve room for the marker — sized to fit the worst-case marker.
|
|
108
|
+
const reserve = byteLength(marker(99999999));
|
|
109
|
+
const buf = Buffer.from(s, 'utf8');
|
|
110
|
+
const truncatedBytes = buf.byteLength - (max - reserve);
|
|
111
|
+
const sliced = buf.subarray(0, max - reserve).toString('utf8');
|
|
112
|
+
// Cleanup: drop a trailing replacement char if we cut a multi-byte sequence.
|
|
113
|
+
const cleaned = sliced.endsWith('�') ? sliced.slice(0, -1) : sliced;
|
|
114
|
+
return cleaned + marker(truncatedBytes);
|
|
115
|
+
}
|
|
116
|
+
/** Validates and normalizes a tool name. Throws if missing or non-string. */
|
|
117
|
+
function validateToolName(name) {
|
|
118
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
119
|
+
throw new Error('[SignalVault] toolName must be a non-empty string');
|
|
120
|
+
}
|
|
121
|
+
return byteLength(name) > MAX_TOOL_NAME_BYTES
|
|
122
|
+
? truncateString(name, MAX_TOOL_NAME_BYTES)
|
|
123
|
+
: name;
|
|
124
|
+
}
|
|
125
|
+
/** Truncates an error string to the server-accepted byte limit. */
|
|
126
|
+
function sanitizeError(err) {
|
|
127
|
+
if (err === null || err === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const s = typeof err === 'string' ? err : String(err);
|
|
130
|
+
return truncateString(s, MAX_ERROR_BYTES);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns a JSON-safe version of `value`, capped at MAX_PAYLOAD_BYTES.
|
|
134
|
+
*
|
|
135
|
+
* - Circular references, BigInt, functions, Symbols → replaced with a marker
|
|
136
|
+
* object so the audit event is still useful instead of dropped silently.
|
|
137
|
+
* - Oversized values → truncated; the marker `_signalvault_truncated` is set
|
|
138
|
+
* so the receiver can detect partial captures.
|
|
139
|
+
*
|
|
140
|
+
* Never throws — sanitization failures degrade gracefully.
|
|
141
|
+
*/
|
|
142
|
+
function sanitizePayload(value) {
|
|
143
|
+
if (value === undefined)
|
|
144
|
+
return null;
|
|
145
|
+
let serialized;
|
|
146
|
+
try {
|
|
147
|
+
serialized = JSON.stringify(value, replacer());
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
return {
|
|
151
|
+
_signalvault_serialization_error: e instanceof Error ? e.message : String(e),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// JSON.stringify returns undefined for top-level functions/Symbols/undefined.
|
|
155
|
+
if (serialized === undefined) {
|
|
156
|
+
return { _signalvault_serialization_error: 'value is not JSON-serializable' };
|
|
157
|
+
}
|
|
158
|
+
if (byteLength(serialized) <= MAX_PAYLOAD_BYTES) {
|
|
159
|
+
// Re-parse so the body is sent as a structured value, not a JSON string.
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(serialized);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return { _signalvault_serialization_error: 're-parse failed' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Oversized: include the original byte size so debugging is possible.
|
|
168
|
+
return {
|
|
169
|
+
_signalvault_truncated: true,
|
|
170
|
+
_signalvault_original_bytes: byteLength(serialized),
|
|
171
|
+
_signalvault_max_bytes: MAX_PAYLOAD_BYTES,
|
|
172
|
+
preview: truncateString(serialized, MAX_PAYLOAD_BYTES - 200),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* JSON.stringify replacer that handles circular refs, BigInts, and other
|
|
177
|
+
* non-serializable values by substituting markers instead of throwing.
|
|
178
|
+
*/
|
|
179
|
+
function replacer() {
|
|
180
|
+
const seen = new WeakSet();
|
|
181
|
+
return function (_key, value) {
|
|
182
|
+
if (typeof value === 'bigint')
|
|
183
|
+
return `${value.toString()}n`;
|
|
184
|
+
if (typeof value === 'function')
|
|
185
|
+
return '[Function]';
|
|
186
|
+
if (typeof value === 'symbol')
|
|
187
|
+
return value.toString();
|
|
188
|
+
if (value !== null && typeof value === 'object') {
|
|
189
|
+
if (seen.has(value))
|
|
190
|
+
return '[Circular]';
|
|
191
|
+
seen.add(value);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* If the wrapped function takes a single argument, store that value directly
|
|
198
|
+
* (the common case). Otherwise store the whole args array. This avoids storing
|
|
199
|
+
* `[{...}]` when callers pass a single object — better for dashboard rendering.
|
|
200
|
+
*/
|
|
201
|
+
function serializeArgs(args) {
|
|
202
|
+
if (args.length === 0)
|
|
203
|
+
return null;
|
|
204
|
+
if (args.length === 1)
|
|
205
|
+
return args[0];
|
|
206
|
+
return args;
|
|
207
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalvaultio/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"author": "SignalVault <support@signalvault.io>",
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
30
33
|
"repository": {
|
|
31
34
|
"type": "git",
|
|
32
35
|
"url": "git+https://github.com/signalvaultio/signalvault-node.git"
|