@tenova/swt3-ai 0.3.4 → 0.4.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.
Files changed (58) hide show
  1. package/README.md +150 -18
  2. package/dist/adapters/anthropic.d.ts +32 -0
  3. package/dist/adapters/anthropic.d.ts.map +1 -0
  4. package/dist/adapters/anthropic.js +301 -0
  5. package/dist/adapters/anthropic.js.map +1 -0
  6. package/dist/adapters/bedrock.d.ts +33 -0
  7. package/dist/adapters/bedrock.d.ts.map +1 -0
  8. package/dist/adapters/bedrock.js +269 -0
  9. package/dist/adapters/bedrock.js.map +1 -0
  10. package/dist/adapters/openai.d.ts +19 -0
  11. package/dist/adapters/openai.d.ts.map +1 -0
  12. package/dist/adapters/openai.js +289 -0
  13. package/dist/adapters/openai.js.map +1 -0
  14. package/dist/adapters/vercel-ai.d.ts +64 -0
  15. package/dist/adapters/vercel-ai.d.ts.map +1 -0
  16. package/dist/adapters/vercel-ai.js +68 -0
  17. package/dist/adapters/vercel-ai.js.map +1 -0
  18. package/dist/buffer.d.ts +43 -0
  19. package/dist/buffer.d.ts.map +1 -0
  20. package/dist/buffer.js +172 -0
  21. package/dist/buffer.js.map +1 -0
  22. package/dist/clearing.d.ts +39 -0
  23. package/dist/clearing.d.ts.map +1 -0
  24. package/dist/clearing.js +338 -0
  25. package/dist/clearing.js.map +1 -0
  26. package/dist/demo.d.ts +11 -0
  27. package/dist/demo.d.ts.map +1 -0
  28. package/dist/demo.js +238 -0
  29. package/dist/demo.js.map +1 -0
  30. package/dist/exporters/otel.d.ts +36 -0
  31. package/dist/exporters/otel.d.ts.map +1 -0
  32. package/dist/exporters/otel.js +94 -0
  33. package/dist/exporters/otel.js.map +1 -0
  34. package/dist/fingerprint.d.ts +29 -0
  35. package/dist/fingerprint.d.ts.map +1 -0
  36. package/dist/fingerprint.js +57 -0
  37. package/dist/fingerprint.js.map +1 -0
  38. package/dist/handoff.d.ts +17 -0
  39. package/dist/handoff.d.ts.map +1 -0
  40. package/dist/handoff.js +82 -0
  41. package/dist/handoff.js.map +1 -0
  42. package/dist/index.d.ts +22 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +20 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/signing.d.ts +20 -0
  47. package/dist/signing.d.ts.map +1 -0
  48. package/dist/signing.js +28 -0
  49. package/dist/signing.js.map +1 -0
  50. package/dist/types.d.ts +108 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +20 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/witness.d.ts +193 -0
  55. package/dist/witness.d.ts.map +1 -0
  56. package/dist/witness.js +487 -0
  57. package/dist/witness.js.map +1 -0
  58. package/package.json +10 -2
