@driftgard/node 1.5.0 → 1.5.1
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 +29 -2
- package/dist/index.d.ts +18 -1
- package/dist/index.js +101 -16
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,6 @@ const result = await dg.evaluate({
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
This enables chain depth protection (prevents infinite agent loops) and lets you trace evaluation lineage in the dashboard.
|
|
54
|
-
```
|
|
55
54
|
|
|
56
55
|
## A/B experiments
|
|
57
56
|
|
|
@@ -89,11 +88,13 @@ const result = await dg.evaluate({
|
|
|
89
88
|
```
|
|
90
89
|
|
|
91
90
|
All fields in `usage` are optional. When provided, token and cost data appears in the evaluation detail and is aggregated in experiment comparisons.
|
|
92
|
-
```
|
|
93
91
|
|
|
94
92
|
## Features
|
|
95
93
|
|
|
96
94
|
- Single `evaluate()` method — send prompt/response, get verdict
|
|
95
|
+
- Failure mode: `fail-open` or `fail-closed` when API is unreachable
|
|
96
|
+
- Circuit breaker: skips API after consecutive failures, auto-recovers
|
|
97
|
+
- Idempotency: deduplicates retried requests via `x-idempotency-key`
|
|
97
98
|
- Auto-retry with exponential backoff on 5xx and network errors
|
|
98
99
|
- Typed errors: `AuthError`, `RateLimitError`, `FeatureNotAvailableError`, `ChainDepthExceededError`
|
|
99
100
|
- Full TypeScript types for requests and responses
|
|
@@ -107,9 +108,35 @@ const dg = new Driftgard({
|
|
|
107
108
|
baseUrl: "https://api.driftgard.com", // optional
|
|
108
109
|
timeout: 30000, // optional, ms (default 30s)
|
|
109
110
|
maxRetries: 2, // optional (default 2)
|
|
111
|
+
failureMode: "open", // "open" = allow if API down, "closed" = block (default "open")
|
|
112
|
+
circuitBreaker: {
|
|
113
|
+
threshold: 5, // open circuit after 5 consecutive failures (default 5)
|
|
114
|
+
resetTimeoutMs: 30000, // try again after 30s (default 30000)
|
|
115
|
+
},
|
|
110
116
|
});
|
|
111
117
|
```
|
|
112
118
|
|
|
119
|
+
## Failure mode & circuit breaker
|
|
120
|
+
|
|
121
|
+
The SDK never throws on network/server errors during `evaluate()`. Instead, it returns a synthetic response:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const result = await dg.evaluate({ ... });
|
|
125
|
+
|
|
126
|
+
// Check where the decision came from
|
|
127
|
+
console.log(result.decision_source);
|
|
128
|
+
// "policy" — normal API evaluation
|
|
129
|
+
// "failure_mode" — API unreachable, failureMode applied
|
|
130
|
+
// "circuit_open" — circuit breaker open, failureMode applied
|
|
131
|
+
// "idempotency_cache" — duplicate request, cached result returned
|
|
132
|
+
|
|
133
|
+
// Monitor circuit breaker state
|
|
134
|
+
console.log(dg.circuitBreakerState);
|
|
135
|
+
// { state: "closed", failures: 0, openedAt: 0 }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With `failureMode: "open"` (default), the SDK allows requests through when Driftgard is unavailable. With `failureMode: "closed"`, it blocks them with a fallback message.
|
|
139
|
+
|
|
113
140
|
## Error handling
|
|
114
141
|
|
|
115
142
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { DriftgardConfig, EvaluateRequest, EvaluateResponse } from "./types";
|
|
2
|
-
export { DriftgardConfig, EvaluateRequest, EvaluateResponse } from "./types";
|
|
2
|
+
export { DriftgardConfig, EvaluateRequest, EvaluateResponse, CircuitBreakerConfig } from "./types";
|
|
3
3
|
export { Violation, EvaluationResult, FallbackResponse, HitlInfo } from "./types";
|
|
4
4
|
export { DriftgardError, AuthError, RateLimitError, FeatureNotAvailableError, ChainDepthExceededError } from "./errors";
|
|
5
|
+
type CircuitState = "closed" | "open" | "half-open";
|
|
5
6
|
export declare class Driftgard {
|
|
6
7
|
private apiKey;
|
|
7
8
|
private baseUrl;
|
|
8
9
|
private timeout;
|
|
9
10
|
private maxRetries;
|
|
11
|
+
private failureMode;
|
|
12
|
+
private cbThreshold;
|
|
13
|
+
private cbResetMs;
|
|
14
|
+
private cbState;
|
|
15
|
+
private cbFailures;
|
|
16
|
+
private cbOpenedAt;
|
|
10
17
|
constructor(config: DriftgardConfig);
|
|
11
18
|
/**
|
|
12
19
|
* Evaluate a prompt/response pair against your active control pack.
|
|
20
|
+
* Handles circuit breaker and failure mode automatically.
|
|
13
21
|
*/
|
|
14
22
|
evaluate(req: EvaluateRequest): Promise<EvaluateResponse>;
|
|
23
|
+
/** Generate a synthetic response based on failure mode. */
|
|
24
|
+
private syntheticResponse;
|
|
25
|
+
/** Get current circuit breaker state (for observability). */
|
|
26
|
+
get circuitBreakerState(): {
|
|
27
|
+
state: CircuitState;
|
|
28
|
+
failures: number;
|
|
29
|
+
openedAt: number;
|
|
30
|
+
};
|
|
31
|
+
private generateIdempotencyKey;
|
|
15
32
|
private post;
|
|
16
33
|
private delay;
|
|
17
34
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,35 +12,119 @@ const DEFAULT_BASE_URL = "https://api.driftgard.com";
|
|
|
12
12
|
const DEFAULT_TIMEOUT = 30000;
|
|
13
13
|
const DEFAULT_MAX_RETRIES = 2;
|
|
14
14
|
const RETRY_DELAY_MS = 500;
|
|
15
|
+
const DEFAULT_CB_THRESHOLD = 5;
|
|
16
|
+
const DEFAULT_CB_RESET_MS = 30000;
|
|
15
17
|
class Driftgard {
|
|
16
18
|
constructor(config) {
|
|
19
|
+
this.cbState = "closed";
|
|
20
|
+
this.cbFailures = 0;
|
|
21
|
+
this.cbOpenedAt = 0;
|
|
17
22
|
if (!config.apiKey)
|
|
18
23
|
throw new Error("apiKey is required");
|
|
19
24
|
this.apiKey = config.apiKey;
|
|
20
25
|
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
21
26
|
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
22
27
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
28
|
+
this.failureMode = config.failureMode ?? "open";
|
|
29
|
+
this.cbThreshold = config.circuitBreaker?.threshold ?? DEFAULT_CB_THRESHOLD;
|
|
30
|
+
this.cbResetMs = config.circuitBreaker?.resetTimeoutMs ?? DEFAULT_CB_RESET_MS;
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
25
33
|
* Evaluate a prompt/response pair against your active control pack.
|
|
34
|
+
* Handles circuit breaker and failure mode automatically.
|
|
26
35
|
*/
|
|
27
36
|
async evaluate(req) {
|
|
28
|
-
|
|
37
|
+
const idempotencyKey = req.idempotency_key || this.generateIdempotencyKey();
|
|
38
|
+
// Circuit breaker: if open, check if reset timeout has passed
|
|
39
|
+
if (this.cbState === "open") {
|
|
40
|
+
if (Date.now() - this.cbOpenedAt >= this.cbResetMs) {
|
|
41
|
+
this.cbState = "half-open";
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Circuit is open — apply failure mode without calling API
|
|
45
|
+
return this.syntheticResponse(req, "circuit_open");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const result = await this.post("/audit/evaluate", {
|
|
50
|
+
project_id: req.project_id,
|
|
51
|
+
prompt: req.prompt,
|
|
52
|
+
response: req.response,
|
|
53
|
+
model_id: req.model_id,
|
|
54
|
+
timestamp: req.timestamp || new Date().toISOString(),
|
|
55
|
+
...(req.experiment_id ? { experiment_id: req.experiment_id } : {}),
|
|
56
|
+
...(req.session_id ? { session_id: req.session_id } : {}),
|
|
57
|
+
...(req.parent_evaluation_id ? { parent_evaluation_id: req.parent_evaluation_id } : {}),
|
|
58
|
+
...(req.control_pack_id ? { control_pack_id: req.control_pack_id } : {}),
|
|
59
|
+
...(req.control_pack_version ? { control_pack_version: req.control_pack_version } : {}),
|
|
60
|
+
...(req.dry_run ? { dry_run: req.dry_run } : {}),
|
|
61
|
+
...(req.usage ? { usage: req.usage } : {}),
|
|
62
|
+
}, idempotencyKey);
|
|
63
|
+
// Success — reset circuit breaker
|
|
64
|
+
this.cbFailures = 0;
|
|
65
|
+
this.cbState = "closed";
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// Auth, rate limit, chain depth, feature errors are real errors — don't trigger circuit breaker
|
|
70
|
+
if (e instanceof errors_1.AuthError || e instanceof errors_1.RateLimitError ||
|
|
71
|
+
e instanceof errors_1.ChainDepthExceededError || e instanceof errors_1.FeatureNotAvailableError) {
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
// Network/server error — increment circuit breaker
|
|
75
|
+
this.cbFailures++;
|
|
76
|
+
if (this.cbFailures >= this.cbThreshold) {
|
|
77
|
+
this.cbState = "open";
|
|
78
|
+
this.cbOpenedAt = Date.now();
|
|
79
|
+
}
|
|
80
|
+
// Apply failure mode
|
|
81
|
+
return this.syntheticResponse(req, "failure_mode");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Generate a synthetic response based on failure mode. */
|
|
85
|
+
syntheticResponse(req, source) {
|
|
86
|
+
const allowed = this.failureMode === "open";
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
29
89
|
project_id: req.project_id,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
90
|
+
evaluation_id: `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
91
|
+
active_control_pack_id: "",
|
|
92
|
+
active_control_pack_version: 0,
|
|
93
|
+
data_mode: "unknown",
|
|
94
|
+
telemetry_mode: "unknown",
|
|
95
|
+
execution_mode: "sync",
|
|
96
|
+
decision_action: allowed ? "log_only" : "enforce",
|
|
97
|
+
decision_source: source,
|
|
98
|
+
evaluation: {
|
|
99
|
+
allowed,
|
|
100
|
+
risk_score: allowed ? 0 : 10,
|
|
101
|
+
violations: allowed ? [] : [{
|
|
102
|
+
clause_id: "DRIFTGARD_UNAVAILABLE",
|
|
103
|
+
severity: "critical",
|
|
104
|
+
category: "system",
|
|
105
|
+
reason: `Driftgard API unavailable — fail-closed policy applied (${source})`,
|
|
106
|
+
}],
|
|
107
|
+
},
|
|
108
|
+
...(allowed ? {} : {
|
|
109
|
+
fallback: {
|
|
110
|
+
message: "Service temporarily unavailable. Request blocked by fail-closed policy.",
|
|
111
|
+
code: "DRIFTGARD_UNAVAILABLE",
|
|
112
|
+
action: "show_message",
|
|
113
|
+
source,
|
|
114
|
+
escalated: false,
|
|
115
|
+
retry_after_ms: this.cbState === "open" ? this.cbResetMs : null,
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Get current circuit breaker state (for observability). */
|
|
121
|
+
get circuitBreakerState() {
|
|
122
|
+
return { state: this.cbState, failures: this.cbFailures, openedAt: this.cbOpenedAt };
|
|
123
|
+
}
|
|
124
|
+
generateIdempotencyKey() {
|
|
125
|
+
return `idem_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
42
126
|
}
|
|
43
|
-
async post(path, body, attempt = 0) {
|
|
127
|
+
async post(path, body, idempotencyKey, attempt = 0) {
|
|
44
128
|
const controller = new AbortController();
|
|
45
129
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
46
130
|
try {
|
|
@@ -49,6 +133,7 @@ class Driftgard {
|
|
|
49
133
|
headers: {
|
|
50
134
|
"Content-Type": "application/json",
|
|
51
135
|
"x-api-key": this.apiKey,
|
|
136
|
+
...(idempotencyKey ? { "x-idempotency-key": idempotencyKey } : {}),
|
|
52
137
|
},
|
|
53
138
|
body: JSON.stringify(body),
|
|
54
139
|
signal: controller.signal,
|
|
@@ -72,7 +157,7 @@ class Driftgard {
|
|
|
72
157
|
// Retry on 5xx
|
|
73
158
|
if (res.status >= 500 && attempt < this.maxRetries) {
|
|
74
159
|
await this.delay(RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
75
|
-
return this.post(path, body, attempt + 1);
|
|
160
|
+
return this.post(path, body, idempotencyKey, attempt + 1);
|
|
76
161
|
}
|
|
77
162
|
const payload = await res.json();
|
|
78
163
|
if (!res.ok) {
|
|
@@ -86,7 +171,7 @@ class Driftgard {
|
|
|
86
171
|
// Network error — retry
|
|
87
172
|
if (attempt < this.maxRetries) {
|
|
88
173
|
await this.delay(RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
89
|
-
return this.post(path, body, attempt + 1);
|
|
174
|
+
return this.post(path, body, idempotencyKey, attempt + 1);
|
|
90
175
|
}
|
|
91
176
|
const msg = e instanceof Error ? e.message : String(e);
|
|
92
177
|
throw new errors_1.DriftgardError(msg, 0, "network_error");
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
export interface CircuitBreakerConfig {
|
|
2
|
+
/** Number of consecutive failures before opening the circuit. Default 5. */
|
|
3
|
+
threshold?: number;
|
|
4
|
+
/** Milliseconds to wait before trying again after circuit opens. Default 30000. */
|
|
5
|
+
resetTimeoutMs?: number;
|
|
6
|
+
}
|
|
1
7
|
export interface DriftgardConfig {
|
|
2
8
|
apiKey: string;
|
|
3
9
|
baseUrl?: string;
|
|
4
10
|
timeout?: number;
|
|
5
11
|
maxRetries?: number;
|
|
12
|
+
/** What to do when Driftgard API is unreachable. "open" = allow, "closed" = block. Default "open". */
|
|
13
|
+
failureMode?: "open" | "closed";
|
|
14
|
+
/** Circuit breaker config. Skips API calls after consecutive failures. */
|
|
15
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
6
16
|
}
|
|
7
17
|
export interface EvaluateRequest {
|
|
8
18
|
project_id: string;
|
|
@@ -16,6 +26,8 @@ export interface EvaluateRequest {
|
|
|
16
26
|
control_pack_id?: string;
|
|
17
27
|
control_pack_version?: number;
|
|
18
28
|
dry_run?: boolean;
|
|
29
|
+
/** Caller-provided idempotency key. If omitted, the SDK generates one. */
|
|
30
|
+
idempotency_key?: string;
|
|
19
31
|
usage?: {
|
|
20
32
|
prompt_tokens?: number;
|
|
21
33
|
completion_tokens?: number;
|
|
@@ -34,6 +46,8 @@ export interface EvaluationResult {
|
|
|
34
46
|
allowed: boolean;
|
|
35
47
|
risk_score: number;
|
|
36
48
|
violations: Violation[];
|
|
49
|
+
/** Original policy decision before execution_mode override (only present when overridden). */
|
|
50
|
+
policy_allowed?: boolean;
|
|
37
51
|
flags?: {
|
|
38
52
|
pii_detected?: boolean;
|
|
39
53
|
pii_in_prompt?: boolean;
|
|
@@ -70,6 +84,12 @@ export interface EvaluateResponse {
|
|
|
70
84
|
active_control_pack_version: number;
|
|
71
85
|
data_mode: string;
|
|
72
86
|
telemetry_mode: string;
|
|
87
|
+
/** Server-side execution mode: sync, async, or hybrid. */
|
|
88
|
+
execution_mode?: string;
|
|
89
|
+
/** How the decision was applied: "enforce" or "log_only". */
|
|
90
|
+
decision_action?: string;
|
|
91
|
+
/** Where the decision came from: "policy", "failure_mode", "circuit_open". */
|
|
92
|
+
decision_source?: string;
|
|
73
93
|
dry_run?: boolean;
|
|
74
94
|
experiment_id?: string;
|
|
75
95
|
session_id?: string;
|
package/package.json
CHANGED