@driftgard/node 1.11.0 → 1.12.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
@@ -35,6 +35,94 @@ if (result.evaluation.allowed) {
35
35
  }
36
36
  ```
37
37
 
38
+ ## Local evaluation mode (beta)
39
+
40
+ For privacy-sensitive deployments — mental health, clinical, healthcare — where no patient data can leave your environment. The SDK evaluates locally via a compiled WebAssembly engine. No prompt, response, or conversation content is sent to DriftGard.
41
+
42
+ ### Local mode — zero network calls after init
43
+
44
+ ```typescript
45
+ import { Driftgard } from "@driftgard/node";
46
+
47
+ const dg = new Driftgard({
48
+ apiKey: process.env.DRIFTGARD_API_KEY,
49
+ mode: "local",
50
+ projectId: "your-project-id",
51
+ });
52
+
53
+ // Fetches the active control pack (one-time network call)
54
+ await dg.init();
55
+
56
+ // All evaluate() calls now run locally via WASM
57
+ const result = await dg.evaluate({
58
+ project_id: "your-project-id",
59
+ prompt: "I feel really anxious today",
60
+ response: "I hear you. Would you like to talk about what's triggering it?",
61
+ model_id: "gpt-4o",
62
+ });
63
+
64
+ console.log(result.evaluation.allowed); // true
65
+ console.log(result.decision_source); // "local"
66
+ console.log(result.data_mode); // "local"
67
+
68
+ // Clean up when done
69
+ dg.destroy();
70
+ ```
71
+
72
+ ### Local-with-audit mode — local evaluation, metadata reporting
73
+
74
+ Same as local mode, but posts verdict metadata (no prompt/response) to DriftGard for compliance dashboards:
75
+
76
+ ```typescript
77
+ const dg = new Driftgard({
78
+ apiKey: process.env.DRIFTGARD_API_KEY,
79
+ mode: "local-with-audit",
80
+ projectId: "your-project-id",
81
+ });
82
+
83
+ await dg.init();
84
+
85
+ const result = await dg.evaluate({
86
+ project_id: "your-project-id",
87
+ prompt: "Patient conversation content...",
88
+ response: "Clinical response...",
89
+ model_id: "gpt-4o",
90
+ agent_role: "therapist_agent",
91
+ });
92
+
93
+ // Evaluation ran locally — no patient data sent
94
+ // Only verdict metadata reported: evaluation_id, timestamp, allowed, risk_score,
95
+ // violation clause IDs, severities, model_id, session_id, agent_role
96
+ ```
97
+
98
+ ### Control pack sync
99
+
100
+ On `init()`, the SDK fetches the active control pack for your project and caches it in memory. A background refresh runs every 60 seconds (configurable). If a refresh fails, the SDK uses the last-known-good pack and marks it as stale.
101
+
102
+ ```typescript
103
+ const dg = new Driftgard({
104
+ apiKey: process.env.DRIFTGARD_API_KEY,
105
+ mode: "local",
106
+ projectId: "your-project-id",
107
+ refreshIntervalMs: 120_000, // refresh every 2 minutes (default 60s)
108
+ onControlPackRefresh: (event) => {
109
+ if (event.success) {
110
+ console.log(`Control pack refreshed: v${event.version}`);
111
+ } else {
112
+ console.warn(`Refresh failed: ${event.error}${event.stale ? " (using stale pack)" : ""}`);
113
+ }
114
+ },
115
+ });
116
+ ```
117
+
118
+ ### When to use each mode
119
+
120
+ | Mode | Data sent to DriftGard | Use case |
121
+ |---|---|---|
122
+ | `remote` (default) | Prompt + response + verdict | Standard deployment, full dashboard visibility |
123
+ | `local` | Control pack fetch only (on init) | Maximum privacy — mental health, clinical, sovereign |
124
+ | `local-with-audit` | Control pack fetch + verdict metadata | Privacy with compliance reporting — healthcare, regulated |
125
+
38
126
  ## Conversation tracking
39
127
 
40
128
  Link evaluations within an agent session using `session_id` and `parent_evaluation_id`:
@@ -73,6 +161,32 @@ const result = await dg.evaluate({
73
161
 
74
162
  Agent identity fields are stored on the evaluation record and visible in the Live Activity detail dialog. The `on_behalf_of` field tracks which end-user triggered the agent action. The `parent_agent_id` field identifies which orchestrator agent delegated to this one in multi-agent systems.
75
163
 
164
+ ## Jurisdiction-scoped rules
165
+
166
+ Control pack rules can be scoped to specific jurisdictions. Pass the user's jurisdiction in the evaluate request — only matching rules (plus global rules) will fire:
167
+
168
+ ```typescript
169
+ const result = await dg.evaluate({
170
+ project_id: "your-project-id",
171
+ prompt: "What odds can I get?",
172
+ response: "Current odds for the Melbourne Cup are...",
173
+ model_id: "gpt-4o",
174
+ jurisdiction: "AU-VIC", // only VIC + global rules fire
175
+ });
176
+ ```
177
+
178
+ Rules without a `jurisdictions` field are global — they fire for all requests regardless of jurisdiction. Rules with `jurisdictions: ["AU-VIC", "AU-NSW"]` only fire when the request's `jurisdiction` matches.
179
+
180
+ Supported jurisdiction codes include:
181
+ - Australia: `AU`, `AU-ACT`, `AU-NSW`, `AU-NT`, `AU-QLD`, `AU-SA`, `AU-TAS`, `AU-VIC`, `AU-WA`
182
+ - United States: `US`, `US-AL`, `US-AK`, `US-AZ`, `US-AR`, `US-CA`, `US-CO`, `US-CT`, `US-DE`, `US-FL`, `US-GA`, `US-HI`, `US-ID`, `US-IL`, `US-IN`, `US-IA`, `US-KS`, `US-KY`, `US-LA`, `US-ME`, `US-MD`, `US-MA`, `US-MI`, `US-MN`, `US-MS`, `US-MO`, `US-MT`, `US-NE`, `US-NV`, `US-NH`, `US-NJ`, `US-NM`, `US-NY`, `US-NC`, `US-ND`, `US-OH`, `US-OK`, `US-OR`, `US-PA`, `US-RI`, `US-SC`, `US-SD`, `US-TN`, `US-TX`, `US-UT`, `US-VT`, `US-VA`, `US-WA`, `US-WV`, `US-WI`, `US-WY`, `US-DC`
183
+ - United Kingdom: `GB`, `GB-ENG`, `GB-SCT`, `GB-WLS`, `GB-NIR`
184
+ - Europe: `EU`, `DE`, `FR`, `IE`, `NL`, `ES`, `IT`, `SE`
185
+ - Asia-Pacific: `NZ`, `SG`, `JP`, `IN`, `HK`
186
+ - Other: `CA`, `BR`, `ZA`, `AE`, `SA`
187
+
188
+ Custom codes are also supported — use any string your team agrees on.
189
+
76
190
  ### Per-tool identity rules
77
191
 
78
192
  Control packs support `identity_rules` on each tool — restricting which agents, roles, users, or parent agents can call it. Rules use OR logic across entries and AND logic within each entry:
@@ -204,13 +318,16 @@ Supported: comparisons (`< > <= >= === !==`), logical (`&& || !`), arithmetic (`
204
318
  ## Features
