@signalvaultio/node 0.1.0 → 0.2.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 CHANGED
@@ -1,26 +1,33 @@
1
1
  # @signalvaultio/node
2
2
 
3
- AI audit logs and guardrails for your OpenAI applications.
3
+ AI audit logs and guardrails for your OpenAI and Anthropic applications.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
+ # OpenAI only
8
9
  npm install @signalvaultio/node openai
10
+
11
+ # Anthropic only
12
+ npm install @signalvaultio/node @anthropic-ai/sdk
13
+
14
+ # Both
15
+ npm install @signalvaultio/node openai @anthropic-ai/sdk
9
16
  ```
10
17
 
11
- ## Quick Start
18
+ ## Quick Start — OpenAI
12
19
 
13
20
  ```typescript
14
21
  import SignalVaultClient from '@signalvaultio/node';
15
22
 
16
23
  const client = new SignalVaultClient({
17
- apiKey: 'sk_live_your_signalvault_key', // Your SignalVault API key
18
- openaiApiKey: process.env.OPENAI_API_KEY!, // Your OpenAI API key
19
- baseUrl: 'https://api.signalvault.io', // SignalVault API URL
24
+ apiKey: 'sk_live_your_signalvault_key',
25
+ openaiApiKey: process.env.OPENAI_API_KEY!,
26
+ baseUrl: 'https://api.signalvault.io',
20
27
  environment: 'production',
21
28
  });
22
29
 
