@driftgard/node 1.5.0 → 1.6.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
@@ -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,28 @@ 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.
91
+
92
+ ## Cost alerts
93
+
94
+ When cost alerting is enabled on your project, the response includes a `cost_alert` field if a threshold is exceeded:
95
+
96
+ ```typescript
97
+ const result = await dg.evaluate({ ... });
98
+
99
+ if (result.cost_alert) {
100
+ console.warn(`Cost alert: ${result.cost_alert.scope} spend $${result.cost_alert.actual_usd} exceeds $${result.cost_alert.threshold_usd}`);
101
+ // Throttle the agent, notify the user, etc.
102
+ }
92
103
  ```
93
104
 
105
+ Configure thresholds in Settings — per-project, per-model, or per-session. Session-scoped alerts catch runaway agent loops in real-time.
106
+
94
107
  ## Features
95
108
 
96
109
  - Single `evaluate()` method — send prompt/response, get verdict
110
+ - Failure mode: `fail-open` or `fail-closed` when API is unreachable
111
+ - Circuit breaker: skips API after consecutive failures, auto-recovers
112
+ - Idempotency: deduplicates retried requests via `x-idempotency-key`
97
113
  - Auto-retry with exponential backoff on 5xx and network errors
98
114
  - Typed errors: `AuthError`, `RateLimitError`, `FeatureNotAvailableError`, `ChainDepthExceededError`
99
115
  - Full TypeScript types for requests and responses
@@ -107,9 +123,35 @@ const dg = new Driftgard({
107
123
  baseUrl: "https://api.driftgard.com", // optional
108
124
  timeout: 30000, // optional, ms (default 30s)
109
125
  maxRetries: 2, // optional (default 2)
126
+ failureMode: "open", // "open" = allow if API down, "closed" = block (default "open")
127
+ circuitBreaker: {
128
+ threshold: 5, // open circuit after 5 consecutive failures (default 5)
129
+ resetTimeoutMs: 30000, // try again after 30s (default 30000)
130
+ },
110
131
  });
111
132
  ```
112
133
 
134
+ ## Failure mode & circuit breaker
135
+
136
+ The SDK never throws on network/server errors during `evaluate()`. Instead, it returns a synthetic response:
137
+
138
+ ```typescript
139
+ const result = await dg.evaluate({ ... });
140
+
141
+ // Check where the decision came from
142
+ console.log(result.decision_source);
143
+ // "policy" — normal API evaluation
144
+ // "failure_mode" — API unreachable, failureMode applied
145
+ // "circuit_open" — circuit breaker open, failureMode applied
146
+ // "idempotency_cache" — duplicate request, cached result returned
147
+
148
+ // Monitor circuit breaker state
149
+ console.log(dg.circuitBreakerState);
150
+ // { state: "closed", failures: 0, openedAt: 0 }
151
+ ```
152
+
153
+ With `failureMode: "open"` (default), the SDK allows requests through when Driftgard is unavailable. With `failureMode: "closed"`, it blocks them with a fallback message.
154
+
113
155
  ## Error handling
114
156
 
115
157
  ```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
- return this.post("/audit/evaluate", {
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
- prompt: req.prompt,
31
- response: req.response,
32
- model_id: req.model_id,
33
- timestamp: req.timestamp || new Date().toISOString(),
34
- ...(req.experiment_id ? { experiment_id: req.experiment_id } : {}),
35
- ...(req.session_id ? { session_id: req.session_id } : {}),
36
- ...(req.parent_evaluation_id ? { parent_evaluation_id: req.parent_evaluation_id } : {}),
37
- ...(req.control_pack_id ? { control_pack_id: req.control_pack_id } : {}),
38
- ...(req.control_pack_version ? { control_pack_version: req.control_pack_version } : {}),
39
- ...(req.dry_run ? { dry_run: req.dry_run } : {}),
40
- ...(req.usage ? { usage: req.usage } : {}),
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;
@@ -83,4 +103,13 @@ export interface EvaluateResponse {
83
103
  hitl?: HitlInfo;
84
104
  evaluation: EvaluationResult;
85
105
  fallback?: FallbackResponse;
106
+ cost_alert?: {
107
+ type: string;
108
+ scope: string;
109
+ session_id?: string;
110
+ model_id?: string;
111
+ window: string;
112
+ threshold_usd: number;
113
+ actual_usd: number;
114
+ };
86
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftgard/node",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Official Driftgard Node.js SDK — evaluate LLM interactions against your compliance policy",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",