@signalvaultio/node 0.3.0 → 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SignalVault
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -20,6 +20,18 @@ npm install @signalvaultio/node @anthropic-ai/sdk
20
20
  npm install @signalvaultio/node openai @anthropic-ai/sdk
21
21
  ```
22
22
 
23
+ Supported provider SDKs: `openai` 4.4 through 7.x (openai 7 needs Node.js 22+) and `@anthropic-ai/sdk` 0.20 or later. Install only the one you use; the other is optional. TypeScript projects without `openai` need `skipLibCheck: true` (the default in most setups), because the SDK's type declarations refer to openai's types.
24
+
25
+ The package works with both `import` and `require()`:
26
+
27
+ ```typescript
28
+ import SignalVaultClient from '@signalvaultio/node'; // ESM, TypeScript
29
+ import { SignalVaultClient } from '@signalvaultio/node'; // also works
30
+ const { SignalVaultClient } = require('@signalvaultio/node'); // CommonJS
31
+ ```
32
+
33
+ Server-side bundlers (webpack, esbuild) are supported. esbuild's ESM output needs its usual `createRequire` banner for CommonJS dependencies: `--banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);"`.
34
+
23
35
  ## Quick Start — OpenAI
24
36
 
25
37
  ```typescript
@@ -28,8 +40,6 @@ import SignalVaultClient from '@signalvaultio/node';
28
40
  const client = new SignalVaultClient({
29
41
  apiKey: 'sk_live_your_signalvault_key',
30
42
  openaiApiKey: process.env.OPENAI_API_KEY!,
31
- baseUrl: 'https://api.signalvault.io',
32
- environment: 'production',
33
43
  });
34
44
 
35
45
  // Use exactly like OpenAI SDK
@@ -62,7 +72,7 @@ console.log(response.content[0].text);
62
72
 
63
73
  ## Streaming
64
74
 
65
- Streaming is fully supported for both providers. SignalVault logs the complete response once the stream finishes:
75
+ Streaming is fully supported for both providers. SignalVault logs the response when the stream ends — including a partial response if you stop iterating early or the stream fails. A stream you never iterate is not logged.
66
76
 
67
77
  ```typescript
68
78
  // OpenAI streaming
@@ -91,6 +101,8 @@ for await (const event of stream) {
91
101
  }
92
102
  ```
93
103
 
104
+ For OpenAI streams, the SDK sets `stream_options: { include_usage: true }` so the response is logged with its token counts. OpenAI then sends one extra final chunk that carries only the usage and no choices; the SDK consumes that chunk rather than passing it to you, so the stream you receive looks the same as without usage reporting. If you set `stream_options.include_usage` yourself, your setting is kept: with `true` you receive the usage chunk as usual, with `false` the response is logged without token counts. If your OpenAI-compatible server rejects `stream_options`, pass `stream_options: null` and the SDK sends none.
105
+
94
106
  ## Agent Tool-Use Capture
95
107
 
96
108
  Log every tool invocation made by your agents — name, input, output, duration, and any error — as auditable events alongside your LLM calls.
@@ -130,14 +142,15 @@ When you wrap a tool or call `tools.record()`, SignalVault captures:
130
142
  - `error.message` if the tool throws (truncated to 1900 bytes)
131
143
  - `duration_ms`, `started_at`, and any `metadata` you attach
132
144
 
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:
145
+ `tool_input` and `tool_output` are encrypted at rest server-side. `tool_name`,
146
+ `error`, `started_at` and `metadata` are stored as sent, **not encrypted**. All of
147
+ it goes on the wire to SignalVault's API: **if you pass user PII, secrets, or API
148
+ keys as tool arguments, those values will leave your process and be stored in
149
+ SignalVault.** Recommendations:
137
150
 
138
151
  - Sanitize sensitive arguments before invoking the wrapped tool, or use the
139
152
  manual `tools.record()` API and pass a redacted copy.
140
- - Don't put secrets in error messages — they end up in `error` verbatim.
153
+ - Don't put secrets in error messages — they end up in `error` verbatim and unencrypted.
141
154
  - Use `metadata` for non-sensitive identifiers (`user_id`, `feature`,
142
155
  `workspace_id`); avoid putting raw user content in metadata.
143
156
 
@@ -173,19 +186,37 @@ const response = await client.chat.completions.create(
173
186
  );
174
187
  ```