205
319
 
206
320
  - Single `evaluate()` method — send prompt/response, get verdict
321
+ - Local evaluation mode (beta) — evaluate via WASM, no data leaves your environment
322
+ - Three modes: `remote`, `local`, `local-with-audit`
323
+ - Control pack sync with background refresh and stale-pack fallback
207
324
  - Failure mode: `fail-open` or `fail-closed` when API is unreachable
208
325
  - Circuit breaker: skips API after consecutive failures, auto-recovers
209
326
  - Idempotency: deduplicates retried requests via `x-idempotency-key`
210
327
  - Auto-retry with exponential backoff on 5xx and network errors
211
328
  - Typed errors: `AuthError`, `RateLimitError`, `FeatureNotAvailableError`, `ChainDepthExceededError`
212
329
  - Full TypeScript types for requests and responses
213
- - Zero dependencies (uses native `fetch`)
330
+ - Zero runtime dependencies (uses native `fetch`, WASM bundled)
214
331
 
215
332
  ## Configuration
216
333
 
@@ -225,7 +342,18 @@ const dg = new Driftgard({
225
342
  threshold: 5, // open circuit after 5 consecutive failures (default 5)
226
343
  resetTimeoutMs: 30000, // try again after 30s (default 30000)
227
344
  },
345
+
346
+ // Local mode options (beta)
347
+ mode: "remote", // "remote" | "local" | "local-with-audit" (default "remote")
348
+ projectId: "your-project-id", // required for local/local-with-audit modes
349
+ refreshIntervalMs: 60000, // control pack refresh interval, ms (default 60s)
350
+ onControlPackRefresh: (e) => {}, // callback on refresh success/failure
228
351
  });