23
- // Use exactly like OpenAI SDK — all calls are logged and guarded
30
+ // Use exactly like OpenAI SDK
24
31
  const response = await client.chat.completions.create({
25
32
  model: 'gpt-4',
26
33
  messages: [{ role: 'user', content: 'Hello!' }],
@@ -29,11 +36,31 @@ const response = await client.chat.completions.create({
29
36
  console.log(response.choices[0].message.content);
30
37
  ```
31
38
 
39
+ ## Quick Start — Anthropic
40
+
41
+ ```typescript
42
+ const client = new SignalVaultClient({
43
+ apiKey: 'sk_live_your_signalvault_key',
44
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
45
+ baseUrl: 'https://api.signalvault.io',
46
+ });
47
+
48
+ // Use exactly like Anthropic SDK
49
+ const response = await client.messages.create({
50
+ model: 'claude-3-5-sonnet-20241022',
51
+ messages: [{ role: 'user', content: 'Hello!' }],
52
+ max_tokens: 1024,
53
+ });
54
+
55
+ console.log(response.content[0].text);
56
+ ```
57
+
32
58
  ## Streaming
33
59
 
34
- Streaming responses are fully supported. SignalVault logs the complete response once the stream finishes:
60
+ Streaming is fully supported for both providers. SignalVault logs the complete response once the stream finishes:
35
61
 
36
62
  ```typescript
63
+ // OpenAI streaming
37
64
  const stream = await client.chat.completions.create({
38
65
  model: 'gpt-4',
39
66
  messages: [{ role: 'user', content: 'Write a poem' }],
@@ -43,48 +70,98 @@ const stream = await client.chat.completions.create({
43
70
  for await (const chunk of stream) {
44
71
  process.stdout.write(chunk.choices[0]?.delta?.content || '');
45
72
  }
73
+
74
+ // Anthropic streaming
75
+ const stream = await client.messages.create({
76
+ model: 'claude-3-5-sonnet-20241022',
77
+ messages: [{ role: 'user', content: 'Write a poem' }],
78
+ max_tokens: 1024,
79
+ stream: true,
80
+ });
81
+
82
+ for await (const event of stream) {
83
+ if (event.type === 'content_block_delta') {
84
+ process.stdout.write(event.delta.text || '');
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Metadata
90
+
91
+ Attach contextual metadata to every event — perfect for audit trails, user attribution, and analytics:
92
+
93
+ ```typescript
94
+ // Set defaults at client level
95
+ const client = new SignalVaultClient({
96
+ apiKey: 'sk_live_...',
97
+ openaiApiKey: process.env.OPENAI_API_KEY!,
98
+ metadata: { workspace_id: 'ws_abc', environment: 'production' },
99
+ });
100
+
101
+ // Override per-call
102
+ const response = await client.chat.completions.create(
103
+ { model: 'gpt-4', messages: [...] },
104
+ { metadata: { user_id: 'u_123', feature: 'support-chat' } }
105
+ );
106
+ ```
107
+
108
+ ## Timeout Configuration
109
+
110
+ 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:
111
+
112
+ ```typescript
113
+ const client = new SignalVaultClient({
114
+ apiKey: 'sk_live_...',
115
+ openaiApiKey: process.env.OPENAI_API_KEY!,
116
+ preflightTimeout: 2000, // ms — pre-flight check timeout (fails open). Default: 2000
117
+ timeout: 10000, // ms — background/post-flight calls. Default: 10000
118
+ });
46
119
  ```
47
120
 
48
121
  ## Mirror Mode
49
122
 
50
- In mirror mode, requests go directly to OpenAI first and SignalVault audits them asynchronously — no latency added, never blocks:
123
+ In mirror mode, requests go directly to the AI provider first and SignalVault audits them asynchronously — no latency added, never blocks:
51
124
 
52
125
  ```typescript
53
126
  const client = new SignalVaultClient({
54
127
  apiKey: 'sk_live_...',
55
128
  openaiApiKey: process.env.OPENAI_API_KEY!,
56
- mirrorMode: true, // Monitor only, never block
129
+ mirrorMode: true,
57
130
  });
58
131
  ```
59
132
 
60
133
  ## Features
61
134
 
62
135
  - **Automatic Logging** — Every request and response is recorded in your SignalVault dashboard
63
- - **Pre-flight Guardrails** — Block or redact requests before they reach OpenAI
136
+ - **Pre-flight Guardrails** — Block or redact requests before they reach the AI provider
64
137
  - **PII Detection** — Automatically detect emails, phone numbers, SSNs in prompts
65
138
  - **Secret Detection** — Block API keys and tokens in prompts
66
139
  - **Token Limits** — Enforce cost controls per request
67
140
  - **Model Allowlists** — Restrict which AI models can be used
68
- - **Streaming Support** — Full support for streaming completions
141
+ - **Streaming Support** — Full support for streaming completions (OpenAI + Anthropic)
69
142
  - **Mirror Mode** — Observe without blocking (zero added latency)
143
+ - **Metadata** — Tag every event with user_id, feature, workspace_id, etc.
144
+ - **Multi-provider** — OpenAI and Anthropic/Claude support
70
145
 
71
146
  ## Configuration
72
147
 
73
148
  ```typescript
74
149
  const client = new SignalVaultClient({
75
- apiKey: 'sk_live_...', // Your SignalVault API key
76
- openaiApiKey: 'sk-...', // Your OpenAI API key
77
- baseUrl: 'https://api.signalvault.io', // SignalVault API URL
78
- environment: 'production', // 'development' | 'staging' | 'production'
79
- debug: false, // Enable debug logging
80
- mirrorMode: false, // Monitor-only mode
150
+ apiKey: 'sk_live_...', // Your SignalVault API key (required)
151
+ openaiApiKey: 'sk-...', // OpenAI API key (required for chat.completions)
152
+ anthropicApiKey: 'sk-ant-...', // Anthropic API key (required for messages)
153
+ baseUrl: 'https://api.signalvault.io',
154
+ environment: 'production', // 'development' | 'staging' | 'production'
155
+ debug: false,
156
+ mirrorMode: false,
157
+ preflightTimeout: 2000, // ms
158
+ timeout: 10000, // ms
159
+ metadata: {}, // Default metadata for all events
81
160
  });
82
161
  ```
83
162
 
84
163
  ## Error Handling
85
164
 
86
- If a request is blocked by a guardrail, an error will be thrown:
87
-
88
165
  ```typescript
89
166
  try {
90
167
  const response = await client.chat.completions.create({
@@ -1,49 +1,99 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const index_1 = require("../index");
4
- // Minimal unit tests for the SDK (no network calls)
5
4
  describe('SignalVaultClient', () => {
6
- it('should construct with required config', () => {
7
- const client = new index_1.SignalVaultClient({
8
- apiKey: 'sk_test_abc',
9
- openaiApiKey: 'sk-fake',
10
- });
11
- expect(client).toBeDefined();
12
- expect(client.chat).toBeDefined();
13
- expect(client.chat.completions).toBeDefined();
14
- expect(typeof client.chat.completions.create).toBe('function');
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
+ });
15
62
  });
16
- it('should default to localhost and production', () => {
17
- const client = new index_1.SignalVaultClient({
18
- apiKey: 'sk_test_abc',
19
- openaiApiKey: 'sk-fake',
20
- });
21
- // Access private fields via bracket notation for testing
22
- expect(client.signalvaultBaseUrl).toBe('http://localhost:4000');
23
- expect(client.signalvaultEnvironment).toBe('production');
24
- expect(client.debugMode).toBe(false);
25
- expect(client.mirrorMode).toBe(false);
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
+ });
26
76
  });
27
- it('should accept custom baseUrl and environment', () => {
28
- const client = new index_1.SignalVaultClient({
29
- apiKey: 'sk_test_abc',
30
- openaiApiKey: 'sk-fake',
31
- baseUrl: 'https://api.signalvault.io/',
32
- environment: 'staging',
33
- debug: true,
34
- mirrorMode: true,
35
- });
36
- expect(client.signalvaultBaseUrl).toBe('https://api.signalvault.io');
37
- expect(client.signalvaultEnvironment).toBe('staging');
38
- expect(client.debugMode).toBe(true);
39
- expect(client.mirrorMode).toBe(true);
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
+ });
40
85
  });
41
- it('should strip trailing slashes from baseUrl', () => {
42
- const client = new index_1.SignalVaultClient({
43
- apiKey: 'sk_test_abc',
44
- openaiApiKey: 'sk-fake',
45
- baseUrl: 'https://example.com///',
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([]);
46
97
  });
47
- expect(client.signalvaultBaseUrl).toBe('https://example.com');
48
98
  });
49
99
  });
package/dist/index.d.ts CHANGED
@@ -1,11 +1,30 @@
1
1
  import OpenAI from 'openai';
2
2
  export interface SignalVaultConfig {
3
3
  apiKey: string;
4
- openaiApiKey: string;
4
+ /** Required for OpenAI (chat.completions). */
5
+ openaiApiKey?: string;
6
+ /** Required for Anthropic (messages). Install @anthropic-ai/sdk separately. */
7
+ anthropicApiKey?: string;
5
8
  baseUrl?: string;
6
9
  environment?: 'development' | 'staging' | 'production';
7
10
  debug?: boolean;
8
11
  mirrorMode?: boolean;
12
+ /**
13
+ * Timeout (ms) for the pre-flight /v1/events call in normal mode.
14
+ * This is in the critical path — on timeout/error the SDK fails open (allows).
15
+ * Default: 2000
16
+ */
17
+ preflightTimeout?: number;
18
+ /**
19
+ * Timeout (ms) for all background / post-flight event calls.
20
+ * Default: 10000
21
+ */
22
+ timeout?: number;
23
+ /**
24
+ * Default metadata attached to every event. Can be overridden per-call.
25
+ * Use for user_id, feature, workspace_id, etc.
26
+ */
27
+ metadata?: Record<string, unknown>;
9
28
  }
10
29
  export interface SignalVaultDecision {
11
30
  decision: 'allow' | 'warn' | 'block' | 'redact';
@@ -22,50 +41,64 @@ export interface SignalVaultDecision {
22
41
  replacement: string;
23
42
  }>;
24
43
  }
44
+ export interface CreateOptions {
45
+ /** Per-call metadata, merged over config-level metadata. */
46
+ metadata?: Record<string, unknown>;
47
+ }
25
48
  /**
26
- * SignalVault Client — wraps OpenAI with guardrails and audit logging.
49
+ * SignalVault Client — wraps OpenAI and/or Anthropic with guardrails and audit logging.
27
50
  *
28
- * @example
51
+ * @example OpenAI
29
52
  * ```typescript
30
53
  * const client = new SignalVaultClient({
31
54
  * apiKey: 'sk_live_...',
32
55
  * openaiApiKey: process.env.OPENAI_API_KEY!,
33
56
  * baseUrl: 'https://api.signalvault.io',
57
+ * metadata: { user_id: '123' },
34
58
  * });
35
- *
36
- * // Non-streaming
37
59
  * const res = await client.chat.completions.create({ model: 'gpt-4', messages: [...] });
60
+ * ```
38
61
  *
39
- * // Streaming
40
- * const stream = await client.chat.completions.create({ model: 'gpt-4', messages: [...], stream: true });
41
- * for await (const chunk of stream) { ... }
62
+ * @example Anthropic
63
+ * ```typescript
64
+ * const client = new SignalVaultClient({
65
+ * apiKey: 'sk_live_...',
66
+ * anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
67
+ * baseUrl: 'https://api.signalvault.io',
68
+ * });
69
+ * const res = await client.messages.create({ model: 'claude-3-5-sonnet-20241022', messages: [...], max_tokens: 1024 });
42
70
  * ```
43
71
  */
44
72
  export declare class SignalVaultClient {
45
- private signalvaultApiKey;
46
- private signalvaultBaseUrl;
47
- private signalvaultEnvironment;
48
- private debugMode;
49
- private mirrorMode;
50
- private openai;
73
+ private readonly svApiKey;
74
+ private readonly svBaseUrl;
75
+ private readonly svEnvironment;
76
+ private readonly debugMode;
77
+ private readonly mirrorMode;
78
+ private readonly preflightTimeout;
79
+ private readonly bgTimeout;
80
+ private readonly defaultMetadata;
81
+ private readonly openai;
82
+ private readonly anthropic;
51
83
  constructor(config: SignalVaultConfig);
52
- /**
53
- * Chat completions with SignalVault guardrails.
54
- * Supports both streaming and non-streaming.
55
- */
56
84
  get chat(): {
57
85
  completions: {
58
- create: (params: OpenAI.Chat.ChatCompletionCreateParams) => Promise<any>;
86
+ create(params: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming, options?: CreateOptions): Promise<OpenAI.Chat.ChatCompletion>;
87
+ create(params: OpenAI.Chat.ChatCompletionCreateParamsStreaming, options?: CreateOptions): Promise<AsyncGenerator<OpenAI.Chat.Completions.ChatCompletionChunk>>;
59
88
  };
60
89
  };
61
- private handleMirrorMode;
62
- private handleNormalMode;
63
- private wrapStreamForAudit;
64
- private wrapStreamForPostFlight;
90
+ get messages(): {
91
+ create(params: any, options?: CreateOptions): Promise<any>;
92
+ };
93
+ private handleOpenAIMirror;
94
+ private handleOpenAINormal;
95
+ private handleAnthropicMirror;
96
+ private handleAnthropicNormal;
97
+ private wrapStream;
98
+ private wrapAnthropicStream;
99
+ private authHeaders;
65
100
  private sendRequest;
66
- private sendResponse;
67
- private sendStreamResponse;
68
- private sendAudit;
69
- private sendStreamAudit;
101
+ private sendResponseEvent;
102
+ private sendAuditEvents;
70
103
  }
71
104
  export default SignalVaultClient;
package/dist/index.js CHANGED
@@ -6,91 +6,140 @@ 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
+ // ---------------------------------------------------------------------------
10
+ // SignalVaultClient
11
+ // ---------------------------------------------------------------------------
9
12
  /**
10
- * SignalVault Client — wraps OpenAI with guardrails and audit logging.
13
+ * SignalVault Client — wraps OpenAI and/or Anthropic with guardrails and audit logging.
11
14
  *
12
- * @example
15
+ * @example OpenAI
13
16
  * ```typescript
14
17
  * const client = new SignalVaultClient({
15
18
  * apiKey: 'sk_live_...',
16
19
  * openaiApiKey: process.env.OPENAI_API_KEY!,
17
20
  * baseUrl: 'https://api.signalvault.io',
21
+ * metadata: { user_id: '123' },
18
22
  * });
19
- *
20
- * // Non-streaming
21
23
  * const res = await client.chat.completions.create({ model: 'gpt-4', messages: [...] });
24
+ * ```
22
25
  *
23
- * // Streaming
24
- * const stream = await client.chat.completions.create({ model: 'gpt-4', messages: [...], stream: true });
25
- * for await (const chunk of stream) { ... }
26
+ * @example Anthropic
27
+ * ```typescript
28
+ * const client = new SignalVaultClient({
29
+ * apiKey: 'sk_live_...',
30
+ * anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
31
+ * baseUrl: 'https://api.signalvault.io',
32
+ * });
33
+ * const res = await client.messages.create({ model: 'claude-3-5-sonnet-20241022', messages: [...], max_tokens: 1024 });
26
34
  * ```
27
35
  */
28
36
  class SignalVaultClient {
29
37
  constructor(config) {
30
- this.signalvaultApiKey = config.apiKey;
31
- this.signalvaultBaseUrl = (config.baseUrl || 'http://localhost:4000').replace(/\/+$/, '');
32
- this.signalvaultEnvironment = config.environment || 'production';
38
+ if (!config.openaiApiKey && !config.anthropicApiKey) {
39
+ throw new Error('[SignalVault] At least one of openaiApiKey or anthropicApiKey is required.');
40
+ }
41
+ this.svApiKey = config.apiKey;
42
+ this.svBaseUrl = (config.baseUrl || 'http://localhost:4000').replace(/\/+$/, '');
43
+ this.svEnvironment = config.environment || 'production';
33
44
  this.debugMode = config.debug || false;
34
45
  this.mirrorMode = config.mirrorMode || false;
35
- this.openai = new openai_1.default({
36
- apiKey: config.openaiApiKey,
37
- });
46
+ this.preflightTimeout = config.preflightTimeout ?? 2000;
47
+ this.bgTimeout = config.timeout ?? 10000;
48
+ this.defaultMetadata = config.metadata ?? {};
49
+ this.openai = config.openaiApiKey
50
+ ? new openai_1.default({ apiKey: config.openaiApiKey })
51
+ : null;
52
+ if (config.anthropicApiKey) {
53
+ try {
54
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
55
+ const mod = require('@anthropic-ai/sdk');
56
+ const Anthropic = mod.default ?? mod;
57
+ this.anthropic = new Anthropic({ apiKey: config.anthropicApiKey });
58
+ }
59
+ catch {
60
+ throw new Error('[SignalVault] @anthropic-ai/sdk is not installed. Run: npm install @anthropic-ai/sdk');
61
+ }
62
+ }
63
+ else {
64
+ this.anthropic = null;
65
+ }
38
66
  }
39
- /**
40
- * Chat completions with SignalVault guardrails.
41
- * Supports both streaming and non-streaming.
42
- */
67
+ // ---------------------------------------------------------------------------
68
+ // OpenAI: chat.completions
69
+ // ---------------------------------------------------------------------------
43
70
  get chat() {
71
+ if (!this.openai) {
72
+ throw new Error('[SignalVault] openaiApiKey was not provided.');
73
+ }
74
+ const self = this;
44
75
  return {
45
76
  completions: {
46
- create: async (params) => {
77
+ async create(params, options) {
47
78
  const requestId = (0, uuid_1.v4)();
48
- if (this.debugMode) {
49
- console.log('[SignalVault] Processing request:', requestId, this.mirrorMode ? '(MIRROR MODE)' : '', params.stream ? '(STREAMING)' : '');
79
+ const metadata = { ...self.defaultMetadata, ...(options?.metadata ?? {}) };
80
+ if (self.debugMode) {
81
+ console.log('[SignalVault] Processing request:', requestId, self.mirrorMode ? '(MIRROR MODE)' : '', params.stream ? '(STREAMING)' : '');
50
82
  }
51
- // Mirror mode: call OpenAI first, then audit
52
- if (this.mirrorMode) {
53
- return this.handleMirrorMode(requestId, params);
83
+ if (self.mirrorMode) {
84
+ return self.handleOpenAIMirror(requestId, params, metadata);
54
85
  }
55
- // Normal mode: pre-flight checks first
56
- return this.handleNormalMode(requestId, params);
86
+ return self.handleOpenAINormal(requestId, params, metadata);
57
87
  },
58
88
  },
59
89
  };
60
90
  }
61
91
  // ---------------------------------------------------------------------------
62
- // Mirror mode — no latency added, audit asynchronously
92
+ // Anthropic: messages
93
+ // ---------------------------------------------------------------------------
94
+ get messages() {
95
+ if (!this.anthropic) {
96
+ throw new Error('[SignalVault] anthropicApiKey not provided or @anthropic-ai/sdk not installed.');
97
+ }
98
+ const self = this;
99
+ return {
100
+ async create(params, options) {
101
+ const requestId = (0, uuid_1.v4)();
102
+ const metadata = { ...self.defaultMetadata, ...(options?.metadata ?? {}) };
103
+ if (self.debugMode) {
104
+ console.log('[SignalVault] Anthropic request:', requestId, self.mirrorMode ? '(MIRROR MODE)' : '', params.stream ? '(STREAMING)' : '');
105
+ }
106
+ if (self.mirrorMode) {
107
+ return self.handleAnthropicMirror(requestId, params, metadata);
108
+ }
109
+ return self.handleAnthropicNormal(requestId, params, metadata);
110
+ },
111
+ };
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // OpenAI — mirror mode
63
115
  // ---------------------------------------------------------------------------
64
- async handleMirrorMode(requestId, params) {
116
+ async handleOpenAIMirror(requestId, params, metadata) {
65
117
  try {
66
118
  const response = await this.openai.chat.completions.create(params);
67
119
  if (params.stream) {
68
- // Wrap the stream to collect chunks for auditing
69
- return this.wrapStreamForAudit(requestId, params, response);
120
+ return this.wrapStream(requestId, params.model, params.messages, response, async (output, promptTokens, completionTokens) => {
121
+ await this.sendAuditEvents(requestId, params.model, params.messages, output, promptTokens, completionTokens, metadata, 'openai');
122
+ });
70
123
  }
71
- // Non-streaming: audit in background
72
124
  const completion = response;
73
- this.sendAudit(requestId, params, completion).catch((err) => {
74
- if (this.debugMode) {
125
+ this.sendAuditEvents(requestId, params.model, params.messages, completion.choices[0]?.message?.content || '', completion.usage?.prompt_tokens || 0, completion.usage?.completion_tokens || 0, metadata, 'openai').catch((err) => {
126
+ if (this.debugMode)
75
127
  console.error('[SignalVault] Audit failed (non-blocking):', err);
76
- }
77
128
  });
78
129
  return completion;
79
130
  }
80
131
  catch (error) {
81
- if (this.debugMode) {
132
+ if (this.debugMode)
82
133
  console.error('[SignalVault] OpenAI error:', error);
83
- }
84
134
  throw error;
85
135
  }
86
136
  }
87
137
  // ---------------------------------------------------------------------------
88
- // Normal mode — pre-flight guardrails, then OpenAI, then post-flight
138
+ // OpenAI — normal mode
89
139
  // ---------------------------------------------------------------------------
90
- async handleNormalMode(requestId, params) {
140
+ async handleOpenAINormal(requestId, params, metadata) {
91
141
  try {
92
- // Pre-flight check
93
- const decision = await this.sendRequest(requestId, params);
142
+ const decision = await this.sendRequest(requestId, params.model, params.messages, metadata, 'openai');
94
143
  if (decision.decision === 'block') {
95
144
  throw new Error(`[SignalVault] Request blocked: ${JSON.stringify(decision.violations)}`);
96
145
  }
@@ -99,25 +148,76 @@ class SignalVaultClient {
99
148
  }
100
149
  const response = await this.openai.chat.completions.create(params);
101
150
  if (params.stream) {
102
- // Wrap stream to collect output for post-flight logging
103
- return this.wrapStreamForPostFlight(requestId, params, response);
151
+ return this.wrapStream(requestId, params.model, params.messages, response, async (output, promptTokens, completionTokens) => {
152
+ await this.sendResponseEvent(requestId, params.model, output, promptTokens, completionTokens, metadata, 'openai');
153
+ });
104
154
  }
105
- // Non-streaming: post-flight logging
106
155
  const completion = response;
107
- await this.sendResponse(requestId, completion, params.model);
156
+ await this.sendResponseEvent(requestId, params.model, completion.choices[0]?.message?.content || '', completion.usage?.prompt_tokens || 0, completion.usage?.completion_tokens || 0, metadata, 'openai');
108
157
  return completion;
109
158
  }
110
159
  catch (error) {
111
- if (this.debugMode) {
160
+ if (this.debugMode)
112
161
  console.error('[SignalVault] Error:', error);
162
+ throw error;
163
+ }
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Anthropic — mirror mode
167
+ // ---------------------------------------------------------------------------
168
+ async handleAnthropicMirror(requestId, params, metadata) {
169
+ try {
170
+ const response = await this.anthropic.messages.create(params);
171
+ if (params.stream) {
172
+ return this.wrapAnthropicStream(requestId, params.model, params.messages, response, async (output, inputTokens, outputTokens) => {
173
+ await this.sendAuditEvents(requestId, params.model, params.messages, output, inputTokens, outputTokens, metadata, 'anthropic');
174
+ });
113
175
  }
176
+ const output = response.content?.[0]?.text || '';
177
+ this.sendAuditEvents(requestId, params.model, params.messages, output, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, metadata, 'anthropic').catch((err) => {
178
+ if (this.debugMode)
179
+ console.error('[SignalVault] Anthropic audit failed (non-blocking):', err);
180
+ });
181
+ return response;
182
+ }
183
+ catch (error) {
184
+ if (this.debugMode)
185
+ console.error('[SignalVault] Anthropic error:', error);
114
186
  throw error;
115
187
  }
116
188
  }
117
189
  // ---------------------------------------------------------------------------
118
- // Stream wrappers — collect chunks and send audit/post-flight events
190
+ // Anthropic — normal mode
119
191
  // ---------------------------------------------------------------------------
120
- async *wrapStreamForAudit(requestId, params, stream) {
192
+ async handleAnthropicNormal(requestId, params, metadata) {
193
+ try {
194
+ const decision = await this.sendRequest(requestId, params.model, params.messages, metadata, 'anthropic');
195
+ if (decision.decision === 'block') {
196
+ throw new Error(`[SignalVault] Request blocked: ${JSON.stringify(decision.violations)}`);
197
+ }
198
+ if (decision.decision === 'warn' && this.debugMode) {
199
+ console.warn('[SignalVault] Warnings:', decision.violations);
200
+ }
201
+ const response = await this.anthropic.messages.create(params);
202
+ if (params.stream) {
203
+ return this.wrapAnthropicStream(requestId, params.model, params.messages, response, async (output, inputTokens, outputTokens) => {
204
+ await this.sendResponseEvent(requestId, params.model, output, inputTokens, outputTokens, metadata, 'anthropic');
205
+ });
206
+ }
207
+ const output = response.content?.[0]?.text || '';
208
+ await this.sendResponseEvent(requestId, params.model, output, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, metadata, 'anthropic');
209
+ return response;
210
+ }
211
+ catch (error) {
212
+ if (this.debugMode)
213
+ console.error('[SignalVault] Anthropic error:', error);
214
+ throw error;
215
+ }
216
+ }
217
+ // ---------------------------------------------------------------------------
218
+ // Unified OpenAI stream wrapper
219
+ // ---------------------------------------------------------------------------
220
+ async *wrapStream(requestId, model, messages, stream, onComplete) {
121
221
  const chunks = [];
122
222
  let promptTokens = 0;
123
223
  let completionTokens = 0;
@@ -125,210 +225,138 @@ class SignalVaultClient {
125
225
  const content = chunk.choices[0]?.delta?.content || '';
126
226
  if (content)
127
227
  chunks.push(content);
128
- // Collect usage if provided in final chunk
129
228
  if (chunk.usage) {
130
229
  promptTokens = chunk.usage.prompt_tokens || 0;
131
230
  completionTokens = chunk.usage.completion_tokens || 0;
132
231
  }
133
232
  yield chunk;
134
233
  }
135
- // Send audit after stream completes
136
- const fullOutput = chunks.join('');
137
- this.sendStreamAudit(requestId, params, fullOutput, promptTokens, completionTokens).catch((err) => {
138
- if (this.debugMode) {
139
- console.error('[SignalVault] Stream audit failed:', err);
140
- }
234
+ onComplete(chunks.join(''), promptTokens, completionTokens).catch((err) => {
235
+ if (this.debugMode)
236
+ console.error('[SignalVault] Post-stream callback failed:', err);
141
237
  });
142
238
  }
143
- async *wrapStreamForPostFlight(requestId, params, stream) {
239
+ // ---------------------------------------------------------------------------
240
+ // Anthropic stream wrapper
241
+ // ---------------------------------------------------------------------------
242
+ async *wrapAnthropicStream(requestId, model, messages, stream, onComplete) {
144
243
  const chunks = [];
145
- let promptTokens = 0;
146
- let completionTokens = 0;
147
- for await (const chunk of stream) {
148
- const content = chunk.choices[0]?.delta?.content || '';
149
- if (content)
150
- chunks.push(content);
151
- if (chunk.usage) {
152
- promptTokens = chunk.usage.prompt_tokens || 0;
153
- completionTokens = chunk.usage.completion_tokens || 0;
244
+ let inputTokens = 0;
245
+ let outputTokens = 0;
246
+ for await (const event of stream) {
247
+ if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
248
+ chunks.push(event.delta.text || '');
154
249
  }
155
- yield chunk;
156
- }
157
- // Post-flight logging
158
- const fullOutput = chunks.join('');
159
- try {
160
- await this.sendStreamResponse(requestId, params.model, fullOutput, promptTokens, completionTokens);
161
- }
162
- catch (err) {
163
- if (this.debugMode) {
164
- console.error('[SignalVault] Post-flight logging failed:', err);
250
+ if (event.type === 'message_start' && event.message?.usage) {
251
+ inputTokens = event.message.usage.input_tokens || 0;
252
+ }
253
+ if (event.type === 'message_delta' && event.usage) {
254
+ outputTokens = event.usage.output_tokens || 0;
165
255
  }
256
+ yield event;
166
257
  }
258
+ onComplete(chunks.join(''), inputTokens, outputTokens).catch((err) => {
259
+ if (this.debugMode)
260
+ console.error('[SignalVault] Anthropic post-stream callback failed:', err);
261
+ });
167
262
  }
168
263
  // ---------------------------------------------------------------------------
169
264
  // API communication
170
265
  // ---------------------------------------------------------------------------
171
- async sendRequest(requestId, params) {
172
- const response = await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
173
- method: 'POST',
174
- headers: {
175
- 'Content-Type': 'application/json',
176
- Authorization: `Bearer ${this.signalvaultApiKey}`,
177
- },
178
- body: JSON.stringify({
179
- type: 'ai.request',
180
- request_id: requestId,
181
- environment: this.signalvaultEnvironment,
182
- provider: 'openai',
183
- model: params.model,
184
- metadata: {},
185
- payload: { messages: params.messages },
186
- }),
187
- });
188
- if (!response.ok) {
189
- if (this.debugMode) {
190
- console.error('[SignalVault] API error:', response.status);
266
+ authHeaders() {
267
+ return {
268
+ 'Content-Type': 'application/json',
269
+ Authorization: `Bearer ${this.svApiKey}`,
270
+ };
271
+ }
272
+ async sendRequest(requestId, model, messages, metadata, provider) {
273
+ try {
274
+ const response = await fetch(`${this.svBaseUrl}/v1/events`, {
275
+ method: 'POST',
276
+ headers: this.authHeaders(),
277
+ signal: AbortSignal.timeout(this.preflightTimeout),
278
+ body: JSON.stringify({
279
+ type: 'ai.request',
280
+ request_id: requestId,
281
+ environment: this.svEnvironment,
282
+ provider,
283
+ model,
284
+ metadata,
285
+ payload: { messages },
286
+ }),
287
+ });
288
+ if (!response.ok) {
289
+ if (this.debugMode)
290
+ console.error('[SignalVault] API error:', response.status);
291
+ return { decision: 'allow', violations: [], redactions: [] };
191
292
  }
293
+ return (await response.json());
294
+ }
295
+ catch (error) {
296
+ if (this.debugMode)
297
+ console.error('[SignalVault] Pre-flight failed (fail-open):', error);
192
298
  return { decision: 'allow', violations: [], redactions: [] };
193
299
  }
194
- return (await response.json());
195
300
  }
196
- async sendResponse(requestId, response, model) {
301
+ async sendResponseEvent(requestId, model, output, promptTokens, completionTokens, metadata, provider) {
197
302
  try {
198
- const output = response.choices[0]?.message?.content || '';
199
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
303
+ await fetch(`${this.svBaseUrl}/v1/events`, {
200
304
  method: 'POST',
201
- headers: {
202
- 'Content-Type': 'application/json',
203
- Authorization: `Bearer ${this.signalvaultApiKey}`,
204
- },
305
+ headers: this.authHeaders(),
306
+ signal: AbortSignal.timeout(this.bgTimeout),
205
307
  body: JSON.stringify({
206
308
  type: 'ai.response',
207
309
  request_id: requestId,
208
- environment: this.signalvaultEnvironment,
209
- provider: 'openai',
310
+ environment: this.svEnvironment,
311
+ provider,
210
312
  model,
211
- metadata: {},
313
+ metadata,
212
314
  payload: {
213
315
  output,
214
- usage: {
215
- prompt_tokens: response.usage?.prompt_tokens || 0,
216
- completion_tokens: response.usage?.completion_tokens || 0,
217
- },
316
+ usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
218
317
  },
219
318
  }),
220
319
  });
221
320
  }
222
321
  catch (error) {
223
- if (this.debugMode) {
224
- console.error('[SignalVault] Failed to send response:', error);
225
- }
322
+ if (this.debugMode)
323
+ console.error('[SignalVault] Failed to send response event:', error);
226
324
  }
227
325
  }
228
- async sendStreamResponse(requestId, model, output, promptTokens, completionTokens) {
229
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
230
- method: 'POST',
231
- headers: {
232
- 'Content-Type': 'application/json',
233
- Authorization: `Bearer ${this.signalvaultApiKey}`,
234
- },
235
- body: JSON.stringify({
236
- type: 'ai.response',
237
- request_id: requestId,
238
- environment: this.signalvaultEnvironment,
239
- provider: 'openai',
240
- model,
241
- metadata: {},
242
- payload: {
243
- output,
244
- usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
245
- },
246
- }),
247
- });
248
- }
249
- async sendAudit(requestId, params, response) {
250
- const output = response.choices[0]?.message?.content || '';
251
- // Send ai.request event
252
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
253
- method: 'POST',
254
- headers: {
255
- 'Content-Type': 'application/json',
256
- Authorization: `Bearer ${this.signalvaultApiKey}`,
257
- },
258
- body: JSON.stringify({
259
- type: 'ai.request',
260
- request_id: requestId,
261
- environment: this.signalvaultEnvironment,
262
- provider: 'openai',
263
- model: params.model,
264
- metadata: {},
265
- payload: { messages: params.messages, monitor_mode: true },
326
+ async sendAuditEvents(requestId, model, messages, output, promptTokens, completionTokens, metadata, provider) {
327
+ const base = {
328
+ request_id: requestId,
329
+ environment: this.svEnvironment,
330
+ provider,
331
+ model,
332
+ metadata,
333
+ };
334
+ await Promise.all([
335
+ fetch(`${this.svBaseUrl}/v1/events`, {
336
+ method: 'POST',
337
+ headers: this.authHeaders(),
338
+ signal: AbortSignal.timeout(this.bgTimeout),
339
+ body: JSON.stringify({
340
+ ...base,
341
+ type: 'ai.request',
342
+ payload: { messages, monitor_mode: true },
343
+ }),
266
344
  }),
267
- });
268
- // Send ai.response event
269
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
270
- method: 'POST',
271
- headers: {
272
- 'Content-Type': 'application/json',
273
- Authorization: `Bearer ${this.signalvaultApiKey}`,
274
- },
275
- body: JSON.stringify({
276
- type: 'ai.response',
277
- request_id: requestId,
278
- environment: this.signalvaultEnvironment,
279
- provider: 'openai',
280
- model: params.model,
281
- metadata: {},
282
- payload: {
283
- output,
284
- usage: {
285
- prompt_tokens: response.usage?.prompt_tokens || 0,
286
- completion_tokens: response.usage?.completion_tokens || 0,
345
+ fetch(`${this.svBaseUrl}/v1/events`, {
346
+ method: 'POST',
347
+ headers: this.authHeaders(),
348
+ signal: AbortSignal.timeout(this.bgTimeout),
349
+ body: JSON.stringify({
350
+ ...base,
351
+ type: 'ai.response',
352
+ payload: {
353
+ output,
354
+ usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
355
+ monitor_mode: true,
287
356
  },
288
- monitor_mode: true,
289
- },
290
- }),
291
- });
292
- }
293
- async sendStreamAudit(requestId, params, output, promptTokens, completionTokens) {
294
- // Send ai.request event
295
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
296
- method: 'POST',
297
- headers: {
298
- 'Content-Type': 'application/json',
299
- Authorization: `Bearer ${this.signalvaultApiKey}`,
300
- },
301
- body: JSON.stringify({
302
- type: 'ai.request',
303
- request_id: requestId,
304
- environment: this.signalvaultEnvironment,
305
- provider: 'openai',
306
- model: params.model,
307
- metadata: {},
308
- payload: { messages: params.messages, monitor_mode: true },
309
- }),
310
- });
311
- // Send ai.response event
312
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
313
- method: 'POST',
314
- headers: {
315
- 'Content-Type': 'application/json',
316
- Authorization: `Bearer ${this.signalvaultApiKey}`,
317
- },
318
- body: JSON.stringify({
319
- type: 'ai.response',
320
- request_id: requestId,
321
- environment: this.signalvaultEnvironment,
322
- provider: 'openai',
323
- model: params.model,
324
- metadata: {},
325
- payload: {
326
- output,
327
- usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
328
- monitor_mode: true,
329
- },
357
+ }),
330
358
  }),
331
- });
359
+ ]);
332
360
  }
333
361
  }
334
362
  exports.SignalVaultClient = SignalVaultClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalvaultio/node",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -18,6 +18,8 @@
18
18
  "audit",
19
19
  "guardrails",
20
20
  "openai",
21
+ "anthropic",
22
+ "claude",
21
23
  "llm",
22
24
  "compliance",
23
25
  "security",
@@ -27,14 +29,19 @@
27
29
  "license": "MIT",
28
30
  "repository": {
29
31
  "type": "git",
30
- "url": "https://github.com/signalvaultio/signalvault-node"
32
+ "url": "git+https://github.com/signalvaultio/signalvault-node.git"
31
33
  },
32
34
  "bugs": {
33
35
  "url": "https://github.com/signalvaultio/signalvault-node/issues"
34
36
  },
35
37
  "homepage": "https://signalvault.io/docs",
36
38
  "peerDependencies": {
37
- "openai": "^4.0.0"
39
+ "openai": "^4.0.0",
40
+ "@anthropic-ai/sdk": ">=0.20.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "openai": { "optional": true },
44
+ "@anthropic-ai/sdk": { "optional": true }
38
45
  },
39
46
  "dependencies": {
40
47
  "uuid": "^9.0.0"