175
188
 
176
- ## Timeout Configuration
189
+ ## When SignalVault Is Unavailable
190
+
191
+ The pre-flight guardrail check is in your request's critical path. If it cannot return a decision — timeout, network error, invalid or revoked API key (401), inactive subscription (402), access denied (403), rate or trial limit (429), server error, or an invalid response — `failMode` decides what happens:
177
192
 
178
- The pre-flight guardrail check is in your request's critical path. SignalVaultClient uses a short timeout and **fails open** — your request always goes through even if the SignalVault API is slow or unreachable:
193
+ - `'open'` (default): the request goes to the provider **without** guardrails, and the SDK prints a warning (at most once a minute per cause, whether or not `debug` is on). If SignalVault was unreachable, timed out or errored, the request is still recorded in the background, marked `preflight_unavailable`, so it appears in your audit log.
194
+ - `'closed'`: the SDK throws `SignalVaultUnavailableError` and the provider is never called. Use this if every request must be checked.
195
+
196
+ `failMode` applies to the pre-flight check, so it has no effect in mirror mode.
197
+
198
+ **Rate limit.** The ingest API allows 120 events per minute per app, and each call sends two (request and response), plus one per tool call. Above roughly 60 LLM calls a minute, pre-flight checks are rate limited: with `failMode: 'open'` those requests go through unchecked. If you run at that volume and need enforcement, use `failMode: 'closed'`.
179
199
 
180
200
  ```typescript
181
201
  const client = new SignalVaultClient({
182
202
  apiKey: 'sk_live_...',
183
203
  openaiApiKey: process.env.OPENAI_API_KEY!,
184
- preflightTimeout: 2000, // ms — pre-flight check timeout (fails open). Default: 2000
204
+ failMode: 'closed', // 'open' | 'closed'. Default: 'open'
205
+ preflightTimeout: 2000, // ms — pre-flight check timeout. Default: 2000
185
206
  timeout: 10000, // ms — background/post-flight calls. Default: 10000
186
207
  });
187
208
  ```
188
209
 
210
+ Audit events (responses, mirror-mode events, tool calls) are sent in the background. Each carries an `event_id`, so a retry is never double-counted. The SDK retries once on connection errors and 5xx (not on timeouts). A rate-limited (429) event is retried only if `Retry-After` is 5 seconds or less; the SignalVault API currently asks for 60, so in practice those events are dropped, with a warning. `tools.record()` does not retry.
211
+
212
+ ## Shutting Down
213
+
214
+ Background events are queued in memory. Before a short-lived process exits (a script, a serverless handler), wait for them:
215
+
216
+ ```typescript
217
+ await client.flush(); // or client.close(); both wait up to 5s by default
218
+ ```
219
+
189
220
  ## Mirror Mode
190
221
 
191
222
  In mirror mode, requests go directly to the AI provider first and SignalVault audits them asynchronously — no latency added, never blocks:
@@ -201,7 +232,8 @@ const client = new SignalVaultClient({
201
232
  ## Features
202
233
 
203
234
  - **Automatic Logging** — Every request and response is recorded in your SignalVault dashboard
204
- - **Pre-flight Guardrails** — Block or redact requests before they reach the AI provider
235
+ - **Pre-flight Guardrails** — Block requests that break your rules before they reach the AI provider
236
+ - **Redaction at rest** — Rules with the `redact` action remove matches from what SignalVault stores. The request sent to the AI provider is **not** modified.
205
237
  - **PII Detection** — Automatically detect emails, phone numbers, SSNs in prompts
206
238
  - **Secret Detection** — Block API keys and tokens in prompts
207
239
  - **Token Limits** — Enforce cost controls per request
@@ -218,27 +250,48 @@ const client = new SignalVaultClient({
218
250
  apiKey: 'sk_live_...', // Your SignalVault API key (required)
219
251
  openaiApiKey: 'sk-...', // OpenAI API key (required for chat.completions)
220
252
  anthropicApiKey: 'sk-ant-...', // Anthropic API key (required for messages)
221
- baseUrl: 'https://api.signalvault.io',
222
- environment: 'production', // 'development' | 'staging' | 'production'
253
+ baseUrl: 'https://api.signalvault.io', // default; plain http:// only allowed for localhost
254
+ environment: 'production', // sent with events; the server uses the API key's environment
223
255
  debug: false,
224
256
  mirrorMode: false,
257
+ failMode: 'open', // 'open' | 'closed' — see "When SignalVault Is Unavailable"
225
258
  preflightTimeout: 2000, // ms
226
259
  timeout: 10000, // ms
227
260
  metadata: {}, // Default metadata for all events
228
261
  });
229
262
  ```
230
263
 
264
+ `environment` does not choose which rules apply: SignalVault uses the environment of the API key you pass as `apiKey`. Use a key created for the environment you want.
265
+
231
266
  ## Error Handling
232
267
 
268
+ When a guardrail rule blocks a request, `create()` throws `SignalVaultBlockedError` and the provider is never called. It carries:
269
+
270
+ - `violations` — the rules that matched, each with `rule_id`, `type` (for example `contains_secret`), `severity`, `action` and `details`
271
+ - `requestId` — the ID the SDK sent to SignalVault as `request_id` for this request
272
+ - `dashboardUrl` — a link to the blocked request, when the API provides one
273
+
274
+ The message lists only the violation types, never the matched content.
275
+
276
+ `SignalVaultUnavailableError` (with `requestId` and the HTTP `status`, if any) is thrown only with `failMode: 'closed'`, when no decision could be obtained.
277
+
233
278
  ```typescript
279
+ import { SignalVaultBlockedError, SignalVaultUnavailableError } from '@signalvaultio/node';
280
+
234
281
  try {
235
282
  const response = await client.chat.completions.create({
236
283
  model: 'gpt-4',
237
284
  messages: [{ role: 'user', content: 'my SSN is 123-45-6789' }],
238
285
  });
239
286
  } catch (error) {
240
- if (error.message.includes('[SignalVault]')) {
241
- console.log('Request blocked by guardrail');
287
+ if (error instanceof SignalVaultBlockedError) {
288
+ console.log('Blocked by guardrails:', error.violations.map((v) => v.type));
289
+ console.log('Details:', error.dashboardUrl);
290
+ } else if (error instanceof SignalVaultUnavailableError) {
291
+ // Only thrown with failMode: 'closed'
292
+ console.log('Guardrail check unavailable:', error.status);
293
+ } else {
294
+ throw error;
242
295
  }
243
296
  }
244
297
  ```
@@ -0,0 +1,4 @@
1
+ // Types for the ESM entry point (see index.mjs).
2
+ export * from './index.js';
3
+ import { SignalVaultClient } from './index.js';
4
+ export default SignalVaultClient;
package/dist/index.d.ts CHANGED
@@ -1,20 +1,43 @@
1
- import OpenAI from 'openai';
1
+ import type OpenAI from 'openai';
2
2
  import { ToolsAPI } from './tools';
3
3
  export type { ToolRecordOptions, ToolsAPI } from './tools';
4
+ /** Kept in sync with package.json by a unit test. */
5
+ export declare const SDK_VERSION = "0.5.0";
6
+ /**
7
+ * Normalizes the base URL and refuses plaintext HTTP to anything but the local
8
+ * machine. The API key and full prompts travel in every request, so an
9
+ * `http://` URL would expose both before the server could redirect to HTTPS.
10
+ */
11
+ export declare function normalizeBaseUrl(raw: string): string;
12
+ /**
13
+ * What to do when SignalVault cannot return a guardrail decision (unreachable,
14
+ * timed out, rate limited, invalid key, inactive subscription, ...).
15
+ *
16
+ * - `'open'` (default): allow the request and print a warning.
17
+ * - `'closed'`: throw {@link SignalVaultUnavailableError} instead of calling
18
+ * the provider unchecked.
19
+ */
20
+ export type FailMode = 'open' | 'closed';
4
21
  export interface SignalVaultConfig {
5
22
  apiKey: string;
6
23
  /** Required for OpenAI (chat.completions). */
7
24
  openaiApiKey?: string;
8
25
  /** Required for Anthropic (messages). Install @anthropic-ai/sdk separately. */
9
26
  anthropicApiKey?: string;
27
+ /** Default: https://api.signalvault.io. Plain http:// is only accepted for localhost. */
10
28
  baseUrl?: string;
29
+ /**
30
+ * Sent with every event for backwards compatibility. The server takes the
31
+ * environment from the API key, so this value does not select rules.
32
+ */
11
33
  environment?: 'development' | 'staging' | 'production';
12
34
  debug?: boolean;
13
35
  mirrorMode?: boolean;
36
+ /** Behaviour when no guardrail decision can be obtained. Default: 'open'. */
37
+ failMode?: FailMode;
14
38
  /**
15
39
  * Timeout (ms) for the pre-flight /v1/events call in normal mode.
16
- * This is in the critical path — on timeout/error the SDK fails open (allows).
17
- * Default: 2000
40
+ * This is in the critical path. Default: 2000
18
41
  */
19
42
  preflightTimeout?: number;
20
43
  /**
@@ -28,25 +51,45 @@ export interface SignalVaultConfig {
28
51
  */
29
52
  metadata?: Record<string, unknown>;
30
53
  }
54
+ export interface SignalVaultViolation {
55
+ rule_id?: string;
56
+ type: string;
57
+ severity: number;
58
+ action: string;
59
+ details: Record<string, unknown>;
60
+ }
31
61
  export interface SignalVaultDecision {
32
62
  decision: 'allow' | 'warn' | 'block' | 'redact';
33
- violations: Array<{
34
- rule_id?: string;
35
- type: string;
36
- severity: number;
37
- action: string;
38
- details: Record<string, unknown>;
39
- }>;
63
+ violations: SignalVaultViolation[];
64
+ /**
65
+ * Rule types whose redact action matched, with match counts. Redaction
66
+ * applies to what SignalVault stores; the request sent to the provider is
67
+ * not modified. Check `.length`, never truthiness.
68
+ */
40
69
  redactions: Array<{
41
- start: number;
42
- end: number;
43
- replacement: string;
70
+ type: string;
71
+ count: number;
44
72
  }>;
73
+ /** Present on block decisions. */
74
+ dashboard_url?: string;
45
75
  }
46
76
  export interface CreateOptions {
47
77
  /** Per-call metadata, merged over config-level metadata. */
48
78
  metadata?: Record<string, unknown>;
49
79
  }
80
+ /** Thrown when a guardrail rule blocks the request. */
81
+ export declare class SignalVaultBlockedError extends Error {
82
+ readonly requestId: string;
83
+ readonly violations: SignalVaultViolation[];
84
+ readonly dashboardUrl?: string;
85
+ constructor(requestId: string, violations: SignalVaultViolation[], dashboardUrl?: string);
86
+ }
87
+ /** Thrown in `failMode: 'closed'` when no guardrail decision could be obtained. */
88
+ export declare class SignalVaultUnavailableError extends Error {
89
+ readonly requestId: string;
90
+ readonly status?: number;
91
+ constructor(requestId: string, reason: string, status?: number);
92
+ }
50
93
  /**
51
94
  * SignalVault Client — wraps OpenAI and/or Anthropic with guardrails and audit logging.
52
95
  *
@@ -55,10 +98,9 @@ export interface CreateOptions {
55
98
  * const client = new SignalVaultClient({
56
99
  * apiKey: 'sk_live_...',
57
100
  * openaiApiKey: process.env.OPENAI_API_KEY!,
58
- * baseUrl: 'https://api.signalvault.io',
59
101
  * metadata: { user_id: '123' },
60
102
  * });
61
- * const res = await client.chat.completions.create({ model: 'gpt-4', messages: [...] });
103
+ * const res = await client.chat.completions.create({ model: 'gpt-4o', messages: [...] });
62
104
  * ```
63
105
  *
64
106
  * @example Anthropic
@@ -66,9 +108,8 @@ export interface CreateOptions {
66
108
  * const client = new SignalVaultClient({
67
109
  * apiKey: 'sk_live_...',
68
110
  * anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
69
- * baseUrl: 'https://api.signalvault.io',
70
111
  * });
71
- * const res = await client.messages.create({ model: 'claude-3-5-sonnet-20241022', messages: [...], max_tokens: 1024 });
112
+ * const res = await client.messages.create({ model: 'claude-sonnet-4-5', messages: [...], max_tokens: 1024 });
72
113
  * ```
73
114
  */
74
115
  export declare class SignalVaultClient {
@@ -77,12 +118,27 @@ export declare class SignalVaultClient {
77
118
  private readonly svEnvironment;
78
119
  private readonly debugMode;
79
120
  private readonly mirrorMode;
121
+ private readonly failMode;
80
122
  private readonly preflightTimeout;
81
123
  private readonly bgTimeout;
82
124
  private readonly defaultMetadata;
83
125
  private readonly openai;
84
126
  private readonly anthropic;
127
+ private readonly pending;
128
+ private readonly lastWarned;
85
129
  constructor(config: SignalVaultConfig);
130
+ /**
131
+ * Waits for in-flight audit events to finish sending, up to `timeoutMs`.
132
+ * Call before a short-lived process exits (scripts, serverless handlers),
133
+ * otherwise queued events are lost.
134
+ */
135
+ flush(timeoutMs?: number): Promise<void>;
136
+ /** Flushes pending events. The client can still be used afterwards. */
137
+ close(timeoutMs?: number): Promise<void>;
138
+ /** Tracks a background promise so flush() can wait for it. Never rejects. */
139
+ private track;
140
+ /** Prints a warning, at most once per minute per key. Not gated on debug. */
141
+ private warn;
86
142
  /**
87
143
  * Wraps a function so every call records an `agent.tool_call` event with
88
144
  * timing, input, output, and any error. The wrapper auto-times, runs the
@@ -150,11 +206,50 @@ export declare class SignalVaultClient {
150
206
  private handleOpenAINormal;
151
207
  private handleAnthropicMirror;
152
208
  private handleAnthropicNormal;
209
+ /**
210
+ * Yields chunks through unchanged and records the response when the stream
211
+ * ends — including when the consumer breaks out early or the stream throws,
212
+ * in which case the partial output is recorded. When the SDK added
213
+ * `include_usage` itself (`hideUsageChunk`), the usage-only final chunk is
214
+ * consumed here rather than passed on.
215
+ */
153
216
  private wrapStream;
154
217
  private wrapAnthropicStream;
155
- private authHeaders;
218
+ /** Runs the pre-flight check and throws if the request must not proceed. */
219
+ private enforce;
220
+ /**
221
+ * Called when no decision could be obtained. Throws in failMode 'closed';
222
+ * otherwise warns (never silently) and allows.
223
+ */
224
+ private unavailable;
225
+ /**
226
+ * The ai.request body to record in the background when the pre-flight got
227
+ * no decision, so an unchecked call is still in the audit log. Undefined
228
+ * when the pre-flight was recorded normally; null when the server refused
229
+ * it and no audit event should be sent.
230
+ */
231
+ private fallbackRequest;
232
+ private requestBody;
233
+ private describeFailure;
234
+ private headers;
235
+ /** Single POST. Never follows redirects, so the API key is never re-sent elsewhere. */
236
+ private post;
237
+ /**
238
+ * POST for background events. Adds an `event_id` so a retry is never
239
+ * double-counted, and retries once on connection errors, 5xx, and 429 with a
240
+ * short Retry-After. Timeouts are not retried: the server may be slow rather
241
+ * than down, and a retry would double the time the event is held. Returns
242
+ * the last response, or null if nothing was received.
243
+ */
244
+ private postBackground;
245
+ private logUndelivered;
156
246
  private sendRequest;
157
247
  private sendResponseEvent;
248
+ /**
249
+ * Mirror mode: records the request, then the response. Sequential on
250
+ * purpose — the server rejects an ai.response whose ai.request it has not
251
+ * stored yet, so sending both at once can lose the response.
252
+ */
158
253
  private sendAuditEvents;
159
254
  }
160
255
  export default SignalVaultClient;