@@ -0,0 +1,193 @@
1
+ /**
2
+ * SWT3 AI Witness SDK — Core Witness class.
3
+ *
4
+ * Usage:
5
+ * import { Witness } from "@tenova/swt3-ai";
6
+ * import OpenAI from "openai";
7
+ *
8
+ * const witness = new Witness({
9
+ * endpoint: "https://sovereign.tenova.io",
10
+ * apiKey: "axm_live_...",
11
+ * tenantId: "YOUR_TENANT_ID",
12
+ * });
13
+ *
14
+ * const client = witness.wrap(new OpenAI()) as OpenAI;
15
+ * const response = await client.chat.completions.create({ ... });
16
+ *
17
+ * // Graceful shutdown
18
+ * await witness.flush();
19
+ *
20
+ * Copyright (c) 2026 Tenable Nova LLC. Apache 2.0. Patent pending.
21
+ */
22
+ import { type VercelOnFinishOptions } from "./adapters/vercel-ai.js";
23
+ import type { WitnessPayload, WitnessReceipt, InferenceRecord } from "./types.js";
24
+ export interface WitnessOptions {
25
+ endpoint: string;
26
+ apiKey: string;
27
+ tenantId: string;
28
+ clearingLevel?: 0 | 1 | 2 | 3;
29
+ bufferSize?: number;
30
+ flushInterval?: number;
31
+ timeout?: number;
32
+ maxRetries?: number;
33
+ latencyThresholdMs?: number;
34
+ guardrailsRequired?: number;
35
+ guardrailNames?: string[];
36
+ procedures?: string[];
37
+ factorHandoff?: "file";
38
+ factorHandoffPath?: string;
39
+ agentId?: string;
40
+ signingKey?: string;
41
+ cycleId?: string;
42
+ strict?: boolean;
43
+ policyVersion?: string;
44
+ jurisdiction?: string;
45
+ legalBasis?: string;
46
+ purposeClass?: string;
47
+ onFlush?: (payloads: WitnessPayload[], receipts: WitnessReceipt[]) => void;
48
+ }
49
+ /**
50
+ * Raised when strict (gatekeeper) mode blocks an inference due to
51
+ * insufficient guardrails. The inference never reaches the AI model.
52
+ */
53
+ export declare class GatekeeperError extends Error {
54
+ readonly required: number;
55
+ readonly active: number;
56
+ readonly missingNames: string[];
57
+ constructor(required: number, active: number, missingNames?: string[]);
58
+ }
59
+ export declare class Witness {
60
+ private config;
61
+ private buffer;
62
+ private handoffWarned;
63
+ private _strict;
64
+ constructor(options: WitnessOptions);
65
+ /**
66
+ * Wrap an AI client with transparent witnessing.
67
+ *
68
+ * Supported: OpenAI, Anthropic
69
+ * Returns a proxy that behaves identically to the original client.
70
+ *
71
+ * Usage:
72
+ * const client = witness.wrap(new OpenAI()) as OpenAI;
73
+ * const client = witness.wrap(new Anthropic()) as Anthropic;
74
+ */
75
+ wrap(client: unknown): unknown;
76
+ /** Whether strict (gatekeeper) mode is enabled. Used by adapters. */
77
+ get strict(): boolean;
78
+ /**
79
+ * Pre-call guardrail gate (strict mode only).
80
+ *
81
+ * Evaluates whether configured guardrail requirements are met BEFORE
82
+ * the inference call reaches the AI model. If requirements are not met,
83
+ * throws `GatekeeperError` and mints an AI-GRD.3 anchor recording
84
+ * the rejection. The rejection is evidence — it is enqueued for flush.
85
+ *
86
+ * This method is SYNCHRONOUS — evaluates local config only, no network.
87
+ *
88
+ * @throws {GatekeeperError} if guardrail requirements are not met
89
+ */
90
+ gateCheck(_messages?: unknown, _model?: string): string;
91
+ /**
92
+ * Wrap a function as a witnessed tool call (AI-TOOL.1).
93
+ *
94
+ * Usage:
95
+ * const search = witness.wrapTool(searchDatabase, "search_db");
96
+ * const result = await search("SELECT ...");
97
+ *
98
+ * Each call mints an AI-TOOL.1 anchor with:
99
+ * factor_a = 1 (tool was called)
100
+ * factor_b = latency_ms
101
+ * factor_c = 1 if succeeded, 0 if exception raised
102
+ */
103
+ wrapTool<T extends (...args: any[]) => any>(fn: T, toolName?: string): T;
104
+ /**
105
+ * Wrap a function as a witnessed access attempt (AI-ACC.1).
106
+ *
107
+ * Usage:
108
+ * const queryDb = witness.wrapAccess(dbQuery, "prod-database", "read-only analytics");
109
+ * const result = await queryDb("SELECT ...");
110
+ *
111
+ * Each call mints an AI-ACC.1 anchor with:
112
+ * factor_a = 1 (access attempt occurred)
113
+ * factor_b = 1 if within declared scope (or no scope set), 0 if out of scope
114
+ * factor_c = 1 if access granted, 0 if denied/failed
115
+ */
116
+ wrapAccess<T extends (...args: any[]) => any>(fn: T, resourceName?: string, scope?: string): T;
117
+ /**
118
+ * Witness a security/adversarial detection result (AI-SEC.1).
119
+ *
120
+ * Call after running your own detection system (Prompt Guard, LlamaGuard,
121
+ * NeMo Guardrails, etc.) to record the result as a tamper-evident anchor.
122
+ *
123
+ * @param threatScore - Detection score from your system (0-1000 scale).
124
+ * @param options.threshold - Score above which input is a threat. Default 500.
125
+ * @param options.threatType - One of: none, prompt_injection, data_poisoning,
126
+ * model_extraction, jailbreak, adversarial_input.
127
+ *
128
+ * @example
129
+ * const score = await promptGuard.scan(userInput);
130
+ * witness.witnessSecurityScan(score, { threatType: "prompt_injection" });
131
+ */
132
+ witnessSecurityScan(threatScore: number, options?: {
133
+ threshold?: number;
134
+ threatType?: string;
135
+ }): void;
136
+ /**
137
+ * Witness an input validation/sanitization result (AI-SEC.2).
138
+ *
139
+ * Call after validating or sanitizing user input before inference.
140
+ *
141
+ * @param passed - True if input accepted (clean or sanitized). False if blocked.
142
+ * @param options.sanitized - True if input was modified during validation.
143
+ *
144
+ * @example
145
+ * const { clean, modified } = mySanitizer.validate(userInput);
146
+ * witness.witnessInputValidation(clean, { sanitized: modified });
147
+ */
148
+ witnessInputValidation(passed: boolean, options?: {
149
+ sanitized?: boolean;
150
+ }): void;
151
+ /**
152
+ * Revoke a previously-issued witness anchor (AI-REV.1).
153
+ *
154
+ * Mints an AI-REV.1 anchor that references the target anchor's
155
+ * fingerprint, creating an immutable revocation receipt.
156
+ *
157
+ * @param fingerprint - The 12-character anchor fingerprint to revoke.
158
+ * @param reason - Revocation reason: model_recall, policy_violation,
159
+ * data_contamination, consent_withdrawal, regulatory_order,
160
+ * error_correction, or unspecified.
161
+ * @returns The fingerprint of the revocation anchor itself.
162
+ */
163
+ revoke(fingerprint: string, reason?: string): string;
164
+ /**
165
+ * Record a witnessed inference. Extracts factors, applies clearing,
166
+ * and enqueues payloads for background flush.
167
+ *
168
+ * If factorHandoff is configured, factors are written to the handoff
169
+ * destination BEFORE clearing proceeds. If the handoff fails, the
170
+ * payload is NOT transmitted.
171
+ */
172
+ record(inference: InferenceRecord, authorizationId?: string): void;
173
+ /**
174
+ * Create a Vercel AI SDK `onFinish` callback for streamText / generateText.
175
+ *
176
+ * Usage:
177
+ * const result = await streamText({
178
+ * model: openai("gpt-4o"),
179
+ * prompt: myPrompt,
180
+ * onFinish: witness.vercelOnFinish({ promptText: myPrompt }),
181
+ * });
182
+ */
183
+ vercelOnFinish(options?: VercelOnFinishOptions): (result: unknown) => void;
184
+ /** Force-flush all buffered payloads. */
185
+ flush(): Promise<WitnessReceipt[]>;
186
+ /** Stop the witness and flush remaining payloads. */
187
+ stop(): Promise<WitnessReceipt[]>;
188
+ /** Number of payloads waiting. */
189
+ get pending(): number;
190
+ /** All receipts from completed flushes. */
191
+ get receipts(): WitnessReceipt[];
192
+ }
193
+ //# sourceMappingURL=witness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"witness.d.ts","sourceRoot":"","sources":["../src/witness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,487 @@
1
+ /**
2
+ * SWT3 AI Witness SDK — Core Witness class.
3
+ *
4
+ * Usage:
5
+ * import { Witness } from "@tenova/swt3-ai";
6
+ * import OpenAI from "openai";
7
+ *
8
+ * const witness = new Witness({
9
+ * endpoint: "https://sovereign.tenova.io",
10
+ * apiKey: "axm_live_...",
11
+ * tenantId: "YOUR_TENANT_ID",
12
+ * });
13
+ *
14
+ * const client = witness.wrap(new OpenAI()) as OpenAI;
15
+ * const response = await client.chat.completions.create({ ... });
16
+ *
17
+ * // Graceful shutdown
18
+ * await witness.flush();
19
+ *
20
+ * Copyright (c) 2026 Tenable Nova LLC. Apache 2.0. Patent pending.
21
+ */
22
+ import { randomUUID } from "node:crypto";
23
+ import { sha256Truncated, mintFingerprint, timestampMs } from "./fingerprint.js";
24
+ import { extractPayloads, extractGatekeeperPayload, extractRevocationPayload, REVOCATION_REASONS } from "./clearing.js";
25
+ import { signPayload } from "./signing.js";
26
+ import { WitnessBuffer } from "./buffer.js";
27
+ import { writeHandoffFiles } from "./handoff.js";
28
+ import { wrapOpenAI } from "./adapters/openai.js";
29
+ import { wrapAnthropic } from "./adapters/anthropic.js";
30
+ import { wrapBedrock } from "./adapters/bedrock.js";
31
+ import { createVercelOnFinish } from "./adapters/vercel-ai.js";
32
+ /**
33
+ * Raised when strict (gatekeeper) mode blocks an inference due to
34
+ * insufficient guardrails. The inference never reaches the AI model.
35
+ */
36
+ export class GatekeeperError extends Error {
37
+ required;
38
+ active;
39
+ missingNames;
40
+ constructor(required, active, missingNames = []) {
41
+ const msg = `Gatekeeper blocked: ${active} guardrails active, ${required} required` +
42
+ (missingNames.length ? `. Missing: ${missingNames.join(", ")}` : "");
43
+ super(msg);
44
+ this.name = "GatekeeperError";
45
+ this.required = required;
46
+ this.active = active;
47
+ this.missingNames = missingNames;
48
+ }
49
+ }
50
+ export class Witness {
51
+ config;
52
+ buffer;
53
+ handoffWarned = false;
54
+ _strict;
55
+ constructor(options) {
56
+ if (!options.endpoint)
57
+ throw new Error("endpoint is required");
58
+ if (!options.apiKey)
59
+ throw new Error("apiKey is required");
60
+ if (!options.apiKey.startsWith("axm_"))
61
+ throw new Error("apiKey must start with 'axm_'");
62
+ if (!options.tenantId)
63
+ throw new Error("tenantId is required");
64
+ if (options.factorHandoff && options.factorHandoff !== "file") {
65
+ throw new Error("factorHandoff must be 'file'");
66
+ }
67
+ if (options.factorHandoff === "file" && !options.factorHandoffPath) {
68
+ throw new Error("factorHandoffPath is required when factorHandoff is 'file'");
69
+ }
70
+ this.config = {
71
+ endpoint: options.endpoint.replace(/\/+$/, ""),
72
+ apiKey: options.apiKey,
73
+ tenantId: options.tenantId,
74
+ clearingLevel: options.clearingLevel ?? 1,
75
+ bufferSize: options.bufferSize ?? 10,
76
+ flushInterval: options.flushInterval ?? 5.0,
77
+ timeout: options.timeout ?? 10000,
78
+ maxRetries: options.maxRetries ?? 3,
79
+ latencyThresholdMs: options.latencyThresholdMs ?? 30000,
80
+ guardrailsRequired: options.guardrailsRequired ?? 0,
81
+ guardrailNames: options.guardrailNames ?? [],
82
+ procedures: options.procedures,
83
+ factorHandoff: options.factorHandoff,
84
+ factorHandoffPath: options.factorHandoffPath,
85
+ agentId: options.agentId,
86
+ signingKey: options.signingKey,
87
+ cycleId: options.cycleId,
88
+ policyVersion: options.policyVersion,
89
+ jurisdiction: options.jurisdiction,
90
+ legalBasis: options.legalBasis,
91
+ purposeClass: options.purposeClass,
92
+ onFlush: options.onFlush,
93
+ };
94
+ this._strict = options.strict ?? false;
95
+ this.buffer = new WitnessBuffer(this.config);
96
+ }
97
+ /**
98
+ * Wrap an AI client with transparent witnessing.
99
+ *
100
+ * Supported: OpenAI, Anthropic
101
+ * Returns a proxy that behaves identically to the original client.
102
+ *
103
+ * Usage:
104
+ * const client = witness.wrap(new OpenAI()) as OpenAI;
105
+ * const client = witness.wrap(new Anthropic()) as Anthropic;
106
+ */
107
+ wrap(client) {
108
+ const proto = Object.getPrototypeOf(client);
109
+ const name = proto?.constructor?.name ?? "";
110
+ const obj = client;
111
+ // OpenAI: has client.chat.completions
112
+ if (name === "OpenAI" || obj?.chat) {
113
+ return wrapOpenAI(client, this);
114
+ }
115
+ // Anthropic: has client.messages
116
+ if (name === "Anthropic" || (obj?.messages && !obj?.chat)) {
117
+ return wrapAnthropic(client, this);
118
+ }
119
+ // AWS Bedrock: has client.send and client.config
120
+ if (name === "BedrockRuntimeClient" || (obj?.send && obj?.config)) {
121
+ return wrapBedrock(client, this);
122
+ }
123
+ throw new TypeError(`Unsupported client: ${name || "unknown"}. Supported: OpenAI, Anthropic, BedrockRuntimeClient.`);
124
+ }
125
+ /** Whether strict (gatekeeper) mode is enabled. Used by adapters. */
126
+ get strict() {
127
+ return this._strict;
128
+ }
129
+ /**
130
+ * Pre-call guardrail gate (strict mode only).
131
+ *
132
+ * Evaluates whether configured guardrail requirements are met BEFORE
133
+ * the inference call reaches the AI model. If requirements are not met,
134
+ * throws `GatekeeperError` and mints an AI-GRD.3 anchor recording
135
+ * the rejection. The rejection is evidence — it is enqueued for flush.
136
+ *
137
+ * This method is SYNCHRONOUS — evaluates local config only, no network.
138
+ *
139
+ * @throws {GatekeeperError} if guardrail requirements are not met
140
+ */
141
+ gateCheck(_messages, _model) {
142
+ const required = this.config.guardrailsRequired;
143
+ const active = this.config.guardrailNames.length;
144
+ const gatePassed = active >= required;
145
+ // Mint AI-GRD.3 anchor regardless of outcome — rejection is evidence
146
+ const policyHash = this.config.policyVersion
147
+ ? sha256Truncated(this.config.policyVersion, 12)
148
+ : undefined;
149
+ const payload = extractGatekeeperPayload(this.config.tenantId, required, active, gatePassed, this.config.clearingLevel, this.config.agentId, this.config.signingKey, this.config.cycleId, policyHash, this.config.jurisdiction, this.config.legalBasis, this.config.purposeClass);
150
+ this.buffer.enqueueMany([payload]);
151
+ if (!gatePassed) {
152
+ throw new GatekeeperError(required, active);
153
+ }
154
+ return payload.anchor_fingerprint;
155
+ }
156
+ /**
157
+ * Wrap a function as a witnessed tool call (AI-TOOL.1).
158
+ *
159
+ * Usage:
160
+ * const search = witness.wrapTool(searchDatabase, "search_db");
161
+ * const result = await search("SELECT ...");
162
+ *
163
+ * Each call mints an AI-TOOL.1 anchor with:
164
+ * factor_a = 1 (tool was called)
165
+ * factor_b = latency_ms
166
+ * factor_c = 1 if succeeded, 0 if exception raised
167
+ */
168
+ wrapTool(fn, toolName) {
169
+ const name = toolName ?? fn.name ?? "anonymous";
170
+ const self = this;
171
+ const wrapper = function (...args) {
172
+ const callId = randomUUID().replace(/-/g, "").slice(0, 12);
173
+ const start = performance.now();
174
+ let succeeded = true;
175
+ let result;
176
+ const finish = () => {
177
+ const elapsedMs = Math.round(performance.now() - start);
178
+ const inputHash = sha256Truncated(JSON.stringify(args));
179
+ const outputHash = sha256Truncated(succeeded ? JSON.stringify(result) : "ERROR");
180
+ const record = {
181
+ modelId: name,
182
+ modelHash: sha256Truncated(name),
183
+ promptHash: inputHash,
184
+ responseHash: outputHash,
185
+ latencyMs: elapsedMs,
186
+ guardrailsActive: 0,
187
+ guardrailsRequired: 0,
188
+ guardrailPassed: true,
189
+ hasRefusal: !succeeded,
190
+ provider: "tool",
191
+ guardrailNames: [],
192
+ toolName: name,
193
+ toolCallId: callId,
194
+ };
195
+ self.record(record);
196
+ };
197
+ try {
198
+ result = fn.apply(this, args);
199
+ }
200
+ catch (err) {
201
+ succeeded = false;
202
+ finish();
203
+ throw err;
204
+ }
205
+ // Handle async functions (Promise detection)
206
+ if (result && typeof result.then === "function") {
207
+ return result.then((v) => {
208
+ result = v;
209
+ finish();
210
+ return v;
211
+ }, (err) => {
212
+ succeeded = false;
213
+ finish();
214
+ throw err;
215
+ });
216
+ }
217
+ finish();
218
+ return result;
219
+ };
220
+ return wrapper;
221
+ }
222
+ /**
223
+ * Wrap a function as a witnessed access attempt (AI-ACC.1).
224
+ *
225
+ * Usage:
226
+ * const queryDb = witness.wrapAccess(dbQuery, "prod-database", "read-only analytics");
227
+ * const result = await queryDb("SELECT ...");
228
+ *
229
+ * Each call mints an AI-ACC.1 anchor with:
230
+ * factor_a = 1 (access attempt occurred)
231
+ * factor_b = 1 if within declared scope (or no scope set), 0 if out of scope
232
+ * factor_c = 1 if access granted, 0 if denied/failed
233
+ */
234
+ wrapAccess(fn, resourceName, scope) {
235
+ const name = resourceName ?? fn.name ?? "unknown-resource";
236
+ const self = this;
237
+ const wrapper = function (...args) {
238
+ const start = performance.now();
239
+ let granted = true;
240
+ let result;
241
+ const finish = () => {
242
+ const elapsedMs = Math.round(performance.now() - start);
243
+ const inputHash = sha256Truncated(JSON.stringify(args));
244
+ const outputHash = sha256Truncated(granted ? JSON.stringify(result) : "ACCESS_DENIED");
245
+ const record = {
246
+ modelId: name,
247
+ modelHash: sha256Truncated(name),
248
+ promptHash: inputHash,
249
+ responseHash: outputHash,
250
+ latencyMs: elapsedMs,
251
+ guardrailsActive: 0,
252
+ guardrailsRequired: 0,
253
+ guardrailPassed: true,
254
+ hasRefusal: !granted,
255
+ provider: "access",
256
+ guardrailNames: [],
257
+ accessTarget: name,
258
+ accessGranted: granted,
259
+ accessScope: scope,
260
+ };
261
+ self.record(record);
262
+ };
263
+ try {
264
+ result = fn.apply(this, args);
265
+ }
266
+ catch (err) {
267
+ granted = false;
268
+ finish();
269
+ throw err;
270
+ }
271
+ // Handle async functions (Promise detection)
272
+ if (result && typeof result.then === "function") {
273
+ return result.then((v) => {
274
+ result = v;
275
+ finish();
276
+ return v;
277
+ }, (err) => {
278
+ granted = false;
279
+ finish();
280
+ throw err;
281
+ });
282
+ }
283
+ finish();
284
+ return result;
285
+ };
286
+ return wrapper;
287
+ }
288
+ /**
289
+ * Witness a security/adversarial detection result (AI-SEC.1).
290
+ *
291
+ * Call after running your own detection system (Prompt Guard, LlamaGuard,
292
+ * NeMo Guardrails, etc.) to record the result as a tamper-evident anchor.
293
+ *
294
+ * @param threatScore - Detection score from your system (0-1000 scale).
295
+ * @param options.threshold - Score above which input is a threat. Default 500.
296
+ * @param options.threatType - One of: none, prompt_injection, data_poisoning,
297
+ * model_extraction, jailbreak, adversarial_input.
298
+ *
299
+ * @example
300
+ * const score = await promptGuard.scan(userInput);
301
+ * witness.witnessSecurityScan(score, { threatType: "prompt_injection" });
302
+ */
303
+ witnessSecurityScan(threatScore, options) {
304
+ const threshold = options?.threshold ?? 500;
305
+ const threatType = options?.threatType ?? "none";
306
+ const threatCodes = {
307
+ none: 0, prompt_injection: 1, data_poisoning: 2,
308
+ model_extraction: 3, jailbreak: 4, adversarial_input: 5,
309
+ };
310
+ const [ts, epoch] = timestampMs();
311
+ const fa = threshold;
312
+ const fb = threatScore;
313
+ const fc = threatCodes[threatType] ?? 0;
314
+ const fp = mintFingerprint(this.config.tenantId, "AI-SEC.1", fa, fb, fc, ts);
315
+ const payload = {
316
+ procedure_id: "AI-SEC.1",
317
+ factor_a: fa,
318
+ factor_b: fb,
319
+ factor_c: fc,
320
+ clearing_level: this.config.clearingLevel,
321
+ anchor_fingerprint: fp,
322
+ anchor_epoch: epoch,
323
+ fingerprint_timestamp_ms: ts,
324
+ };
325
+ if (this.config.clearingLevel <= 1) {
326
+ payload.ai_model_id = "security-scan";
327
+ payload.ai_context = { provider: "security" };
328
+ }
329
+ if (this.config.agentId)
330
+ payload.agent_id = this.config.agentId;
331
+ if (this.config.cycleId)
332
+ payload.cycle_id = this.config.cycleId;
333
+ if (this.config.jurisdiction)
334
+ payload.jurisdiction = this.config.jurisdiction;
335
+ if (this.config.legalBasis)
336
+ payload.legal_basis = this.config.legalBasis;
337
+ if (this.config.purposeClass)
338
+ payload.purpose_class = this.config.purposeClass;
339
+ if (this.config.policyVersion) {
340
+ payload.policy_version_hash = sha256Truncated(this.config.policyVersion, 12);
341
+ }
342
+ if (this.config.signingKey) {
343
+ payload.payload_signature = signPayload(this.config.signingKey, fp, this.config.agentId);
344
+ }
345
+ this.buffer.enqueueMany([payload]);
346
+ }
347
+ /**
348
+ * Witness an input validation/sanitization result (AI-SEC.2).
349
+ *
350
+ * Call after validating or sanitizing user input before inference.
351
+ *
352
+ * @param passed - True if input accepted (clean or sanitized). False if blocked.
353
+ * @param options.sanitized - True if input was modified during validation.
354
+ *
355
+ * @example
356
+ * const { clean, modified } = mySanitizer.validate(userInput);
357
+ * witness.witnessInputValidation(clean, { sanitized: modified });
358
+ */
359
+ witnessInputValidation(passed, options) {
360
+ const sanitized = options?.sanitized ?? false;
361
+ const [ts, epoch] = timestampMs();
362
+ const fa = 1;
363
+ const fb = passed ? 1 : 0;
364
+ const fc = (passed && !sanitized) ? 0 : (passed && sanitized) ? 1 : 2;
365
+ const fp = mintFingerprint(this.config.tenantId, "AI-SEC.2", fa, fb, fc, ts);
366
+ const payload = {
367
+ procedure_id: "AI-SEC.2",
368
+ factor_a: fa,
369
+ factor_b: fb,
370
+ factor_c: fc,
371
+ clearing_level: this.config.clearingLevel,
372
+ anchor_fingerprint: fp,
373
+ anchor_epoch: epoch,
374
+ fingerprint_timestamp_ms: ts,
375
+ };
376
+ if (this.config.clearingLevel <= 1) {
377
+ payload.ai_model_id = "input-validation";
378
+ payload.ai_context = { provider: "security" };
379
+ }
380
+ if (this.config.agentId)
381
+ payload.agent_id = this.config.agentId;
382
+ if (this.config.cycleId)
383
+ payload.cycle_id = this.config.cycleId;
384
+ if (this.config.jurisdiction)
385
+ payload.jurisdiction = this.config.jurisdiction;
386
+ if (this.config.legalBasis)
387
+ payload.legal_basis = this.config.legalBasis;
388
+ if (this.config.purposeClass)
389
+ payload.purpose_class = this.config.purposeClass;
390
+ if (this.config.policyVersion) {
391
+ payload.policy_version_hash = sha256Truncated(this.config.policyVersion, 12);
392
+ }
393
+ if (this.config.signingKey) {
394
+ payload.payload_signature = signPayload(this.config.signingKey, fp, this.config.agentId);
395
+ }
396
+ this.buffer.enqueueMany([payload]);
397
+ }
398
+ /**
399
+ * Revoke a previously-issued witness anchor (AI-REV.1).
400
+ *
401
+ * Mints an AI-REV.1 anchor that references the target anchor's
402
+ * fingerprint, creating an immutable revocation receipt.
403
+ *
404
+ * @param fingerprint - The 12-character anchor fingerprint to revoke.
405
+ * @param reason - Revocation reason: model_recall, policy_violation,
406
+ * data_contamination, consent_withdrawal, regulatory_order,
407
+ * error_correction, or unspecified.
408
+ * @returns The fingerprint of the revocation anchor itself.
409
+ */
410
+ revoke(fingerprint, reason = "unspecified") {
411
+ if (!fingerprint?.trim()) {
412
+ throw new Error("fingerprint is required for revocation");
413
+ }
414
+ if (!(reason in REVOCATION_REASONS)) {
415
+ throw new Error(`Unknown revocation reason: "${reason}". Valid: ${Object.keys(REVOCATION_REASONS).sort().join(", ")}`);
416
+ }
417
+ const policyHash = this.config.policyVersion
418
+ ? sha256Truncated(this.config.policyVersion, 12)
419
+ : undefined;
420
+ const payload = extractRevocationPayload(this.config.tenantId, fingerprint.trim(), reason, this.config.clearingLevel, this.config.agentId, this.config.signingKey, this.config.cycleId, policyHash, this.config.jurisdiction, this.config.legalBasis, this.config.purposeClass);
421
+ this.buffer.enqueueMany([payload]);
422
+ return payload.anchor_fingerprint;
423
+ }
424
+ /**
425
+ * Record a witnessed inference. Extracts factors, applies clearing,
426
+ * and enqueues payloads for background flush.
427
+ *
428
+ * If factorHandoff is configured, factors are written to the handoff
429
+ * destination BEFORE clearing proceeds. If the handoff fails, the
430
+ * payload is NOT transmitted.
431
+ */
432
+ record(inference, authorizationId) {
433
+ // Merge guardrail config
434
+ if (this.config.guardrailNames.length > 0 && inference.guardrailNames.length === 0) {
435
+ inference.guardrailNames = this.config.guardrailNames;
436
+ inference.guardrailsActive = this.config.guardrailNames.length;
437
+ inference.guardrailsRequired = this.config.guardrailsRequired;
438
+ }
439
+ const policyHash = this.config.policyVersion
440
+ ? sha256Truncated(this.config.policyVersion, 12)
441
+ : undefined;
442
+ const payloads = extractPayloads(inference, this.config.tenantId, this.config.clearingLevel, this.config.latencyThresholdMs, this.config.guardrailsRequired, this.config.procedures, this.config.agentId, this.config.signingKey, this.config.cycleId, policyHash, this.config.jurisdiction, this.config.legalBasis, this.config.purposeClass, authorizationId);
443
+ // Factor handoff: write full (uncleared) data to custody destination
444
+ // BEFORE enqueuing the cleared payload for transmission.
445
+ // If this fails, we do NOT proceed.
446
+ if (this.config.factorHandoff === "file" && this.config.factorHandoffPath) {
447
+ writeHandoffFiles(payloads, inference, this.config.tenantId, this.config.factorHandoffPath);
448
+ if (!this.handoffWarned) {
449
+ this.handoffWarned = true;
450
+ console.info(`\n [SWT3] ${payloads.length} anchors saved locally to ${this.config.factorHandoffPath}` +
451
+ `\n [SWT3] \u26a0 Local anchors won\u2019t survive a compliance audit.` +
452
+ `\n [SWT3] Connect to Axiom Engine \u2192 https://sovereign.tenova.io/signup?ref=sdk (free)\n`);
453
+ }
454
+ }
455
+ this.buffer.enqueueMany(payloads);
456
+ }
457
+ /**
458
+ * Create a Vercel AI SDK `onFinish` callback for streamText / generateText.
459
+ *
460
+ * Usage:
461
+ * const result = await streamText({
462
+ * model: openai("gpt-4o"),
463
+ * prompt: myPrompt,
464
+ * onFinish: witness.vercelOnFinish({ promptText: myPrompt }),
465
+ * });
466
+ */
467
+ vercelOnFinish(options) {
468
+ return createVercelOnFinish(this, options);
469
+ }
470
+ /** Force-flush all buffered payloads. */
471
+ async flush() {
472
+ return this.buffer.flush();
473
+ }
474
+ /** Stop the witness and flush remaining payloads. */
475
+ async stop() {
476
+ return this.buffer.stop();
477
+ }
478
+ /** Number of payloads waiting. */
479
+ get pending() {
480
+ return this.buffer.pending;
481
+ }
482
+ /** All receipts from completed flushes. */
483
+ get receipts() {
484
+ return this.buffer.receipts;
485
+ }
486
+ }
487
+ //# sourceMappingURL=witness.js.map