@wytness/sdk 0.3.1 → 0.5.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 +5 -2
- package/dist/index.cjs +229 -23
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +225 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,9 +30,11 @@ await sendEmail("team@company.com", "Report", "Weekly summary");
|
|
|
30
30
|
|
|
31
31
|
## Features
|
|
32
32
|
|
|
33
|
+
- **Response capture** — AI agent responses are automatically captured, truncated to 5K chars, and PII-redacted
|
|
34
|
+
- **PII redaction** — emails, SSN, TFN, credit cards, and phone numbers are automatically redacted from responses and parameter values
|
|
33
35
|
- **Cryptographic signing** — every event is signed with Ed25519 (tweetnacl)
|
|
34
36
|
- **Hash chaining** — tamper-evident chain of events per agent session
|
|
35
|
-
- **Automatic redaction** — secrets in
|
|
37
|
+
- **Automatic secret redaction** — secrets in parameter names are automatically redacted
|
|
36
38
|
- **HTTP transport** — stream events to api.wytness.dev over HTTPS
|
|
37
39
|
- **File fallback** — events are saved locally if the API is unreachable
|
|
38
40
|
|
|
@@ -66,7 +68,7 @@ const client = new AuditClient({
|
|
|
66
68
|
|
|
67
69
|
### `auditTool(client, fn, options)`
|
|
68
70
|
|
|
69
|
-
Wraps a function to automatically log audit events. Captures function name, parameters, execution duration, success/failure status, cryptographic signature, and hash chain link.
|
|
71
|
+
Wraps a function to automatically log audit events. Captures function name, parameters, **response text** (truncated to 5K chars, PII-redacted), execution duration, success/failure status, cryptographic signature, and hash chain link.
|
|
70
72
|
|
|
71
73
|
### `client.sessionId`
|
|
72
74
|
|
|
@@ -114,6 +116,7 @@ import {
|
|
|
114
116
|
AuditClientOptions,
|
|
115
117
|
auditTool,
|
|
116
118
|
hashValue,
|
|
119
|
+
redactPii,
|
|
117
120
|
AuditEvent,
|
|
118
121
|
AuditEventSchema,
|
|
119
122
|
generateKeypair,
|
package/dist/index.cjs
CHANGED
|
@@ -32,12 +32,14 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AuditClient: () => AuditClient,
|
|
34
34
|
AuditEventSchema: () => AuditEventSchema,
|
|
35
|
+
SessionTokenizer: () => SessionTokenizer,
|
|
35
36
|
auditTool: () => auditTool,
|
|
36
37
|
computeEventHash: () => computeEventHash,
|
|
37
38
|
createFileEmitter: () => createFileEmitter,
|
|
38
39
|
createHttpEmitter: () => createHttpEmitter,
|
|
39
40
|
generateKeypair: () => generateKeypair,
|
|
40
41
|
hashValue: () => hashValue,
|
|
42
|
+
redactPii: () => redactPii,
|
|
41
43
|
signEvent: () => signEvent,
|
|
42
44
|
verifyChain: () => verifyChain,
|
|
43
45
|
verifyEvent: () => verifyEvent
|
|
@@ -72,6 +74,7 @@ var AuditEventSchema = import_zod.z.object({
|
|
|
72
74
|
inputs_source: import_zod.z.string().default(""),
|
|
73
75
|
outputs_hash: import_zod.z.string().default(""),
|
|
74
76
|
outputs_destination: import_zod.z.string().default(""),
|
|
77
|
+
response: import_zod.z.string().default(""),
|
|
75
78
|
// Layer 5: Outcome
|
|
76
79
|
status: import_zod.z.string().default("success"),
|
|
77
80
|
error_code: import_zod.z.string().nullable().default(null),
|
|
@@ -79,7 +82,10 @@ var AuditEventSchema = import_zod.z.object({
|
|
|
79
82
|
retry_count: import_zod.z.number().int().default(0),
|
|
80
83
|
// Chain integrity
|
|
81
84
|
prev_event_hash: import_zod.z.string().default(""),
|
|
82
|
-
signature: import_zod.z.string().default("")
|
|
85
|
+
signature: import_zod.z.string().default(""),
|
|
86
|
+
// Pseudonymization
|
|
87
|
+
encrypted_token_map: import_zod.z.string().default(""),
|
|
88
|
+
pseudonymization_version: import_zod.z.string().default("")
|
|
83
89
|
});
|
|
84
90
|
|
|
85
91
|
// src/signing.ts
|
|
@@ -230,9 +236,150 @@ async function flushPending() {
|
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
// src/client.ts
|
|
233
|
-
var
|
|
239
|
+
var import_crypto4 = require("crypto");
|
|
234
240
|
var import_fs2 = require("fs");
|
|
235
241
|
var import_path2 = require("path");
|
|
242
|
+
|
|
243
|
+
// src/tokenizer.ts
|
|
244
|
+
var import_crypto3 = require("crypto");
|
|
245
|
+
var import_tweetnacl2 = __toESM(require("tweetnacl"), 1);
|
|
246
|
+
var import_tweetnacl_util2 = require("tweetnacl-util");
|
|
247
|
+
var SECRET_PATTERN = /key|secret|token|password|passwd|pwd|credential|auth/i;
|
|
248
|
+
var PII_PATTERNS = [
|
|
249
|
+
[/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "EMAIL"],
|
|
250
|
+
[/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, "SSN"],
|
|
251
|
+
[/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{3}\b/g, "TFN"],
|
|
252
|
+
[/\b(?:\d{4}[-\s]?){3}\d{4}\b/g, "CARD"],
|
|
253
|
+
[/\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "PHONE"]
|
|
254
|
+
];
|
|
255
|
+
function hmacPseudonym(value, secret, piiType = "PII") {
|
|
256
|
+
const digest = (0, import_crypto3.createHmac)("sha256", secret).update(value, "utf-8").digest("hex");
|
|
257
|
+
return `${piiType}_${digest.slice(0, 8)}`;
|
|
258
|
+
}
|
|
259
|
+
var SessionTokenizer = class {
|
|
260
|
+
_hmacSecret;
|
|
261
|
+
_encryptionPub;
|
|
262
|
+
_piiFields;
|
|
263
|
+
_tokenMap = {};
|
|
264
|
+
// pseudonym → original
|
|
265
|
+
_reverseMap = {};
|
|
266
|
+
// original → pseudonym
|
|
267
|
+
/**
|
|
268
|
+
* @param hmacSecret 32-byte secret for deterministic pseudonyms.
|
|
269
|
+
* @param encryptionPublicKey 32-byte X25519 public key for encrypting the token map.
|
|
270
|
+
* @param piiFields Parameter names whose values should be fully pseudonymized.
|
|
271
|
+
*/
|
|
272
|
+
constructor(hmacSecret, encryptionPublicKey, piiFields = []) {
|
|
273
|
+
this._hmacSecret = hmacSecret;
|
|
274
|
+
this._encryptionPub = encryptionPublicKey;
|
|
275
|
+
this._piiFields = piiFields;
|
|
276
|
+
}
|
|
277
|
+
get tokenMap() {
|
|
278
|
+
return { ...this._tokenMap };
|
|
279
|
+
}
|
|
280
|
+
addMapping(original, pseudonym) {
|
|
281
|
+
this._tokenMap[pseudonym] = original;
|
|
282
|
+
this._reverseMap[original] = pseudonym;
|
|
283
|
+
}
|
|
284
|
+
pseudonymizeValue(value, fieldPath = "") {
|
|
285
|
+
if (!value || !value.trim())
|
|
286
|
+
return value;
|
|
287
|
+
if (this._piiFields.includes(fieldPath)) {
|
|
288
|
+
if (this._reverseMap[value])
|
|
289
|
+
return this._reverseMap[value];
|
|
290
|
+
const pseudonym = hmacPseudonym(value, this._hmacSecret);
|
|
291
|
+
this.addMapping(value, pseudonym);
|
|
292
|
+
return pseudonym;
|
|
293
|
+
}
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
pseudonymizeText(text) {
|
|
297
|
+
if (!text)
|
|
298
|
+
return text;
|
|
299
|
+
const known = Object.keys(this._reverseMap).sort(
|
|
300
|
+
(a, b) => b.length - a.length
|
|
301
|
+
);
|
|
302
|
+
for (const original of known) {
|
|
303
|
+
if (text.includes(original)) {
|
|
304
|
+
text = text.split(original).join(this._reverseMap[original]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const [pattern, piiType] of PII_PATTERNS) {
|
|
308
|
+
pattern.lastIndex = 0;
|
|
309
|
+
text = text.replace(pattern, (matched) => {
|
|
310
|
+
if (this._reverseMap[matched])
|
|
311
|
+
return this._reverseMap[matched];
|
|
312
|
+
const pseudonym = hmacPseudonym(matched, this._hmacSecret, piiType);
|
|
313
|
+
this.addMapping(matched, pseudonym);
|
|
314
|
+
return pseudonym;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return text;
|
|
318
|
+
}
|
|
319
|
+
pseudonymizeParams(params) {
|
|
320
|
+
const result = {};
|
|
321
|
+
for (const [k, v] of Object.entries(params)) {
|
|
322
|
+
if (SECRET_PATTERN.test(k)) {
|
|
323
|
+
result[k] = "[REDACTED]";
|
|
324
|
+
} else if (this._piiFields.includes(k)) {
|
|
325
|
+
const val = String(v).slice(0, 500);
|
|
326
|
+
result[k] = this.pseudonymizeValue(val, k);
|
|
327
|
+
} else {
|
|
328
|
+
const val = String(v).slice(0, 500);
|
|
329
|
+
result[k] = this.pseudonymizeText(val);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Encrypt the token map with the customer's X25519 public key.
|
|
336
|
+
*
|
|
337
|
+
* Format: base64([ephemeral_pub 32B][nonce 24B][ciphertext])
|
|
338
|
+
* Uses NaCl box (X25519 + XSalsa20-Poly1305).
|
|
339
|
+
*/
|
|
340
|
+
encryptTokenMap() {
|
|
341
|
+
const plaintext = Buffer.from(
|
|
342
|
+
JSON.stringify(this._tokenMap),
|
|
343
|
+
"utf-8"
|
|
344
|
+
);
|
|
345
|
+
const ephemeral = import_tweetnacl2.default.box.keyPair();
|
|
346
|
+
const nonce = import_tweetnacl2.default.randomBytes(24);
|
|
347
|
+
const ciphertext = import_tweetnacl2.default.box(
|
|
348
|
+
plaintext,
|
|
349
|
+
nonce,
|
|
350
|
+
this._encryptionPub,
|
|
351
|
+
ephemeral.secretKey
|
|
352
|
+
);
|
|
353
|
+
if (!ciphertext) {
|
|
354
|
+
throw new Error("Encryption failed");
|
|
355
|
+
}
|
|
356
|
+
const packed = new Uint8Array(32 + 24 + ciphertext.length);
|
|
357
|
+
packed.set(ephemeral.publicKey, 0);
|
|
358
|
+
packed.set(nonce, 32);
|
|
359
|
+
packed.set(ciphertext, 56);
|
|
360
|
+
return (0, import_tweetnacl_util2.encodeBase64)(packed);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Decrypt a token map using the customer's X25519 private key.
|
|
364
|
+
*
|
|
365
|
+
* @param encryptedB64 Base64-encoded encrypted blob from encryptTokenMap().
|
|
366
|
+
* @param privateKey 32-byte X25519 private (secret) key.
|
|
367
|
+
* @returns The token map (pseudonym → original).
|
|
368
|
+
*/
|
|
369
|
+
static decryptTokenMap(encryptedB64, privateKey) {
|
|
370
|
+
const packed = (0, import_tweetnacl_util2.decodeBase64)(encryptedB64);
|
|
371
|
+
const ephemeralPub = packed.slice(0, 32);
|
|
372
|
+
const nonce = packed.slice(32, 56);
|
|
373
|
+
const ciphertext = packed.slice(56);
|
|
374
|
+
const plaintext = import_tweetnacl2.default.box.open(ciphertext, nonce, ephemeralPub, privateKey);
|
|
375
|
+
if (!plaintext) {
|
|
376
|
+
throw new Error("Decryption failed \u2014 wrong key or corrupted data");
|
|
377
|
+
}
|
|
378
|
+
return JSON.parse(Buffer.from(plaintext).toString("utf-8"));
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/client.ts
|
|
236
383
|
var AuditClient = class {
|
|
237
384
|
agentId;
|
|
238
385
|
agentVersion;
|
|
@@ -241,11 +388,12 @@ var AuditClient = class {
|
|
|
241
388
|
_sessionId;
|
|
242
389
|
_secretKey;
|
|
243
390
|
_emit;
|
|
391
|
+
_tokenizer = null;
|
|
244
392
|
constructor(options) {
|
|
245
393
|
this.agentId = options.agentId;
|
|
246
394
|
this.agentVersion = options.agentVersion ?? "0.1.0";
|
|
247
395
|
this.humanOperatorId = options.humanOperatorId ?? "unknown";
|
|
248
|
-
this._sessionId = (0,
|
|
396
|
+
this._sessionId = (0, import_crypto4.randomUUID)();
|
|
249
397
|
if (options.signingKey) {
|
|
250
398
|
this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
|
|
251
399
|
} else {
|
|
@@ -270,10 +418,18 @@ var AuditClient = class {
|
|
|
270
418
|
} else {
|
|
271
419
|
this._emit = createFileEmitter(fallback);
|
|
272
420
|
}
|
|
421
|
+
if (options.piiHmacSecret && options.encryptionPublicKey) {
|
|
422
|
+
const hmacBytes = new Uint8Array(Buffer.from(options.piiHmacSecret, "base64"));
|
|
423
|
+
const pubBytes = new Uint8Array(Buffer.from(options.encryptionPublicKey, "base64"));
|
|
424
|
+
this._tokenizer = new SessionTokenizer(hmacBytes, pubBytes, options.piiFields ?? []);
|
|
425
|
+
}
|
|
273
426
|
}
|
|
274
427
|
get sessionId() {
|
|
275
428
|
return this._sessionId;
|
|
276
429
|
}
|
|
430
|
+
get tokenizer() {
|
|
431
|
+
return this._tokenizer;
|
|
432
|
+
}
|
|
277
433
|
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
278
434
|
record(event) {
|
|
279
435
|
try {
|
|
@@ -297,39 +453,87 @@ var AuditClient = class {
|
|
|
297
453
|
};
|
|
298
454
|
|
|
299
455
|
// src/decorator.ts
|
|
300
|
-
var
|
|
301
|
-
var
|
|
456
|
+
var import_crypto5 = require("crypto");
|
|
457
|
+
var SECRET_PATTERN2 = /key|secret|token|password|passwd|pwd|credential|auth/i;
|
|
458
|
+
var PII_PATTERNS2 = [
|
|
459
|
+
[/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[EMAIL_REDACTED]"],
|
|
460
|
+
[/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, "[SSN_REDACTED]"],
|
|
461
|
+
[/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{3}\b/g, "[TFN_REDACTED]"],
|
|
462
|
+
[/\b(?:\d{4}[-\s]?){3}\d{4}\b/g, "[CARD_REDACTED]"],
|
|
463
|
+
[/\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "[PHONE_REDACTED]"]
|
|
464
|
+
];
|
|
465
|
+
var RESPONSE_MAX_CHARS = 5e3;
|
|
466
|
+
function redactPii(text) {
|
|
467
|
+
for (const [pattern, replacement] of PII_PATTERNS2) {
|
|
468
|
+
pattern.lastIndex = 0;
|
|
469
|
+
text = text.replace(pattern, replacement);
|
|
470
|
+
}
|
|
471
|
+
return text;
|
|
472
|
+
}
|
|
302
473
|
function sanitise(params) {
|
|
303
474
|
const result = {};
|
|
304
475
|
for (const [k, v] of Object.entries(params)) {
|
|
305
|
-
|
|
476
|
+
if (SECRET_PATTERN2.test(k)) {
|
|
477
|
+
result[k] = "[REDACTED]";
|
|
478
|
+
} else {
|
|
479
|
+
result[k] = redactPii(String(v).slice(0, 500));
|
|
480
|
+
}
|
|
306
481
|
}
|
|
307
482
|
return result;
|
|
308
483
|
}
|
|
309
484
|
function hashValue(value) {
|
|
310
|
-
return (0,
|
|
485
|
+
return (0, import_crypto5.createHash)("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
|
|
311
486
|
}
|
|
312
487
|
function recordEvent(client, toolName, taskId, prompt, args, start, status, errorCode, result) {
|
|
313
488
|
const params = {};
|
|
314
489
|
args.forEach((arg, i) => {
|
|
315
490
|
params[`arg${i}`] = arg;
|
|
316
491
|
});
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
492
|
+
const outputsHash = result != null ? hashValue(result) : "";
|
|
493
|
+
const tokenizer = client.tokenizer;
|
|
494
|
+
if (tokenizer) {
|
|
495
|
+
const pseudonymizedParams = tokenizer.pseudonymizeParams(params);
|
|
496
|
+
const pseudonymizedPrompt = prompt ? tokenizer.pseudonymizeText(prompt) : "";
|
|
497
|
+
const response = result != null ? tokenizer.pseudonymizeText(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
|
|
498
|
+
const event = AuditEventSchema.parse({
|
|
499
|
+
agent_id: client.agentId,
|
|
500
|
+
agent_version: client.agentVersion,
|
|
501
|
+
human_operator_id: client.humanOperatorId,
|
|
502
|
+
task_id: taskId,
|
|
503
|
+
session_id: client.sessionId,
|
|
504
|
+
tool_name: toolName,
|
|
505
|
+
tool_parameters: pseudonymizedParams,
|
|
506
|
+
prompt: pseudonymizedPrompt,
|
|
507
|
+
inputs_hash: hashValue(args),
|
|
508
|
+
outputs_hash: outputsHash,
|
|
509
|
+
response,
|
|
510
|
+
status,
|
|
511
|
+
error_code: errorCode,
|
|
512
|
+
duration_ms: Math.round(performance.now() - start),
|
|
513
|
+
encrypted_token_map: tokenizer.encryptTokenMap(),
|
|
514
|
+
pseudonymization_version: "1.0"
|
|
515
|
+
});
|
|
516
|
+
client.record(event);
|
|
517
|
+
} else {
|
|
518
|
+
const response = result != null ? redactPii(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
|
|
519
|
+
const event = AuditEventSchema.parse({
|
|
520
|
+
agent_id: client.agentId,
|
|
521
|
+
agent_version: client.agentVersion,
|
|
522
|
+
human_operator_id: client.humanOperatorId,
|
|
523
|
+
task_id: taskId,
|
|
524
|
+
session_id: client.sessionId,
|
|
525
|
+
tool_name: toolName,
|
|
526
|
+
tool_parameters: sanitise(params),
|
|
527
|
+
prompt,
|
|
528
|
+
inputs_hash: hashValue(args),
|
|
529
|
+
outputs_hash: outputsHash,
|
|
530
|
+
response,
|
|
531
|
+
status,
|
|
532
|
+
error_code: errorCode,
|
|
533
|
+
duration_ms: Math.round(performance.now() - start)
|
|
534
|
+
});
|
|
535
|
+
client.record(event);
|
|
536
|
+
}
|
|
333
537
|
}
|
|
334
538
|
function wrapFn(client, fn, toolName, taskId, prompt) {
|
|
335
539
|
const wrapped = function(...args) {
|
|
@@ -375,12 +579,14 @@ function auditTool(client, fnOrTaskId, options) {
|
|
|
375
579
|
0 && (module.exports = {
|
|
376
580
|
AuditClient,
|
|
377
581
|
AuditEventSchema,
|
|
582
|
+
SessionTokenizer,
|
|
378
583
|
auditTool,
|
|
379
584
|
computeEventHash,
|
|
380
585
|
createFileEmitter,
|
|
381
586
|
createHttpEmitter,
|
|
382
587
|
generateKeypair,
|
|
383
588
|
hashValue,
|
|
589
|
+
redactPii,
|
|
384
590
|
signEvent,
|
|
385
591
|
verifyChain,
|
|
386
592
|
verifyEvent
|
package/dist/index.d.cts
CHANGED
|
@@ -21,12 +21,15 @@ declare const AuditEventSchema: z.ZodObject<{
|
|
|
21
21
|
inputs_source: z.ZodDefault<z.ZodString>;
|
|
22
22
|
outputs_hash: z.ZodDefault<z.ZodString>;
|
|
23
23
|
outputs_destination: z.ZodDefault<z.ZodString>;
|
|
24
|
+
response: z.ZodDefault<z.ZodString>;
|
|
24
25
|
status: z.ZodDefault<z.ZodString>;
|
|
25
26
|
error_code: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
26
27
|
duration_ms: z.ZodDefault<z.ZodNumber>;
|
|
27
28
|
retry_count: z.ZodDefault<z.ZodNumber>;
|
|
28
29
|
prev_event_hash: z.ZodDefault<z.ZodString>;
|
|
29
30
|
signature: z.ZodDefault<z.ZodString>;
|
|
31
|
+
encrypted_token_map: z.ZodDefault<z.ZodString>;
|
|
32
|
+
pseudonymization_version: z.ZodDefault<z.ZodString>;
|
|
30
33
|
}, "strip", z.ZodTypeAny, {
|
|
31
34
|
event_id: string;
|
|
32
35
|
agent_id: string;
|
|
@@ -49,11 +52,14 @@ declare const AuditEventSchema: z.ZodObject<{
|
|
|
49
52
|
inputs_source: string;
|
|
50
53
|
outputs_hash: string;
|
|
51
54
|
outputs_destination: string;
|
|
55
|
+
response: string;
|
|
52
56
|
error_code: string | null;
|
|
53
57
|
duration_ms: number;
|
|
54
58
|
retry_count: number;
|
|
55
59
|
prev_event_hash: string;
|
|
56
60
|
signature: string;
|
|
61
|
+
encrypted_token_map: string;
|
|
62
|
+
pseudonymization_version: string;
|
|
57
63
|
}, {
|
|
58
64
|
agent_id: string;
|
|
59
65
|
agent_version: string;
|
|
@@ -76,11 +82,14 @@ declare const AuditEventSchema: z.ZodObject<{
|
|
|
76
82
|
inputs_source?: string | undefined;
|
|
77
83
|
outputs_hash?: string | undefined;
|
|
78
84
|
outputs_destination?: string | undefined;
|
|
85
|
+
response?: string | undefined;
|
|
79
86
|
error_code?: string | null | undefined;
|
|
80
87
|
duration_ms?: number | undefined;
|
|
81
88
|
retry_count?: number | undefined;
|
|
82
89
|
prev_event_hash?: string | undefined;
|
|
83
90
|
signature?: string | undefined;
|
|
91
|
+
encrypted_token_map?: string | undefined;
|
|
92
|
+
pseudonymization_version?: string | undefined;
|
|
84
93
|
}>;
|
|
85
94
|
type AuditEvent = z.infer<typeof AuditEventSchema>;
|
|
86
95
|
|
|
@@ -103,6 +112,47 @@ type EmitFn = (eventDict: Record<string, unknown>) => void;
|
|
|
103
112
|
declare function createHttpEmitter(apiUrl: string, apiKey: string, fallbackPath: string): EmitFn;
|
|
104
113
|
declare function createFileEmitter(fallbackPath: string): EmitFn;
|
|
105
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Zero-knowledge PII pseudonymization with encrypted token map.
|
|
117
|
+
*
|
|
118
|
+
* Replaces PII with deterministic HMAC-based pseudonyms and encrypts the
|
|
119
|
+
* original-to-pseudonym mapping with the customer's X25519 public key.
|
|
120
|
+
* Wytness never sees the real PII — only the customer can decrypt the map.
|
|
121
|
+
*/
|
|
122
|
+
declare class SessionTokenizer {
|
|
123
|
+
private _hmacSecret;
|
|
124
|
+
private _encryptionPub;
|
|
125
|
+
private _piiFields;
|
|
126
|
+
private _tokenMap;
|
|
127
|
+
private _reverseMap;
|
|
128
|
+
/**
|
|
129
|
+
* @param hmacSecret 32-byte secret for deterministic pseudonyms.
|
|
130
|
+
* @param encryptionPublicKey 32-byte X25519 public key for encrypting the token map.
|
|
131
|
+
* @param piiFields Parameter names whose values should be fully pseudonymized.
|
|
132
|
+
*/
|
|
133
|
+
constructor(hmacSecret: Uint8Array, encryptionPublicKey: Uint8Array, piiFields?: string[]);
|
|
134
|
+
get tokenMap(): Record<string, string>;
|
|
135
|
+
private addMapping;
|
|
136
|
+
pseudonymizeValue(value: string, fieldPath?: string): string;
|
|
137
|
+
pseudonymizeText(text: string): string;
|
|
138
|
+
pseudonymizeParams(params: Record<string, unknown>): Record<string, unknown>;
|
|
139
|
+
/**
|
|
140
|
+
* Encrypt the token map with the customer's X25519 public key.
|
|
141
|
+
*
|
|
142
|
+
* Format: base64([ephemeral_pub 32B][nonce 24B][ciphertext])
|
|
143
|
+
* Uses NaCl box (X25519 + XSalsa20-Poly1305).
|
|
144
|
+
*/
|
|
145
|
+
encryptTokenMap(): string;
|
|
146
|
+
/**
|
|
147
|
+
* Decrypt a token map using the customer's X25519 private key.
|
|
148
|
+
*
|
|
149
|
+
* @param encryptedB64 Base64-encoded encrypted blob from encryptTokenMap().
|
|
150
|
+
* @param privateKey 32-byte X25519 private (secret) key.
|
|
151
|
+
* @returns The token map (pseudonym → original).
|
|
152
|
+
*/
|
|
153
|
+
static decryptTokenMap(encryptedB64: string, privateKey: Uint8Array): Record<string, string>;
|
|
154
|
+
}
|
|
155
|
+
|
|
106
156
|
interface AuditClientOptions {
|
|
107
157
|
agentId: string;
|
|
108
158
|
agentVersion?: string;
|
|
@@ -112,6 +162,9 @@ interface AuditClientOptions {
|
|
|
112
162
|
fallbackLogPath?: string;
|
|
113
163
|
httpApiKey?: string;
|
|
114
164
|
httpEndpoint?: string;
|
|
165
|
+
piiHmacSecret?: string;
|
|
166
|
+
encryptionPublicKey?: string;
|
|
167
|
+
piiFields?: string[];
|
|
115
168
|
}
|
|
116
169
|
declare class AuditClient {
|
|
117
170
|
readonly agentId: string;
|
|
@@ -121,8 +174,10 @@ declare class AuditClient {
|
|
|
121
174
|
private _sessionId;
|
|
122
175
|
private _secretKey;
|
|
123
176
|
private _emit;
|
|
177
|
+
private _tokenizer;
|
|
124
178
|
constructor(options: AuditClientOptions);
|
|
125
179
|
get sessionId(): string;
|
|
180
|
+
get tokenizer(): SessionTokenizer | null;
|
|
126
181
|
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
127
182
|
record(event: AuditEvent): void;
|
|
128
183
|
/**
|
|
@@ -133,6 +188,7 @@ declare class AuditClient {
|
|
|
133
188
|
flush(): Promise<void>;
|
|
134
189
|
}
|
|
135
190
|
|
|
191
|
+
declare function redactPii(text: string): string;
|
|
136
192
|
declare function hashValue(value: unknown): string;
|
|
137
193
|
interface AuditToolOptions {
|
|
138
194
|
toolName?: string;
|
|
@@ -153,4 +209,4 @@ interface AuditToolOptions {
|
|
|
153
209
|
*/
|
|
154
210
|
declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
|
|
155
211
|
|
|
156
|
-
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, signEvent, verifyChain, verifyEvent };
|
|
212
|
+
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, SessionTokenizer, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, redactPii, signEvent, verifyChain, verifyEvent };
|
package/dist/index.d.ts
CHANGED
|
@@ -21,12 +21,15 @@ declare const AuditEventSchema: z.ZodObject<{
|
|
|
21
21
|
inputs_source: z.ZodDefault<z.ZodString>;
|
|
22
22
|
outputs_hash: z.ZodDefault<z.ZodString>;
|
|
23
23
|
outputs_destination: z.ZodDefault<z.ZodString>;
|
|
24
|
+
response: z.ZodDefault<z.ZodString>;
|
|
24
25
|
status: z.ZodDefault<z.ZodString>;
|
|
25
26
|
error_code: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
26
27
|
duration_ms: z.ZodDefault<z.ZodNumber>;
|
|
27
28
|
retry_count: z.ZodDefault<z.ZodNumber>;
|
|
28
29
|
prev_event_hash: z.ZodDefault<z.ZodString>;
|
|
29
30
|
signature: z.ZodDefault<z.ZodString>;
|
|
31
|
+
encrypted_token_map: z.ZodDefault<z.ZodString>;
|
|
32
|
+
pseudonymization_version: z.ZodDefault<z.ZodString>;
|
|
30
33
|
}, "strip", z.ZodTypeAny, {
|
|
31
34
|
event_id: string;
|
|
32
35
|
agent_id: string;
|
|
@@ -49,11 +52,14 @@ declare const AuditEventSchema: z.ZodObject<{
|
|
|
49
52
|
inputs_source: string;
|
|
50
53
|
outputs_hash: string;
|
|
51
54
|
outputs_destination: string;
|
|
55
|
+
response: string;
|
|
52
56
|
error_code: string | null;
|
|
53
57
|
duration_ms: number;
|
|
54
58
|
retry_count: number;
|
|
55
59
|
prev_event_hash: string;
|
|
56
60
|
signature: string;
|
|
61
|
+
encrypted_token_map: string;
|
|
62
|
+
pseudonymization_version: string;
|
|
57
63
|
}, {
|
|
58
64
|
agent_id: string;
|
|
59
65
|
agent_version: string;
|
|
@@ -76,11 +82,14 @@ declare const AuditEventSchema: z.ZodObject<{
|
|
|
76
82
|
inputs_source?: string | undefined;
|
|
77
83
|
outputs_hash?: string | undefined;
|
|
78
84
|
outputs_destination?: string | undefined;
|
|
85
|
+
response?: string | undefined;
|
|
79
86
|
error_code?: string | null | undefined;
|
|
80
87
|
duration_ms?: number | undefined;
|
|
81
88
|
retry_count?: number | undefined;
|
|
82
89
|
prev_event_hash?: string | undefined;
|
|
83
90
|
signature?: string | undefined;
|
|
91
|
+
encrypted_token_map?: string | undefined;
|
|
92
|
+
pseudonymization_version?: string | undefined;
|
|
84
93
|
}>;
|
|
85
94
|
type AuditEvent = z.infer<typeof AuditEventSchema>;
|
|
86
95
|
|
|
@@ -103,6 +112,47 @@ type EmitFn = (eventDict: Record<string, unknown>) => void;
|
|
|
103
112
|
declare function createHttpEmitter(apiUrl: string, apiKey: string, fallbackPath: string): EmitFn;
|
|
104
113
|
declare function createFileEmitter(fallbackPath: string): EmitFn;
|
|
105
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Zero-knowledge PII pseudonymization with encrypted token map.
|
|
117
|
+
*
|
|
118
|
+
* Replaces PII with deterministic HMAC-based pseudonyms and encrypts the
|
|
119
|
+
* original-to-pseudonym mapping with the customer's X25519 public key.
|
|
120
|
+
* Wytness never sees the real PII — only the customer can decrypt the map.
|
|
121
|
+
*/
|
|
122
|
+
declare class SessionTokenizer {
|
|
123
|
+
private _hmacSecret;
|
|
124
|
+
private _encryptionPub;
|
|
125
|
+
private _piiFields;
|
|
126
|
+
private _tokenMap;
|
|
127
|
+
private _reverseMap;
|
|
128
|
+
/**
|
|
129
|
+
* @param hmacSecret 32-byte secret for deterministic pseudonyms.
|
|
130
|
+
* @param encryptionPublicKey 32-byte X25519 public key for encrypting the token map.
|
|
131
|
+
* @param piiFields Parameter names whose values should be fully pseudonymized.
|
|
132
|
+
*/
|
|
133
|
+
constructor(hmacSecret: Uint8Array, encryptionPublicKey: Uint8Array, piiFields?: string[]);
|
|
134
|
+
get tokenMap(): Record<string, string>;
|
|
135
|
+
private addMapping;
|
|
136
|
+
pseudonymizeValue(value: string, fieldPath?: string): string;
|
|
137
|
+
pseudonymizeText(text: string): string;
|
|
138
|
+
pseudonymizeParams(params: Record<string, unknown>): Record<string, unknown>;
|
|
139
|
+
/**
|
|
140
|
+
* Encrypt the token map with the customer's X25519 public key.
|
|
141
|
+
*
|
|
142
|
+
* Format: base64([ephemeral_pub 32B][nonce 24B][ciphertext])
|
|
143
|
+
* Uses NaCl box (X25519 + XSalsa20-Poly1305).
|
|
144
|
+
*/
|
|
145
|
+
encryptTokenMap(): string;
|
|
146
|
+
/**
|
|
147
|
+
* Decrypt a token map using the customer's X25519 private key.
|
|
148
|
+
*
|
|
149
|
+
* @param encryptedB64 Base64-encoded encrypted blob from encryptTokenMap().
|
|
150
|
+
* @param privateKey 32-byte X25519 private (secret) key.
|
|
151
|
+
* @returns The token map (pseudonym → original).
|
|
152
|
+
*/
|
|
153
|
+
static decryptTokenMap(encryptedB64: string, privateKey: Uint8Array): Record<string, string>;
|
|
154
|
+
}
|
|
155
|
+
|
|
106
156
|
interface AuditClientOptions {
|
|
107
157
|
agentId: string;
|
|
108
158
|
agentVersion?: string;
|
|
@@ -112,6 +162,9 @@ interface AuditClientOptions {
|
|
|
112
162
|
fallbackLogPath?: string;
|
|
113
163
|
httpApiKey?: string;
|
|
114
164
|
httpEndpoint?: string;
|
|
165
|
+
piiHmacSecret?: string;
|
|
166
|
+
encryptionPublicKey?: string;
|
|
167
|
+
piiFields?: string[];
|
|
115
168
|
}
|
|
116
169
|
declare class AuditClient {
|
|
117
170
|
readonly agentId: string;
|
|
@@ -121,8 +174,10 @@ declare class AuditClient {
|
|
|
121
174
|
private _sessionId;
|
|
122
175
|
private _secretKey;
|
|
123
176
|
private _emit;
|
|
177
|
+
private _tokenizer;
|
|
124
178
|
constructor(options: AuditClientOptions);
|
|
125
179
|
get sessionId(): string;
|
|
180
|
+
get tokenizer(): SessionTokenizer | null;
|
|
126
181
|
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
127
182
|
record(event: AuditEvent): void;
|
|
128
183
|
/**
|
|
@@ -133,6 +188,7 @@ declare class AuditClient {
|
|
|
133
188
|
flush(): Promise<void>;
|
|
134
189
|
}
|
|
135
190
|
|
|
191
|
+
declare function redactPii(text: string): string;
|
|
136
192
|
declare function hashValue(value: unknown): string;
|
|
137
193
|
interface AuditToolOptions {
|
|
138
194
|
toolName?: string;
|
|
@@ -153,4 +209,4 @@ interface AuditToolOptions {
|
|
|
153
209
|
*/
|
|
154
210
|
declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
|
|
155
211
|
|
|
156
|
-
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, signEvent, verifyChain, verifyEvent };
|
|
212
|
+
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, SessionTokenizer, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, redactPii, signEvent, verifyChain, verifyEvent };
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ var AuditEventSchema = z.object({
|
|
|
26
26
|
inputs_source: z.string().default(""),
|
|
27
27
|
outputs_hash: z.string().default(""),
|
|
28
28
|
outputs_destination: z.string().default(""),
|
|
29
|
+
response: z.string().default(""),
|
|
29
30
|
// Layer 5: Outcome
|
|
30
31
|
status: z.string().default("success"),
|
|
31
32
|
error_code: z.string().nullable().default(null),
|
|
@@ -33,7 +34,10 @@ var AuditEventSchema = z.object({
|
|
|
33
34
|
retry_count: z.number().int().default(0),
|
|
34
35
|
// Chain integrity
|
|
35
36
|
prev_event_hash: z.string().default(""),
|
|
36
|
-
signature: z.string().default("")
|
|
37
|
+
signature: z.string().default(""),
|
|
38
|
+
// Pseudonymization
|
|
39
|
+
encrypted_token_map: z.string().default(""),
|
|
40
|
+
pseudonymization_version: z.string().default("")
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
// src/signing.ts
|
|
@@ -187,6 +191,147 @@ async function flushPending() {
|
|
|
187
191
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
188
192
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
189
193
|
import { dirname as dirname2 } from "path";
|
|
194
|
+
|
|
195
|
+
// src/tokenizer.ts
|
|
196
|
+
import { createHmac } from "crypto";
|
|
197
|
+
import nacl2 from "tweetnacl";
|
|
198
|
+
import { encodeBase64 as encodeBase642, decodeBase64 as decodeBase642 } from "tweetnacl-util";
|
|
199
|
+
var SECRET_PATTERN = /key|secret|token|password|passwd|pwd|credential|auth/i;
|
|
200
|
+
var PII_PATTERNS = [
|
|
201
|
+
[/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "EMAIL"],
|
|
202
|
+
[/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, "SSN"],
|
|
203
|
+
[/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{3}\b/g, "TFN"],
|
|
204
|
+
[/\b(?:\d{4}[-\s]?){3}\d{4}\b/g, "CARD"],
|
|
205
|
+
[/\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "PHONE"]
|
|
206
|
+
];
|
|
207
|
+
function hmacPseudonym(value, secret, piiType = "PII") {
|
|
208
|
+
const digest = createHmac("sha256", secret).update(value, "utf-8").digest("hex");
|
|
209
|
+
return `${piiType}_${digest.slice(0, 8)}`;
|
|
210
|
+
}
|
|
211
|
+
var SessionTokenizer = class {
|
|
212
|
+
_hmacSecret;
|
|
213
|
+
_encryptionPub;
|
|
214
|
+
_piiFields;
|
|
215
|
+
_tokenMap = {};
|
|
216
|
+
// pseudonym → original
|
|
217
|
+
_reverseMap = {};
|
|
218
|
+
// original → pseudonym
|
|
219
|
+
/**
|
|
220
|
+
* @param hmacSecret 32-byte secret for deterministic pseudonyms.
|
|
221
|
+
* @param encryptionPublicKey 32-byte X25519 public key for encrypting the token map.
|
|
222
|
+
* @param piiFields Parameter names whose values should be fully pseudonymized.
|
|
223
|
+
*/
|
|
224
|
+
constructor(hmacSecret, encryptionPublicKey, piiFields = []) {
|
|
225
|
+
this._hmacSecret = hmacSecret;
|
|
226
|
+
this._encryptionPub = encryptionPublicKey;
|
|
227
|
+
this._piiFields = piiFields;
|
|
228
|
+
}
|
|
229
|
+
get tokenMap() {
|
|
230
|
+
return { ...this._tokenMap };
|
|
231
|
+
}
|
|
232
|
+
addMapping(original, pseudonym) {
|
|
233
|
+
this._tokenMap[pseudonym] = original;
|
|
234
|
+
this._reverseMap[original] = pseudonym;
|
|
235
|
+
}
|
|
236
|
+
pseudonymizeValue(value, fieldPath = "") {
|
|
237
|
+
if (!value || !value.trim())
|
|
238
|
+
return value;
|
|
239
|
+
if (this._piiFields.includes(fieldPath)) {
|
|
240
|
+
if (this._reverseMap[value])
|
|
241
|
+
return this._reverseMap[value];
|
|
242
|
+
const pseudonym = hmacPseudonym(value, this._hmacSecret);
|
|
243
|
+
this.addMapping(value, pseudonym);
|
|
244
|
+
return pseudonym;
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
pseudonymizeText(text) {
|
|
249
|
+
if (!text)
|
|
250
|
+
return text;
|
|
251
|
+
const known = Object.keys(this._reverseMap).sort(
|
|
252
|
+
(a, b) => b.length - a.length
|
|
253
|
+
);
|
|
254
|
+
for (const original of known) {
|
|
255
|
+
if (text.includes(original)) {
|
|
256
|
+
text = text.split(original).join(this._reverseMap[original]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [pattern, piiType] of PII_PATTERNS) {
|
|
260
|
+
pattern.lastIndex = 0;
|
|
261
|
+
text = text.replace(pattern, (matched) => {
|
|
262
|
+
if (this._reverseMap[matched])
|
|
263
|
+
return this._reverseMap[matched];
|
|
264
|
+
const pseudonym = hmacPseudonym(matched, this._hmacSecret, piiType);
|
|
265
|
+
this.addMapping(matched, pseudonym);
|
|
266
|
+
return pseudonym;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
271
|
+
pseudonymizeParams(params) {
|
|
272
|
+
const result = {};
|
|
273
|
+
for (const [k, v] of Object.entries(params)) {
|
|
274
|
+
if (SECRET_PATTERN.test(k)) {
|
|
275
|
+
result[k] = "[REDACTED]";
|
|
276
|
+
} else if (this._piiFields.includes(k)) {
|
|
277
|
+
const val = String(v).slice(0, 500);
|
|
278
|
+
result[k] = this.pseudonymizeValue(val, k);
|
|
279
|
+
} else {
|
|
280
|
+
const val = String(v).slice(0, 500);
|
|
281
|
+
result[k] = this.pseudonymizeText(val);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Encrypt the token map with the customer's X25519 public key.
|
|
288
|
+
*
|
|
289
|
+
* Format: base64([ephemeral_pub 32B][nonce 24B][ciphertext])
|
|
290
|
+
* Uses NaCl box (X25519 + XSalsa20-Poly1305).
|
|
291
|
+
*/
|
|
292
|
+
encryptTokenMap() {
|
|
293
|
+
const plaintext = Buffer.from(
|
|
294
|
+
JSON.stringify(this._tokenMap),
|
|
295
|
+
"utf-8"
|
|
296
|
+
);
|
|
297
|
+
const ephemeral = nacl2.box.keyPair();
|
|
298
|
+
const nonce = nacl2.randomBytes(24);
|
|
299
|
+
const ciphertext = nacl2.box(
|
|
300
|
+
plaintext,
|
|
301
|
+
nonce,
|
|
302
|
+
this._encryptionPub,
|
|
303
|
+
ephemeral.secretKey
|
|
304
|
+
);
|
|
305
|
+
if (!ciphertext) {
|
|
306
|
+
throw new Error("Encryption failed");
|
|
307
|
+
}
|
|
308
|
+
const packed = new Uint8Array(32 + 24 + ciphertext.length);
|
|
309
|
+
packed.set(ephemeral.publicKey, 0);
|
|
310
|
+
packed.set(nonce, 32);
|
|
311
|
+
packed.set(ciphertext, 56);
|
|
312
|
+
return encodeBase642(packed);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Decrypt a token map using the customer's X25519 private key.
|
|
316
|
+
*
|
|
317
|
+
* @param encryptedB64 Base64-encoded encrypted blob from encryptTokenMap().
|
|
318
|
+
* @param privateKey 32-byte X25519 private (secret) key.
|
|
319
|
+
* @returns The token map (pseudonym → original).
|
|
320
|
+
*/
|
|
321
|
+
static decryptTokenMap(encryptedB64, privateKey) {
|
|
322
|
+
const packed = decodeBase642(encryptedB64);
|
|
323
|
+
const ephemeralPub = packed.slice(0, 32);
|
|
324
|
+
const nonce = packed.slice(32, 56);
|
|
325
|
+
const ciphertext = packed.slice(56);
|
|
326
|
+
const plaintext = nacl2.box.open(ciphertext, nonce, ephemeralPub, privateKey);
|
|
327
|
+
if (!plaintext) {
|
|
328
|
+
throw new Error("Decryption failed \u2014 wrong key or corrupted data");
|
|
329
|
+
}
|
|
330
|
+
return JSON.parse(Buffer.from(plaintext).toString("utf-8"));
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// src/client.ts
|
|
190
335
|
var AuditClient = class {
|
|
191
336
|
agentId;
|
|
192
337
|
agentVersion;
|
|
@@ -195,6 +340,7 @@ var AuditClient = class {
|
|
|
195
340
|
_sessionId;
|
|
196
341
|
_secretKey;
|
|
197
342
|
_emit;
|
|
343
|
+
_tokenizer = null;
|
|
198
344
|
constructor(options) {
|
|
199
345
|
this.agentId = options.agentId;
|
|
200
346
|
this.agentVersion = options.agentVersion ?? "0.1.0";
|
|
@@ -224,10 +370,18 @@ var AuditClient = class {
|
|
|
224
370
|
} else {
|
|
225
371
|
this._emit = createFileEmitter(fallback);
|
|
226
372
|
}
|
|
373
|
+
if (options.piiHmacSecret && options.encryptionPublicKey) {
|
|
374
|
+
const hmacBytes = new Uint8Array(Buffer.from(options.piiHmacSecret, "base64"));
|
|
375
|
+
const pubBytes = new Uint8Array(Buffer.from(options.encryptionPublicKey, "base64"));
|
|
376
|
+
this._tokenizer = new SessionTokenizer(hmacBytes, pubBytes, options.piiFields ?? []);
|
|
377
|
+
}
|
|
227
378
|
}
|
|
228
379
|
get sessionId() {
|
|
229
380
|
return this._sessionId;
|
|
230
381
|
}
|
|
382
|
+
get tokenizer() {
|
|
383
|
+
return this._tokenizer;
|
|
384
|
+
}
|
|
231
385
|
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
232
386
|
record(event) {
|
|
233
387
|
try {
|
|
@@ -251,39 +405,87 @@ var AuditClient = class {
|
|
|
251
405
|
};
|
|
252
406
|
|
|
253
407
|
// src/decorator.ts
|
|
254
|
-
import { createHash as
|
|
255
|
-
var
|
|
408
|
+
import { createHash as createHash3 } from "crypto";
|
|
409
|
+
var SECRET_PATTERN2 = /key|secret|token|password|passwd|pwd|credential|auth/i;
|
|
410
|
+
var PII_PATTERNS2 = [
|
|
411
|
+
[/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[EMAIL_REDACTED]"],
|
|
412
|
+
[/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, "[SSN_REDACTED]"],
|
|
413
|
+
[/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{3}\b/g, "[TFN_REDACTED]"],
|
|
414
|
+
[/\b(?:\d{4}[-\s]?){3}\d{4}\b/g, "[CARD_REDACTED]"],
|
|
415
|
+
[/\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "[PHONE_REDACTED]"]
|
|
416
|
+
];
|
|
417
|
+
var RESPONSE_MAX_CHARS = 5e3;
|
|
418
|
+
function redactPii(text) {
|
|
419
|
+
for (const [pattern, replacement] of PII_PATTERNS2) {
|
|
420
|
+
pattern.lastIndex = 0;
|
|
421
|
+
text = text.replace(pattern, replacement);
|
|
422
|
+
}
|
|
423
|
+
return text;
|
|
424
|
+
}
|
|
256
425
|
function sanitise(params) {
|
|
257
426
|
const result = {};
|
|
258
427
|
for (const [k, v] of Object.entries(params)) {
|
|
259
|
-
|
|
428
|
+
if (SECRET_PATTERN2.test(k)) {
|
|
429
|
+
result[k] = "[REDACTED]";
|
|
430
|
+
} else {
|
|
431
|
+
result[k] = redactPii(String(v).slice(0, 500));
|
|
432
|
+
}
|
|
260
433
|
}
|
|
261
434
|
return result;
|
|
262
435
|
}
|
|
263
436
|
function hashValue(value) {
|
|
264
|
-
return
|
|
437
|
+
return createHash3("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
|
|
265
438
|
}
|
|
266
439
|
function recordEvent(client, toolName, taskId, prompt, args, start, status, errorCode, result) {
|
|
267
440
|
const params = {};
|
|
268
441
|
args.forEach((arg, i) => {
|
|
269
442
|
params[`arg${i}`] = arg;
|
|
270
443
|
});
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
444
|
+
const outputsHash = result != null ? hashValue(result) : "";
|
|
445
|
+
const tokenizer = client.tokenizer;
|
|
446
|
+
if (tokenizer) {
|
|
447
|
+
const pseudonymizedParams = tokenizer.pseudonymizeParams(params);
|
|
448
|
+
const pseudonymizedPrompt = prompt ? tokenizer.pseudonymizeText(prompt) : "";
|
|
449
|
+
const response = result != null ? tokenizer.pseudonymizeText(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
|
|
450
|
+
const event = AuditEventSchema.parse({
|
|
451
|
+
agent_id: client.agentId,
|
|
452
|
+
agent_version: client.agentVersion,
|
|
453
|
+
human_operator_id: client.humanOperatorId,
|
|
454
|
+
task_id: taskId,
|
|
455
|
+
session_id: client.sessionId,
|
|
456
|
+
tool_name: toolName,
|
|
457
|
+
tool_parameters: pseudonymizedParams,
|
|
458
|
+
prompt: pseudonymizedPrompt,
|
|
459
|
+
inputs_hash: hashValue(args),
|
|
460
|
+
outputs_hash: outputsHash,
|
|
461
|
+
response,
|
|
462
|
+
status,
|
|
463
|
+
error_code: errorCode,
|
|
464
|
+
duration_ms: Math.round(performance.now() - start),
|
|
465
|
+
encrypted_token_map: tokenizer.encryptTokenMap(),
|
|
466
|
+
pseudonymization_version: "1.0"
|
|
467
|
+
});
|
|
468
|
+
client.record(event);
|
|
469
|
+
} else {
|
|
470
|
+
const response = result != null ? redactPii(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
|
|
471
|
+
const event = AuditEventSchema.parse({
|
|
472
|
+
agent_id: client.agentId,
|
|
473
|
+
agent_version: client.agentVersion,
|
|
474
|
+
human_operator_id: client.humanOperatorId,
|
|
475
|
+
task_id: taskId,
|
|
476
|
+
session_id: client.sessionId,
|
|
477
|
+
tool_name: toolName,
|
|
478
|
+
tool_parameters: sanitise(params),
|
|
479
|
+
prompt,
|
|
480
|
+
inputs_hash: hashValue(args),
|
|
481
|
+
outputs_hash: outputsHash,
|
|
482
|
+
response,
|
|
483
|
+
status,
|
|
484
|
+
error_code: errorCode,
|
|
485
|
+
duration_ms: Math.round(performance.now() - start)
|
|
486
|
+
});
|
|
487
|
+
client.record(event);
|
|
488
|
+
}
|
|
287
489
|
}
|
|
288
490
|
function wrapFn(client, fn, toolName, taskId, prompt) {
|
|
289
491
|
const wrapped = function(...args) {
|
|
@@ -328,12 +530,14 @@ function auditTool(client, fnOrTaskId, options) {
|
|
|
328
530
|
export {
|
|
329
531
|
AuditClient,
|
|
330
532
|
AuditEventSchema,
|
|
533
|
+
SessionTokenizer,
|
|
331
534
|
auditTool,
|
|
332
535
|
computeEventHash,
|
|
333
536
|
createFileEmitter,
|
|
334
537
|
createHttpEmitter,
|
|
335
538
|
generateKeypair,
|
|
336
539
|
hashValue,
|
|
540
|
+
redactPii,
|
|
337
541
|
signEvent,
|
|
338
542
|
verifyChain,
|
|
339
543
|
verifyEvent
|
package/package.json
CHANGED