352
+
353
+ // For local modes, call init() to fetch the control pack
354
+ if (dg.mode !== "remote") {
355
+ await dg.init();
356
+ }
229
357
  ```
230
358
 
231
359
  ## Failure mode & circuit breaker
@@ -238,6 +366,8 @@ const result = await dg.evaluate({ ... });
238
366
  // Check where the decision came from
239
367
  console.log(result.decision_source);
240
368
  // "policy" — normal API evaluation
369
+ // "local" — local WASM evaluation
370
+ // "local_stale" — local evaluation with stale control pack
241
371
  // "failure_mode" — API unreachable, failureMode applied
242
372
  // "circuit_open" — circuit breaker open, failureMode applied
243
373
  // "idempotency_cache" — duplicate request, cached result returned
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Control Pack Cache — fetches and caches the active control pack for local evaluation.
3
+ *
4
+ * On init: fetches the active control pack from the DriftGard API.
5
+ * Background: refreshes on a configurable interval.
6
+ * Stale pack: uses last-known-good if refresh fails. Fails closed if no pack exists.
7
+ */
8
+ export interface ControlPackCacheConfig {
9
+ apiKey: string;
10
+ baseUrl: string;
11
+ projectId: string;
12
+ /** Refresh interval in milliseconds. Default 60000 (1 minute). */
13
+ refreshIntervalMs?: number;
14
+ /** Timeout for API calls in milliseconds. Default 10000. */
15
+ timeout?: number;
16
+ /** Called when the control pack is refreshed or refresh fails. */
17
+ onRefresh?: (event: {
18
+ success: boolean;
19
+ version?: number;
20
+ error?: string;
21
+ stale?: boolean;
22
+ }) => void;
23
+ }
24
+ export declare class ControlPackCache {
25
+ private config;
26
+ private controlPack;
27
+ private controlPackId;
28
+ private controlPackVersion;
29
+ private lastRefreshedAt;
30
+ private refreshTimer;
31
+ private stale;
32
+ constructor(config: ControlPackCacheConfig);
33
+ /** Fetch the active control pack. Must be called before evaluating. */
34
+ init(): Promise<void>;
35
+ /** Stop background refresh. */
36
+ destroy(): void;
37
+ /** Get the cached control pack. Throws if no pack is available. */
38
+ getControlPack(): Record<string, unknown>;
39
+ /** Get cache metadata for audit reporting. */
40
+ getMeta(): {
41
+ control_pack_id: string | null;
42
+ version: number;
43
+ stale: boolean;
44
+ last_refreshed_at: string | null;
45
+ };
46
+ /** Refresh the control pack from the API. */
47
+ private refresh;
48
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * Control Pack Cache — fetches and caches the active control pack for local evaluation.
4
+ *
5
+ * On init: fetches the active control pack from the DriftGard API.
6
+ * Background: refreshes on a configurable interval.
7
+ * Stale pack: uses last-known-good if refresh fails. Fails closed if no pack exists.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ControlPackCache = void 0;
11
+ class ControlPackCache {
12
+ constructor(config) {
13
+ this.controlPack = null;
14
+ this.controlPackId = null;
15
+ this.controlPackVersion = 0;
16
+ this.lastRefreshedAt = null;
17
+ this.refreshTimer = null;
18
+ this.stale = false;
19
+ this.config = config;
20
+ }
21
+ /** Fetch the active control pack. Must be called before evaluating. */
22
+ async init() {
23
+ await this.refresh();
24
+ // Start background refresh
25
+ const interval = this.config.refreshIntervalMs ?? 60000;
26
+ if (interval > 0) {
27
+ this.refreshTimer = setInterval(() => {
28
+ this.refresh().catch(() => { });
29
+ }, interval);
30
+ // Don't block process exit
31
+ if (this.refreshTimer && typeof this.refreshTimer === "object" && "unref" in this.refreshTimer) {
32
+ this.refreshTimer.unref();
33
+ }
34
+ }
35
+ }
36
+ /** Stop background refresh. */
37
+ destroy() {
38
+ if (this.refreshTimer) {
39
+ clearInterval(this.refreshTimer);
40
+ this.refreshTimer = null;
41
+ }
42
+ }
43
+ /** Get the cached control pack. Throws if no pack is available. */
44
+ getControlPack() {
45
+ if (!this.controlPack) {
46
+ throw new Error("No control pack available. Call init() first or check that the project has an active control pack.");
47
+ }
48
+ return this.controlPack;
49
+ }
50
+ /** Get cache metadata for audit reporting. */
51
+ getMeta() {
52
+ return {
53
+ control_pack_id: this.controlPackId,
54
+ version: this.controlPackVersion,
55
+ stale: this.stale,
56
+ last_refreshed_at: this.lastRefreshedAt,
57
+ };
58
+ }
59
+ /** Refresh the control pack from the API. */
60
+ async refresh() {
61
+ const { apiKey, baseUrl, projectId, timeout = 10000 } = this.config;
62
+ const controller = new AbortController();
63
+ const timer = setTimeout(() => controller.abort(), timeout);
64
+ try {
65
+ const res = await fetch(`${baseUrl}/audit/cli/control-packs/active?project_id=${encodeURIComponent(projectId)}`, {
66
+ method: "GET",
67
+ headers: {
68
+ "x-api-key": apiKey,
69
+ },
70
+ signal: controller.signal,
71
+ });
72
+ if (!res.ok) {
73
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
74
+ }
75
+ const data = await res.json();
76
+ const cp = data.control_pack;
77
+ if (!cp) {
78
+ throw new Error("No active control pack found for this project");
79
+ }
80
+ this.controlPack = cp;
81
+ this.controlPackId = String(cp.control_pack_id || "");
82
+ this.controlPackVersion = Number(cp.version || 0);
83
+ this.lastRefreshedAt = new Date().toISOString();
84
+ this.stale = false;
85
+ this.config.onRefresh?.({
86
+ success: true,
87
+ version: this.controlPackVersion,
88
+ });
89
+ }
90
+ catch (e) {
91
+ const msg = e instanceof Error ? e.message : String(e);
92
+ // If we have a cached pack, mark as stale but keep using it
93
+ if (this.controlPack) {
94
+ this.stale = true;
95
+ this.config.onRefresh?.({
96
+ success: false,
97
+ error: msg,
98
+ stale: true,
99
+ version: this.controlPackVersion,
100
+ });
101
+ }
102
+ else {
103
+ // No cached pack — this is fatal for local mode
104
+ this.config.onRefresh?.({
105
+ success: false,
106
+ error: msg,
107
+ });
108
+ throw new Error(`Failed to fetch control pack: ${msg}`);
109
+ }
110
+ }
111
+ finally {
112
+ clearTimeout(timer);
113
+ }
114
+ }
115
+ }
116
+ exports.ControlPackCache = ControlPackCache;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import { DriftgardConfig, EvaluateRequest, EvaluateResponse, ToolCallRequest, Ou
2
2
  export { DriftgardConfig, EvaluateRequest, EvaluateResponse, CircuitBreakerConfig, ToolCallRequest, OutcomeRequest } from "./types";
3
3
  export { Violation, EvaluationResult, FallbackResponse, HitlInfo } from "./types";
4
4
  export { DriftgardError, AuthError, RateLimitError, FeatureNotAvailableError, ChainDepthExceededError } from "./errors";
5
+ export { evaluateLocal } from "./local-evaluator";
6
+ export { ControlPackCache } from "./control-pack-cache";
5
7
  type CircuitState = "closed" | "open" | "half-open";
6
8
  export declare class Driftgard {
7
9
  private apiKey;
@@ -9,17 +11,38 @@ export declare class Driftgard {
9
11
  private timeout;
10
12
  private maxRetries;
11
13
  private failureMode;
14
+ private mode;
12
15
  private cbThreshold;
13
16
  private cbResetMs;
14
17
  private cbState;
15
18
  private cbFailures;
16
19
  private cbOpenedAt;
20
+ private cpCache;
21
+ private initialized;
17
22
  constructor(config: DriftgardConfig);
23
+ /**
24
+ * Initialize the SDK. Required for local and local-with-audit modes.
25
+ * Fetches the active control pack and starts background refresh.
26
+ * No-op for remote mode.
27
+ */
28
+ init(): Promise<void>;
29
+ /**
30
+ * Stop background refresh and clean up resources.
31
+ */
32
+ destroy(): void;
18
33
  /**
19
34
  * Evaluate a prompt/response pair against your active control pack.
20
- * Handles circuit breaker and failure mode automatically.
35
+ * In remote mode: sends to DriftGard API.
36
+ * In local mode: evaluates via WASM — no data leaves your environment.
37
+ * In local-with-audit mode: evaluates locally, reports verdict metadata to DriftGard.
21
38
  */
22
39
  evaluate(req: EvaluateRequest): Promise<EvaluateResponse>;
40
+ /** Local evaluation via WASM. */
41
+ private evaluateLocally;
42
+ /** Report verdict metadata to DriftGard (no prompt/response content). */
43
+ private reportAuditMetadata;
44
+ /** Remote evaluation via DriftGard API. */
45
+ private evaluateRemotely;
23
46
  /** Generate a synthetic response based on failure mode. */
24
47
  private syntheticResponse;
25
48
  /**
package/dist/index.js CHANGED
@@ -1,13 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Driftgard = exports.ChainDepthExceededError = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
3
+ exports.Driftgard = exports.ControlPackCache = exports.evaluateLocal = exports.ChainDepthExceededError = exports.FeatureNotAvailableError = exports.RateLimitError = exports.AuthError = exports.DriftgardError = void 0;
4
4
  const errors_1 = require("./errors");
5
+ const local_evaluator_1 = require("./local-evaluator");
6
+ const control_pack_cache_1 = require("./control-pack-cache");
5
7
  var errors_2 = require("./errors");
6
8
  Object.defineProperty(exports, "DriftgardError", { enumerable: true, get: function () { return errors_2.DriftgardError; } });
7
9
  Object.defineProperty(exports, "AuthError", { enumerable: true, get: function () { return errors_2.AuthError; } });
8
10
  Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_2.RateLimitError; } });
9
11
  Object.defineProperty(exports, "FeatureNotAvailableError", { enumerable: true, get: function () { return errors_2.FeatureNotAvailableError; } });
10
12
  Object.defineProperty(exports, "ChainDepthExceededError", { enumerable: true, get: function () { return errors_2.ChainDepthExceededError; } });
13
+ var local_evaluator_2 = require("./local-evaluator");
14
+ Object.defineProperty(exports, "evaluateLocal", { enumerable: true, get: function () { return local_evaluator_2.evaluateLocal; } });
15
+ var control_pack_cache_2 = require("./control-pack-cache");
16
+ Object.defineProperty(exports, "ControlPackCache", { enumerable: true, get: function () { return control_pack_cache_2.ControlPackCache; } });
11
17
  const DEFAULT_BASE_URL = "https://api.driftgard.com";
12
18
  const DEFAULT_TIMEOUT = 30000;
13
19
  const DEFAULT_MAX_RETRIES = 2;
@@ -19,6 +25,9 @@ class Driftgard {
19
25
  this.cbState = "closed";
20
26
  this.cbFailures = 0;
21
27
  this.cbOpenedAt = 0;
28
+ // Local mode
29
+ this.cpCache = null;
30
+ this.initialized = false;
22
31
  if (!config.apiKey)
23
32
  throw new Error("apiKey is required");
24
33
  this.apiKey = config.apiKey;
@@ -26,14 +35,138 @@ class Driftgard {
26
35
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
27
36
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
28
37
  this.failureMode = config.failureMode ?? "open";
38
+ this.mode = config.mode ?? "remote";
29
39
  this.cbThreshold = config.circuitBreaker?.threshold ?? DEFAULT_CB_THRESHOLD;
30
40
  this.cbResetMs = config.circuitBreaker?.resetTimeoutMs ?? DEFAULT_CB_RESET_MS;
41
+ if (this.mode !== "remote") {
42
+ if (!config.projectId)
43
+ throw new Error("projectId is required for local mode");
44
+ this.cpCache = new control_pack_cache_1.ControlPackCache({
45
+ apiKey: this.apiKey,
46
+ baseUrl: this.baseUrl,
47
+ projectId: config.projectId,
48
+ refreshIntervalMs: config.refreshIntervalMs,
49
+ timeout: this.timeout,
50
+ onRefresh: config.onControlPackRefresh,
51
+ });
52
+ }
53
+ }
54
+ /**
55
+ * Initialize the SDK. Required for local and local-with-audit modes.
56
+ * Fetches the active control pack and starts background refresh.
57
+ * No-op for remote mode.
58
+ */
59
+ async init() {
60
+ if (this.cpCache) {
61
+ await this.cpCache.init();
62
+ }
63
+ this.initialized = true;
64
+ }
65
+ /**
66
+ * Stop background refresh and clean up resources.
67
+ */
68
+ destroy() {
69
+ this.cpCache?.destroy();
31
70
  }
32
71
  /**
33
72
  * Evaluate a prompt/response pair against your active control pack.
34
- * Handles circuit breaker and failure mode automatically.
73
+ * In remote mode: sends to DriftGard API.
74
+ * In local mode: evaluates via WASM — no data leaves your environment.
75
+ * In local-with-audit mode: evaluates locally, reports verdict metadata to DriftGard.
35
76
  */
36
77
  async evaluate(req) {
78
+ if (this.mode !== "remote") {
79
+ return this.evaluateLocally(req);
80
+ }
81
+ return this.evaluateRemotely(req);
82
+ }
83
+ /** Local evaluation via WASM. */
84
+ async evaluateLocally(req) {
85
+ if (!this.cpCache)
86
+ throw new Error("Local mode not configured");
87
+ if (!this.initialized)
88
+ throw new Error("Call init() before evaluate() in local mode");
89
+ const cp = this.cpCache.getControlPack();
90
+ const meta = this.cpCache.getMeta();
91
+ const timestamp = req.timestamp || new Date().toISOString();
92
+ const evaluationId = `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
93
+ // Build the WASM request
94
+ const wasmRequest = {
95
+ prompt: req.prompt || "",
96
+ response: req.response || "",
97
+ model_id: req.model_id,
98
+ ...(req.eval_mode ? { eval_mode: req.eval_mode } : {}),
99
+ ...(req.tool_call ? { tool_call: req.tool_call } : {}),
100
+ ...(req.agent_id ? { agent_id: req.agent_id } : {}),
101
+ ...(req.agent_role ? { agent_role: req.agent_role } : {}),
102
+ ...(req.on_behalf_of ? { on_behalf_of: req.on_behalf_of } : {}),
103
+ ...(req.parent_agent_id ? { parent_agent_id: req.parent_agent_id } : {}),
104
+ ...(req.jurisdiction ? { jurisdiction: req.jurisdiction } : {}),
105
+ };
106
+ const verdict = (0, local_evaluator_1.evaluateLocal)(cp, wasmRequest);
107
+ const response = {
108
+ ok: true,
109
+ project_id: req.project_id,
110
+ evaluation_id: evaluationId,
111
+ active_control_pack_id: meta.control_pack_id || "",
112
+ active_control_pack_version: meta.version,
113
+ data_mode: "local",
114
+ telemetry_mode: this.mode === "local-with-audit" ? "metadata_only" : "none",
115
+ execution_mode: "local",
116
+ decision_action: verdict.allowed ? "log_only" : "enforce",
117
+ decision_source: meta.stale ? "local_stale" : "local",
118
+ evaluation: {
119
+ allowed: verdict.allowed,
120
+ risk_score: verdict.risk_score,
121
+ violations: verdict.violations,
122
+ flags: {
123
+ pii_detected: verdict.flags.pii_detected,
124
+ secret_detected: verdict.flags.secret_detected,
125
+ adversarial_input: verdict.flags.adversarial_input,
126
+ meta_bypass_detected: verdict.flags.meta_bypass_detected,
127
+ hard_blocked: verdict.flags.hard_blocked,
128
+ action_blocked: verdict.flags.action_blocked,
129
+ dlp_findings_count: verdict.flags.dlp_findings_count,
130
+ },
131
+ },
132
+ ...(req.eval_mode ? { eval_mode: req.eval_mode } : {}),
133
+ ...(req.tool_call ? { tool_call: req.tool_call } : {}),
134
+ ...(req.session_id ? { session_id: req.session_id } : {}),
135
+ ...(req.usage ? { usage: req.usage } : {}),
136
+ };
137
+ // local-with-audit: report verdict metadata (no prompt/response)
138
+ if (this.mode === "local-with-audit") {
139
+ this.reportAuditMetadata({
140
+ project_id: req.project_id,
141
+ evaluation_id: evaluationId,
142
+ timestamp,
143
+ model_id: req.model_id,
144
+ control_pack_id: meta.control_pack_id || "",
145
+ control_pack_version: meta.version,
146
+ allowed: verdict.allowed,
147
+ risk_score: verdict.risk_score,
148
+ violations: verdict.violations.map(v => ({ clause_id: v.clause_id, severity: v.severity, category: v.category })),
149
+ eval_mode: req.eval_mode,
150
+ session_id: req.session_id,
151
+ agent_id: req.agent_id,
152
+ agent_role: req.agent_role,
153
+ jurisdiction: req.jurisdiction,
154
+ stale_pack: meta.stale,
155
+ }).catch(() => { }); // fire-and-forget
156
+ }
157
+ return response;
158
+ }
159
+ /** Report verdict metadata to DriftGard (no prompt/response content). */
160
+ async reportAuditMetadata(meta) {
161
+ try {
162
+ await this.post("/audit/local-verdict", meta);
163
+ }
164
+ catch {
165
+ // Non-fatal — local evaluation still succeeded
166
+ }
167
+ }
168
+ /** Remote evaluation via DriftGard API. */
169
+ async evaluateRemotely(req) {
37
170
  const idempotencyKey = req.idempotency_key || this.generateIdempotencyKey();
38
171
  // Circuit breaker: if open, check if reset timeout has passed
39
172
  if (this.cbState === "open") {
@@ -66,6 +199,7 @@ class Driftgard {
66
199
  ...(req.on_behalf_of ? { on_behalf_of: req.on_behalf_of } : {}),
67
200
  ...(req.parent_agent_id ? { parent_agent_id: req.parent_agent_id } : {}),
68
201
  ...(req.usage ? { usage: req.usage } : {}),
202
+ ...(req.jurisdiction ? { jurisdiction: req.jurisdiction } : {}),
69
203
  }, idempotencyKey);
70
204
  // Success — reset circuit breaker
71
205
  this.cbFailures = 0;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Local Evaluator — runs the DriftGard evaluation engine locally via WASM.
3
+ * No prompt/response data leaves the customer environment.
4
+ */
5
+ export interface LocalVerdict {
6
+ allowed: boolean;
7
+ risk_score: number;
8
+ violations: Array<{
9
+ clause_id: string;
10
+ severity: string;
11
+ category: string;
12
+ reason: string;
13
+ matched_pattern?: string;
14
+ }>;
15
+ flags: {
16
+ pii_detected: boolean;
17
+ pii_in_params: boolean;
18
+ secret_detected: boolean;
19
+ secret_in_params: boolean;
20
+ adversarial_input: boolean;
21
+ meta_bypass_detected: boolean;
22
+ hard_blocked: boolean;
23
+ action_blocked: boolean;
24
+ dlp_findings_count: number;
25
+ };
26
+ }
27
+ /**
28
+ * Run evaluation locally via WASM.
29
+ * @param controlPack - The cached control pack object
30
+ * @param request - The evaluation request (prompt, response, tool_call, identity, etc.)
31
+ * @returns The verdict — same shape as the server-side evaluation result
32
+ */
33
+ export declare function evaluateLocal(controlPack: Record<string, unknown>, request: Record<string, unknown>): LocalVerdict;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /**
3
+ * Local Evaluator — runs the DriftGard evaluation engine locally via WASM.
4
+ * No prompt/response data leaves the customer environment.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.evaluateLocal = evaluateLocal;
8
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
9
+ const wasmModule = require("./wasm/driftgard_evaluator.js");
10
+ /**
11
+ * Run evaluation locally via WASM.
12
+ * @param controlPack - The cached control pack object
13
+ * @param request - The evaluation request (prompt, response, tool_call, identity, etc.)
14
+ * @returns The verdict — same shape as the server-side evaluation result
15
+ */
16
+ function evaluateLocal(controlPack, request) {
17
+ const resultJson = wasmModule.evaluate(JSON.stringify(controlPack), JSON.stringify(request));
18
+ return JSON.parse(resultJson);
19
+ }
package/dist/types.d.ts CHANGED
@@ -13,6 +13,24 @@ export interface DriftgardConfig {
13
13
  failureMode?: "open" | "closed";
14
14
  /** Circuit breaker config. Skips API calls after consecutive failures. */
15
15
  circuitBreaker?: CircuitBreakerConfig;
16
+ /**
17
+ * Evaluation mode:
18
+ * - "remote" (default) — sends prompt/response to DriftGard API
19
+ * - "local" — evaluates locally via WASM, no data leaves your environment
20
+ * - "local-with-audit" — evaluates locally, reports verdict metadata to DriftGard (no prompt/response sent)
21
+ */
22
+ mode?: "remote" | "local" | "local-with-audit";
23
+ /** Project ID — required for local mode (used to fetch the control pack). */
24
+ projectId?: string;
25
+ /** Control pack refresh interval in milliseconds. Default 60000 (1 minute). Only used in local mode. */
26
+ refreshIntervalMs?: number;
27
+ /** Called when the control pack is refreshed or refresh fails. Only used in local mode. */
28
+ onControlPackRefresh?: (event: {
29
+ success: boolean;
30
+ version?: number;
31
+ error?: string;
32
+ stale?: boolean;
33
+ }) => void;
16
34
  }
17
35
  export interface EvaluateRequest {
18
36
  project_id: string;
@@ -43,6 +61,8 @@ export interface EvaluateRequest {
43
61
  on_behalf_of?: string;
44
62
  /** Parent agent ID — which orchestrator agent delegated to this one. */
45
63
  parent_agent_id?: string;
64
+ /** User's jurisdiction for jurisdiction-scoped rules. e.g. "AU", "AU-VIC", "US-CA". */
65
+ jurisdiction?: string;
46
66
  usage?: {
47
67
  prompt_tokens?: number;
48
68
  completion_tokens?: number;
@@ -0,0 +1,14 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Main evaluation entry point.
6
+ *
7
+ * # Arguments
8
+ * * `control_pack_json` - JSON string of the control pack
9
+ * * `request_json` - JSON string of the evaluation request
10
+ *
11
+ * # Returns
12
+ * JSON string of the verdict: { allowed, risk_score, violations, flags }
13
+ */
14
+ export function evaluate(control_pack_json: string, request_json: string): string;
@@ -0,0 +1,127 @@
1
+ /* @ts-self-types="./driftgard_evaluator.d.ts" */
2
+
3
+ /**
4
+ * Main evaluation entry point.
5
+ *
6
+ * # Arguments
7
+ * * `control_pack_json` - JSON string of the control pack
8
+ * * `request_json` - JSON string of the evaluation request
9
+ *
10
+ * # Returns
11
+ * JSON string of the verdict: { allowed, risk_score, violations, flags }
12
+ * @param {string} control_pack_json
13
+ * @param {string} request_json
14
+ * @returns {string}
15
+ */
16
+ function evaluate(control_pack_json, request_json) {
17
+ let deferred3_0;
18
+ let deferred3_1;
19
+ try {
20
+ const ptr0 = passStringToWasm0(control_pack_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
21
+ const len0 = WASM_VECTOR_LEN;
22
+ const ptr1 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
23
+ const len1 = WASM_VECTOR_LEN;
24
+ const ret = wasm.evaluate(ptr0, len0, ptr1, len1);
25
+ deferred3_0 = ret[0];
26
+ deferred3_1 = ret[1];
27
+ return getStringFromWasm0(ret[0], ret[1]);
28
+ } finally {
29
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
30
+ }
31
+ }
32
+ exports.evaluate = evaluate;
33
+ function __wbg_get_imports() {
34
+ const import0 = {
35
+ __proto__: null,
36
+ __wbindgen_init_externref_table: function() {
37
+ const table = wasm.__wbindgen_externrefs;
38
+ const offset = table.grow(4);
39
+ table.set(0, undefined);
40
+ table.set(offset + 0, undefined);
41
+ table.set(offset + 1, null);
42
+ table.set(offset + 2, true);
43
+ table.set(offset + 3, false);
44
+ },
45
+ };
46
+ return {
47
+ __proto__: null,
48
+ "./driftgard_evaluator_bg.js": import0,
49
+ };
50
+ }
51
+
52
+ function getStringFromWasm0(ptr, len) {
53
+ return decodeText(ptr >>> 0, len);
54
+ }
55
+
56
+ let cachedUint8ArrayMemory0 = null;
57
+ function getUint8ArrayMemory0() {
58
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
59
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
60
+ }
61
+ return cachedUint8ArrayMemory0;
62
+ }
63
+
64
+ function passStringToWasm0(arg, malloc, realloc) {
65
+ if (realloc === undefined) {
66
+ const buf = cachedTextEncoder.encode(arg);
67
+ const ptr = malloc(buf.length, 1) >>> 0;
68
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
69
+ WASM_VECTOR_LEN = buf.length;
70
+ return ptr;
71
+ }
72
+
73
+ let len = arg.length;
74
+ let ptr = malloc(len, 1) >>> 0;
75
+
76
+ const mem = getUint8ArrayMemory0();
77
+
78
+ let offset = 0;
79
+
80
+ for (; offset < len; offset++) {
81
+ const code = arg.charCodeAt(offset);
82
+ if (code > 0x7F) break;
83
+ mem[ptr + offset] = code;
84
+ }
85
+ if (offset !== len) {
86
+ if (offset !== 0) {
87
+ arg = arg.slice(offset);
88
+ }
89
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
90
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
91
+ const ret = cachedTextEncoder.encodeInto(arg, view);
92
+
93
+ offset += ret.written;
94
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
95
+ }
96
+
97
+ WASM_VECTOR_LEN = offset;
98
+ return ptr;
99
+ }
100
+
101
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
102
+ cachedTextDecoder.decode();
103
+ function decodeText(ptr, len) {
104
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
105
+ }
106
+
107
+ const cachedTextEncoder = new TextEncoder();
108
+
109
+ if (!('encodeInto' in cachedTextEncoder)) {
110
+ cachedTextEncoder.encodeInto = function (arg, view) {
111
+ const buf = cachedTextEncoder.encode(arg);
112
+ view.set(buf);
113
+ return {
114
+ read: arg.length,
115
+ written: buf.length
116
+ };
117
+ };
118
+ }
119
+
120
+ let WASM_VECTOR_LEN = 0;
121
+
122
+ const wasmPath = `${__dirname}/driftgard_evaluator_bg.wasm`;
123
+ const wasmBytes = require('fs').readFileSync(wasmPath);
124
+ const wasmModule = new WebAssembly.Module(wasmBytes);
125
+ let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
126
+ let wasm = wasmInstance.exports;
127
+ wasm.__wbindgen_start();
package/package.json CHANGED
@@ -1,15 +1,27 @@
1
1
  {
2
2
  "name": "@driftgard/node",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",
7
- "files": ["dist", "README.md"],
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
8
11
  "scripts": {
9
- "build": "tsc",
12
+ "build": "tsc && rm -rf dist/wasm && cp -r src/wasm dist/wasm",
10
13
  "prepublishOnly": "npm run build"
11
14
  },
12
- "keywords": ["driftgard", "ai", "compliance", "guardrails", "llm", "evaluation", "policy", "audit"],
15
+ "keywords": [
16
+ "driftgard",
17
+ "ai",
18
+ "compliance",
19
+ "guardrails",
20
+ "llm",
21
+ "evaluation",
22
+ "policy",
23
+ "audit"
24
+ ],
13
25
  "author": "Driftgard <support@driftgard.com>",
14
26
  "license": "MIT",
15
27
  "repository": {
@@ -21,6 +33,7 @@
21
33
  "node": ">=18.0.0"
22
34
  },
23
35
  "devDependencies": {
36
+ "@types/node": "^25.6.0",
24
37
  "typescript": "^5.0.0"
25
38
  }
26
39
  }