@wytness/sdk 0.4.0 → 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/dist/index.cjs CHANGED
@@ -32,6 +32,7 @@ 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,
@@ -81,7 +82,10 @@ var AuditEventSchema = import_zod.z.object({
81
82
  retry_count: import_zod.z.number().int().default(0),
82
83
  // Chain integrity
83
84
  prev_event_hash: import_zod.z.string().default(""),
84
- 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("")
85
89
  });
86
90
 
87
91
  // src/signing.ts
@@ -232,9 +236,150 @@ async function flushPending() {
232
236
  }
233
237
 
234
238
  // src/client.ts
235
- var import_crypto3 = require("crypto");
239
+ var import_crypto4 = require("crypto");
236
240
  var import_fs2 = require("fs");
237
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
238
383
  var AuditClient = class {
239
384
  agentId;
240
385
  agentVersion;
@@ -243,11 +388,12 @@ var AuditClient = class {
243
388
  _sessionId;
244
389
  _secretKey;
245
390
  _emit;
391
+ _tokenizer = null;
246
392
  constructor(options) {
247
393
  this.agentId = options.agentId;
248
394
  this.agentVersion = options.agentVersion ?? "0.1.0";
249
395
  this.humanOperatorId = options.humanOperatorId ?? "unknown";
250
- this._sessionId = (0, import_crypto3.randomUUID)();
396
+ this._sessionId = (0, import_crypto4.randomUUID)();
251
397
  if (options.signingKey) {
252
398
  this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
253
399
  } else {
@@ -272,10 +418,18 @@ var AuditClient = class {
272
418
  } else {
273
419
  this._emit = createFileEmitter(fallback);
274
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
+ }
275
426
  }
276
427
  get sessionId() {
277
428
  return this._sessionId;
278
429
  }
430
+ get tokenizer() {
431
+ return this._tokenizer;
432
+ }
279
433
  /** Record an audit event. Never throws — errors are logged and swallowed. */
280
434
  record(event) {
281
435
  try {
@@ -299,9 +453,9 @@ var AuditClient = class {
299
453
  };
300
454
 
301
455
  // src/decorator.ts
302
- var import_crypto4 = require("crypto");
303
- var SECRET_PATTERN = /key|secret|token|password|passwd|pwd|credential|auth/i;
304
- var PII_PATTERNS = [
456
+ var import_crypto5 = require("crypto");
457
+ var SECRET_PATTERN2 = /key|secret|token|password|passwd|pwd|credential|auth/i;
458
+ var PII_PATTERNS2 = [
305
459
  [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[EMAIL_REDACTED]"],
306
460
  [/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, "[SSN_REDACTED]"],
307
461
  [/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{3}\b/g, "[TFN_REDACTED]"],
@@ -310,7 +464,7 @@ var PII_PATTERNS = [
310
464
  ];
311
465
  var RESPONSE_MAX_CHARS = 5e3;
312
466
  function redactPii(text) {
313
- for (const [pattern, replacement] of PII_PATTERNS) {
467
+ for (const [pattern, replacement] of PII_PATTERNS2) {
314
468
  pattern.lastIndex = 0;
315
469
  text = text.replace(pattern, replacement);
316
470
  }
@@ -319,7 +473,7 @@ function redactPii(text) {
319
473
  function sanitise(params) {
320
474
  const result = {};
321
475
  for (const [k, v] of Object.entries(params)) {
322
- if (SECRET_PATTERN.test(k)) {
476
+ if (SECRET_PATTERN2.test(k)) {
323
477
  result[k] = "[REDACTED]";
324
478
  } else {
325
479
  result[k] = redactPii(String(v).slice(0, 500));
@@ -328,7 +482,7 @@ function sanitise(params) {
328
482
  return result;
329
483
  }
330
484
  function hashValue(value) {
331
- return (0, import_crypto4.createHash)("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
485
+ return (0, import_crypto5.createHash)("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
332
486
  }
333
487
  function recordEvent(client, toolName, taskId, prompt, args, start, status, errorCode, result) {
334
488
  const params = {};
@@ -336,24 +490,50 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
336
490
  params[`arg${i}`] = arg;
337
491
  });
338
492
  const outputsHash = result != null ? hashValue(result) : "";
339
- const response = result != null ? redactPii(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
340
- const event = AuditEventSchema.parse({
341
- agent_id: client.agentId,
342
- agent_version: client.agentVersion,
343
- human_operator_id: client.humanOperatorId,
344
- task_id: taskId,
345
- session_id: client.sessionId,
346
- tool_name: toolName,
347
- tool_parameters: sanitise(params),
348
- prompt,
349
- inputs_hash: hashValue(args),
350
- outputs_hash: outputsHash,
351
- response,
352
- status,
353
- error_code: errorCode,
354
- duration_ms: Math.round(performance.now() - start)
355
- });
356
- client.record(event);
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
+ }
357
537
  }
358
538
  function wrapFn(client, fn, toolName, taskId, prompt) {
359
539
  const wrapped = function(...args) {
@@ -399,6 +579,7 @@ function auditTool(client, fnOrTaskId, options) {
399
579
  0 && (module.exports = {
400
580
  AuditClient,
401
581
  AuditEventSchema,
582
+ SessionTokenizer,
402
583
  auditTool,
403
584
  computeEventHash,
404
585
  createFileEmitter,
package/dist/index.d.cts CHANGED
@@ -28,6 +28,8 @@ declare const AuditEventSchema: z.ZodObject<{
28
28
  retry_count: z.ZodDefault<z.ZodNumber>;
29
29
  prev_event_hash: z.ZodDefault<z.ZodString>;
30
30
  signature: z.ZodDefault<z.ZodString>;
31
+ encrypted_token_map: z.ZodDefault<z.ZodString>;
32
+ pseudonymization_version: z.ZodDefault<z.ZodString>;
31
33
  }, "strip", z.ZodTypeAny, {
32
34
  event_id: string;
33
35
  agent_id: string;
@@ -56,6 +58,8 @@ declare const AuditEventSchema: z.ZodObject<{
56
58
  retry_count: number;
57
59
  prev_event_hash: string;
58
60
  signature: string;
61
+ encrypted_token_map: string;
62
+ pseudonymization_version: string;
59
63
  }, {
60
64
  agent_id: string;
61
65
  agent_version: string;
@@ -84,6 +88,8 @@ declare const AuditEventSchema: z.ZodObject<{
84
88
  retry_count?: number | undefined;
85
89
  prev_event_hash?: string | undefined;
86
90
  signature?: string | undefined;
91
+ encrypted_token_map?: string | undefined;
92
+ pseudonymization_version?: string | undefined;
87
93
  }>;
88
94
  type AuditEvent = z.infer<typeof AuditEventSchema>;
89
95
 
@@ -106,6 +112,47 @@ type EmitFn = (eventDict: Record<string, unknown>) => void;
106
112
  declare function createHttpEmitter(apiUrl: string, apiKey: string, fallbackPath: string): EmitFn;
107
113
  declare function createFileEmitter(fallbackPath: string): EmitFn;
108
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
+
109
156
  interface AuditClientOptions {
110
157
  agentId: string;
111
158
  agentVersion?: string;
@@ -115,6 +162,9 @@ interface AuditClientOptions {
115
162
  fallbackLogPath?: string;
116
163
  httpApiKey?: string;
117
164
  httpEndpoint?: string;
165
+ piiHmacSecret?: string;
166
+ encryptionPublicKey?: string;
167
+ piiFields?: string[];
118
168
  }
119
169
  declare class AuditClient {
120
170
  readonly agentId: string;
@@ -124,8 +174,10 @@ declare class AuditClient {
124
174
  private _sessionId;
125
175
  private _secretKey;
126
176
  private _emit;
177
+ private _tokenizer;
127
178
  constructor(options: AuditClientOptions);
128
179
  get sessionId(): string;
180
+ get tokenizer(): SessionTokenizer | null;
129
181
  /** Record an audit event. Never throws — errors are logged and swallowed. */
130
182
  record(event: AuditEvent): void;
131
183
  /**
@@ -157,4 +209,4 @@ interface AuditToolOptions {
157
209
  */
158
210
  declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
159
211
 
160
- export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, redactPii, 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
@@ -28,6 +28,8 @@ declare const AuditEventSchema: z.ZodObject<{
28
28
  retry_count: z.ZodDefault<z.ZodNumber>;
29
29
  prev_event_hash: z.ZodDefault<z.ZodString>;
30
30
  signature: z.ZodDefault<z.ZodString>;
31
+ encrypted_token_map: z.ZodDefault<z.ZodString>;
32
+ pseudonymization_version: z.ZodDefault<z.ZodString>;
31
33
  }, "strip", z.ZodTypeAny, {
32
34
  event_id: string;
33
35
  agent_id: string;
@@ -56,6 +58,8 @@ declare const AuditEventSchema: z.ZodObject<{
56
58
  retry_count: number;
57
59
  prev_event_hash: string;
58
60
  signature: string;
61
+ encrypted_token_map: string;
62
+ pseudonymization_version: string;
59
63
  }, {
60
64
  agent_id: string;
61
65
  agent_version: string;
@@ -84,6 +88,8 @@ declare const AuditEventSchema: z.ZodObject<{
84
88
  retry_count?: number | undefined;
85
89
  prev_event_hash?: string | undefined;
86
90
  signature?: string | undefined;
91
+ encrypted_token_map?: string | undefined;
92
+ pseudonymization_version?: string | undefined;
87
93
  }>;
88
94
  type AuditEvent = z.infer<typeof AuditEventSchema>;
89
95
 
@@ -106,6 +112,47 @@ type EmitFn = (eventDict: Record<string, unknown>) => void;
106
112
  declare function createHttpEmitter(apiUrl: string, apiKey: string, fallbackPath: string): EmitFn;
107
113
  declare function createFileEmitter(fallbackPath: string): EmitFn;
108
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
+
109
156
  interface AuditClientOptions {
110
157
  agentId: string;
111
158
  agentVersion?: string;
@@ -115,6 +162,9 @@ interface AuditClientOptions {
115
162
  fallbackLogPath?: string;
116
163
  httpApiKey?: string;
117
164
  httpEndpoint?: string;
165
+ piiHmacSecret?: string;
166
+ encryptionPublicKey?: string;
167
+ piiFields?: string[];
118
168
  }
119
169
  declare class AuditClient {
120
170
  readonly agentId: string;
@@ -124,8 +174,10 @@ declare class AuditClient {
124
174
  private _sessionId;
125
175
  private _secretKey;
126
176
  private _emit;
177
+ private _tokenizer;
127
178
  constructor(options: AuditClientOptions);
128
179
  get sessionId(): string;
180
+ get tokenizer(): SessionTokenizer | null;
129
181
  /** Record an audit event. Never throws — errors are logged and swallowed. */
130
182
  record(event: AuditEvent): void;
131
183
  /**
@@ -157,4 +209,4 @@ interface AuditToolOptions {
157
209
  */
158
210
  declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
159
211
 
160
- export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, redactPii, 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
@@ -34,7 +34,10 @@ var AuditEventSchema = z.object({
34
34
  retry_count: z.number().int().default(0),
35
35
  // Chain integrity
36
36
  prev_event_hash: z.string().default(""),
37
- signature: z.string().default("")
37
+ signature: z.string().default(""),
38
+ // Pseudonymization
39
+ encrypted_token_map: z.string().default(""),
40
+ pseudonymization_version: z.string().default("")
38
41
  });
39
42
 
40
43
  // src/signing.ts
@@ -188,6 +191,147 @@ async function flushPending() {
188
191
  import { randomUUID as randomUUID2 } from "crypto";
189
192
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
190
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
191
335
  var AuditClient = class {
192
336
  agentId;
193
337
  agentVersion;
@@ -196,6 +340,7 @@ var AuditClient = class {
196
340
  _sessionId;
197
341
  _secretKey;
198
342
  _emit;
343
+ _tokenizer = null;
199
344
  constructor(options) {
200
345
  this.agentId = options.agentId;
201
346
  this.agentVersion = options.agentVersion ?? "0.1.0";
@@ -225,10 +370,18 @@ var AuditClient = class {
225
370
  } else {
226
371
  this._emit = createFileEmitter(fallback);
227
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
+ }
228
378
  }
229
379
  get sessionId() {
230
380
  return this._sessionId;
231
381
  }
382
+ get tokenizer() {
383
+ return this._tokenizer;
384
+ }
232
385
  /** Record an audit event. Never throws — errors are logged and swallowed. */
233
386
  record(event) {
234
387
  try {
@@ -252,9 +405,9 @@ var AuditClient = class {
252
405
  };
253
406
 
254
407
  // src/decorator.ts
255
- import { createHash as createHash2 } from "crypto";
256
- var SECRET_PATTERN = /key|secret|token|password|passwd|pwd|credential|auth/i;
257
- var PII_PATTERNS = [
408
+ import { createHash as createHash3 } from "crypto";
409
+ var SECRET_PATTERN2 = /key|secret|token|password|passwd|pwd|credential|auth/i;
410
+ var PII_PATTERNS2 = [
258
411
  [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[EMAIL_REDACTED]"],
259
412
  [/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, "[SSN_REDACTED]"],
260
413
  [/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{3}\b/g, "[TFN_REDACTED]"],
@@ -263,7 +416,7 @@ var PII_PATTERNS = [
263
416
  ];
264
417
  var RESPONSE_MAX_CHARS = 5e3;
265
418
  function redactPii(text) {
266
- for (const [pattern, replacement] of PII_PATTERNS) {
419
+ for (const [pattern, replacement] of PII_PATTERNS2) {
267
420
  pattern.lastIndex = 0;
268
421
  text = text.replace(pattern, replacement);
269
422
  }
@@ -272,7 +425,7 @@ function redactPii(text) {
272
425
  function sanitise(params) {
273
426
  const result = {};
274
427
  for (const [k, v] of Object.entries(params)) {
275
- if (SECRET_PATTERN.test(k)) {
428
+ if (SECRET_PATTERN2.test(k)) {
276
429
  result[k] = "[REDACTED]";
277
430
  } else {
278
431
  result[k] = redactPii(String(v).slice(0, 500));
@@ -281,7 +434,7 @@ function sanitise(params) {
281
434
  return result;
282
435
  }
283
436
  function hashValue(value) {
284
- return createHash2("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
437
+ return createHash3("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
285
438
  }
286
439
  function recordEvent(client, toolName, taskId, prompt, args, start, status, errorCode, result) {
287
440
  const params = {};
@@ -289,24 +442,50 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
289
442
  params[`arg${i}`] = arg;
290
443
  });
291
444
  const outputsHash = result != null ? hashValue(result) : "";
292
- const response = result != null ? redactPii(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
293
- const event = AuditEventSchema.parse({
294
- agent_id: client.agentId,
295
- agent_version: client.agentVersion,
296
- human_operator_id: client.humanOperatorId,
297
- task_id: taskId,
298
- session_id: client.sessionId,
299
- tool_name: toolName,
300
- tool_parameters: sanitise(params),
301
- prompt,
302
- inputs_hash: hashValue(args),
303
- outputs_hash: outputsHash,
304
- response,
305
- status,
306
- error_code: errorCode,
307
- duration_ms: Math.round(performance.now() - start)
308
- });
309
- client.record(event);
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
+ }
310
489
  }
311
490
  function wrapFn(client, fn, toolName, taskId, prompt) {
312
491
  const wrapped = function(...args) {
@@ -351,6 +530,7 @@ function auditTool(client, fnOrTaskId, options) {
351
530
  export {
352
531
  AuditClient,
353
532
  AuditEventSchema,
533
+ SessionTokenizer,
354
534
  auditTool,
355
535
  computeEventHash,
356
536
  createFileEmitter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wytness/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript SDK for Wytness — audit logging for AI agents with cryptographic signing and chain integrity",
5
5
  "license": "MIT",
6
6
  "author": "Wytness <hello@wytness.dev>",