@wytness/sdk 0.6.0 → 0.7.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
@@ -284,7 +284,7 @@ var SessionTokenizer = class {
284
284
  pseudonymizeValue(value, fieldPath = "") {
285
285
  if (!value || !value.trim())
286
286
  return value;
287
- if (this._piiFields.includes(fieldPath)) {
287
+ if (this.matchPiiField(fieldPath)) {
288
288
  if (this._reverseMap[value])
289
289
  return this._reverseMap[value];
290
290
  const pseudonym = hmacPseudonym(value, this._hmacSecret);
@@ -293,6 +293,19 @@ var SessionTokenizer = class {
293
293
  }
294
294
  return value;
295
295
  }
296
+ matchPiiField(path) {
297
+ if (!path)
298
+ return false;
299
+ if (this._piiFields.includes(path))
300
+ return true;
301
+ const stripped = path.replace(/\[\d+\]/g, "");
302
+ if (this._piiFields.includes(stripped))
303
+ return true;
304
+ const wildcarded = path.replace(/\[\d+\]/g, "[]");
305
+ if (this._piiFields.includes(wildcarded))
306
+ return true;
307
+ return false;
308
+ }
296
309
  pseudonymizeText(text) {
297
310
  if (!text)
298
311
  return text;
@@ -316,20 +329,46 @@ var SessionTokenizer = class {
316
329
  }
317
330
  return text;
318
331
  }
332
+ /**
333
+ * Pseudonymize tool parameters, preserving nested structure.
334
+ *
335
+ * Walks objects and arrays recursively. Keys whose names match
336
+ * SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
337
+ * at paths listed in piiFields are fully pseudonymized; other strings are
338
+ * scanned with regex patterns.
339
+ *
340
+ * Paths use dot notation ("fields.abn") and [index] for arrays
341
+ * ("entities[0].name"). A piiFields entry matches by exact path, by
342
+ * index-stripped path ("entities.name"), or wildcard ("entities[].name").
343
+ */
319
344
  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);
345
+ return this.walk(params, "");
346
+ }
347
+ walk(value, path) {
348
+ if (value === null || value === void 0)
349
+ return value;
350
+ if (typeof value === "boolean" || typeof value === "number")
351
+ return value;
352
+ if (Array.isArray(value)) {
353
+ return value.map((item, i) => this.walk(item, `${path}[${i}]`));
354
+ }
355
+ if (typeof value === "object") {
356
+ const out = {};
357
+ for (const [k, v] of Object.entries(value)) {
358
+ if (SECRET_PATTERN.test(k)) {
359
+ out[k] = "[REDACTED]";
360
+ continue;
361
+ }
362
+ const childPath = path ? `${path}.${k}` : k;
363
+ out[k] = this.walk(v, childPath);
330
364
  }
365
+ return out;
331
366
  }
332
- return result;
367
+ const text = String(value).slice(0, 500);
368
+ if (this.matchPiiField(path)) {
369
+ return this.pseudonymizeValue(text, path);
370
+ }
371
+ return this.pseudonymizeText(text);
333
372
  }
334
373
  /**
335
374
  * Encrypt the token map with the customer's X25519 public key.
@@ -430,10 +469,21 @@ var AuditClient = class {
430
469
  get tokenizer() {
431
470
  return this._tokenizer;
432
471
  }
433
- /** Record an audit event. Never throws — errors are logged and swallowed. */
472
+ /**
473
+ * Record an audit event. Never throws — errors are logged and swallowed.
474
+ *
475
+ * If the client was configured with pseudonymization (piiHmacSecret +
476
+ * encryptionPublicKey), the event is automatically pseudonymized before
477
+ * signing — tool_parameters and response/prompt are walked and declared
478
+ * piiFields (supports dotted paths for nested keys) are replaced with
479
+ * deterministic tokens. The encrypted token map is attached.
480
+ */
434
481
  record(event) {
435
482
  try {
436
- const d = { ...event };
483
+ let d = { ...event };
484
+ if (this._tokenizer) {
485
+ d = this.applyTokenizer(d);
486
+ }
437
487
  d["prev_event_hash"] = this._prevEventHash;
438
488
  d["signature"] = signEvent(d, this._secretKey);
439
489
  this._prevEventHash = computeEventHash(d);
@@ -442,6 +492,22 @@ var AuditClient = class {
442
492
  console.error(`wytness: failed to record event: ${e}`);
443
493
  }
444
494
  }
495
+ applyTokenizer(d) {
496
+ const tokenizer = this._tokenizer;
497
+ const params = d["tool_parameters"] ?? {};
498
+ d["tool_parameters"] = tokenizer.pseudonymizeParams(params);
499
+ const prompt = d["prompt"];
500
+ if (prompt) {
501
+ d["prompt"] = tokenizer.pseudonymizeText(String(prompt));
502
+ }
503
+ const response = d["response"];
504
+ if (response) {
505
+ d["response"] = tokenizer.pseudonymizeText(String(response).slice(0, 5e3));
506
+ }
507
+ d["encrypted_token_map"] = tokenizer.encryptTokenMap();
508
+ d["pseudonymization_version"] = d["pseudonymization_version"] || "1.0";
509
+ return d;
510
+ }
445
511
  /**
446
512
  * Wait for all pending HTTP requests to complete.
447
513
  * Call this before process exit in serverless/short-lived environments
@@ -492,9 +558,7 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
492
558
  const outputsHash = result != null ? hashValue(result) : "";
493
559
  const tokenizer = client.tokenizer;
494
560
  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)) : "";
561
+ const responseRaw = result != null ? String(result).slice(0, RESPONSE_MAX_CHARS) : "";
498
562
  const event = AuditEventSchema.parse({
499
563
  agent_id: client.agentId,
500
564
  agent_version: client.agentVersion,
@@ -502,16 +566,14 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
502
566
  task_id: taskId,
503
567
  session_id: client.sessionId,
504
568
  tool_name: toolName,
505
- tool_parameters: pseudonymizedParams,
506
- prompt: pseudonymizedPrompt,
569
+ tool_parameters: params,
570
+ prompt,
507
571
  inputs_hash: hashValue(args),
508
572
  outputs_hash: outputsHash,
509
- response,
573
+ response: responseRaw,
510
574
  status,
511
575
  error_code: errorCode,
512
- duration_ms: Math.round(performance.now() - start),
513
- encrypted_token_map: tokenizer.encryptTokenMap(),
514
- pseudonymization_version: "1.0"
576
+ duration_ms: Math.round(performance.now() - start)
515
577
  });
516
578
  client.record(event);
517
579
  } else {
package/dist/index.d.cts CHANGED
@@ -134,8 +134,22 @@ declare class SessionTokenizer {
134
134
  get tokenMap(): Record<string, string>;
135
135
  private addMapping;
136
136
  pseudonymizeValue(value: string, fieldPath?: string): string;
137
+ private matchPiiField;
137
138
  pseudonymizeText(text: string): string;
138
- pseudonymizeParams(params: Record<string, unknown>): Record<string, unknown>;
139
+ /**
140
+ * Pseudonymize tool parameters, preserving nested structure.
141
+ *
142
+ * Walks objects and arrays recursively. Keys whose names match
143
+ * SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
144
+ * at paths listed in piiFields are fully pseudonymized; other strings are
145
+ * scanned with regex patterns.
146
+ *
147
+ * Paths use dot notation ("fields.abn") and [index] for arrays
148
+ * ("entities[0].name"). A piiFields entry matches by exact path, by
149
+ * index-stripped path ("entities.name"), or wildcard ("entities[].name").
150
+ */
151
+ pseudonymizeParams(params: unknown): unknown;
152
+ private walk;
139
153
  /**
140
154
  * Encrypt the token map with the customer's X25519 public key.
141
155
  *
@@ -178,8 +192,17 @@ declare class AuditClient {
178
192
  constructor(options: AuditClientOptions);
179
193
  get sessionId(): string;
180
194
  get tokenizer(): SessionTokenizer | null;
181
- /** Record an audit event. Never throws — errors are logged and swallowed. */
195
+ /**
196
+ * Record an audit event. Never throws — errors are logged and swallowed.
197
+ *
198
+ * If the client was configured with pseudonymization (piiHmacSecret +
199
+ * encryptionPublicKey), the event is automatically pseudonymized before
200
+ * signing — tool_parameters and response/prompt are walked and declared
201
+ * piiFields (supports dotted paths for nested keys) are replaced with
202
+ * deterministic tokens. The encrypted token map is attached.
203
+ */
182
204
  record(event: AuditEvent): void;
205
+ private applyTokenizer;
183
206
  /**
184
207
  * Wait for all pending HTTP requests to complete.
185
208
  * Call this before process exit in serverless/short-lived environments
package/dist/index.d.ts CHANGED
@@ -134,8 +134,22 @@ declare class SessionTokenizer {
134
134
  get tokenMap(): Record<string, string>;
135
135
  private addMapping;
136
136
  pseudonymizeValue(value: string, fieldPath?: string): string;
137
+ private matchPiiField;
137
138
  pseudonymizeText(text: string): string;
138
- pseudonymizeParams(params: Record<string, unknown>): Record<string, unknown>;
139
+ /**
140
+ * Pseudonymize tool parameters, preserving nested structure.
141
+ *
142
+ * Walks objects and arrays recursively. Keys whose names match
143
+ * SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
144
+ * at paths listed in piiFields are fully pseudonymized; other strings are
145
+ * scanned with regex patterns.
146
+ *
147
+ * Paths use dot notation ("fields.abn") and [index] for arrays
148
+ * ("entities[0].name"). A piiFields entry matches by exact path, by
149
+ * index-stripped path ("entities.name"), or wildcard ("entities[].name").
150
+ */
151
+ pseudonymizeParams(params: unknown): unknown;
152
+ private walk;
139
153
  /**
140
154
  * Encrypt the token map with the customer's X25519 public key.
141
155
  *
@@ -178,8 +192,17 @@ declare class AuditClient {
178
192
  constructor(options: AuditClientOptions);
179
193
  get sessionId(): string;
180
194
  get tokenizer(): SessionTokenizer | null;
181
- /** Record an audit event. Never throws — errors are logged and swallowed. */
195
+ /**
196
+ * Record an audit event. Never throws — errors are logged and swallowed.
197
+ *
198
+ * If the client was configured with pseudonymization (piiHmacSecret +
199
+ * encryptionPublicKey), the event is automatically pseudonymized before
200
+ * signing — tool_parameters and response/prompt are walked and declared
201
+ * piiFields (supports dotted paths for nested keys) are replaced with
202
+ * deterministic tokens. The encrypted token map is attached.
203
+ */
182
204
  record(event: AuditEvent): void;
205
+ private applyTokenizer;
183
206
  /**
184
207
  * Wait for all pending HTTP requests to complete.
185
208
  * Call this before process exit in serverless/short-lived environments
package/dist/index.js CHANGED
@@ -236,7 +236,7 @@ var SessionTokenizer = class {
236
236
  pseudonymizeValue(value, fieldPath = "") {
237
237
  if (!value || !value.trim())
238
238
  return value;
239
- if (this._piiFields.includes(fieldPath)) {
239
+ if (this.matchPiiField(fieldPath)) {
240
240
  if (this._reverseMap[value])
241
241
  return this._reverseMap[value];
242
242
  const pseudonym = hmacPseudonym(value, this._hmacSecret);
@@ -245,6 +245,19 @@ var SessionTokenizer = class {
245
245
  }
246
246
  return value;
247
247
  }
248
+ matchPiiField(path) {
249
+ if (!path)
250
+ return false;
251
+ if (this._piiFields.includes(path))
252
+ return true;
253
+ const stripped = path.replace(/\[\d+\]/g, "");
254
+ if (this._piiFields.includes(stripped))
255
+ return true;
256
+ const wildcarded = path.replace(/\[\d+\]/g, "[]");
257
+ if (this._piiFields.includes(wildcarded))
258
+ return true;
259
+ return false;
260
+ }
248
261
  pseudonymizeText(text) {
249
262
  if (!text)
250
263
  return text;
@@ -268,20 +281,46 @@ var SessionTokenizer = class {
268
281
  }
269
282
  return text;
270
283
  }
284
+ /**
285
+ * Pseudonymize tool parameters, preserving nested structure.
286
+ *
287
+ * Walks objects and arrays recursively. Keys whose names match
288
+ * SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
289
+ * at paths listed in piiFields are fully pseudonymized; other strings are
290
+ * scanned with regex patterns.
291
+ *
292
+ * Paths use dot notation ("fields.abn") and [index] for arrays
293
+ * ("entities[0].name"). A piiFields entry matches by exact path, by
294
+ * index-stripped path ("entities.name"), or wildcard ("entities[].name").
295
+ */
271
296
  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);
297
+ return this.walk(params, "");
298
+ }
299
+ walk(value, path) {
300
+ if (value === null || value === void 0)
301
+ return value;
302
+ if (typeof value === "boolean" || typeof value === "number")
303
+ return value;
304
+ if (Array.isArray(value)) {
305
+ return value.map((item, i) => this.walk(item, `${path}[${i}]`));
306
+ }
307
+ if (typeof value === "object") {
308
+ const out = {};
309
+ for (const [k, v] of Object.entries(value)) {
310
+ if (SECRET_PATTERN.test(k)) {
311
+ out[k] = "[REDACTED]";
312
+ continue;
313
+ }
314
+ const childPath = path ? `${path}.${k}` : k;
315
+ out[k] = this.walk(v, childPath);
282
316
  }
317
+ return out;
283
318
  }
284
- return result;
319
+ const text = String(value).slice(0, 500);
320
+ if (this.matchPiiField(path)) {
321
+ return this.pseudonymizeValue(text, path);
322
+ }
323
+ return this.pseudonymizeText(text);
285
324
  }
286
325
  /**
287
326
  * Encrypt the token map with the customer's X25519 public key.
@@ -382,10 +421,21 @@ var AuditClient = class {
382
421
  get tokenizer() {
383
422
  return this._tokenizer;
384
423
  }
385
- /** Record an audit event. Never throws — errors are logged and swallowed. */
424
+ /**
425
+ * Record an audit event. Never throws — errors are logged and swallowed.
426
+ *
427
+ * If the client was configured with pseudonymization (piiHmacSecret +
428
+ * encryptionPublicKey), the event is automatically pseudonymized before
429
+ * signing — tool_parameters and response/prompt are walked and declared
430
+ * piiFields (supports dotted paths for nested keys) are replaced with
431
+ * deterministic tokens. The encrypted token map is attached.
432
+ */
386
433
  record(event) {
387
434
  try {
388
- const d = { ...event };
435
+ let d = { ...event };
436
+ if (this._tokenizer) {
437
+ d = this.applyTokenizer(d);
438
+ }
389
439
  d["prev_event_hash"] = this._prevEventHash;
390
440
  d["signature"] = signEvent(d, this._secretKey);
391
441
  this._prevEventHash = computeEventHash(d);
@@ -394,6 +444,22 @@ var AuditClient = class {
394
444
  console.error(`wytness: failed to record event: ${e}`);
395
445
  }
396
446
  }
447
+ applyTokenizer(d) {
448
+ const tokenizer = this._tokenizer;
449
+ const params = d["tool_parameters"] ?? {};
450
+ d["tool_parameters"] = tokenizer.pseudonymizeParams(params);
451
+ const prompt = d["prompt"];
452
+ if (prompt) {
453
+ d["prompt"] = tokenizer.pseudonymizeText(String(prompt));
454
+ }
455
+ const response = d["response"];
456
+ if (response) {
457
+ d["response"] = tokenizer.pseudonymizeText(String(response).slice(0, 5e3));
458
+ }
459
+ d["encrypted_token_map"] = tokenizer.encryptTokenMap();
460
+ d["pseudonymization_version"] = d["pseudonymization_version"] || "1.0";
461
+ return d;
462
+ }
397
463
  /**
398
464
  * Wait for all pending HTTP requests to complete.
399
465
  * Call this before process exit in serverless/short-lived environments
@@ -444,9 +510,7 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
444
510
  const outputsHash = result != null ? hashValue(result) : "";
445
511
  const tokenizer = client.tokenizer;
446
512
  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)) : "";
513
+ const responseRaw = result != null ? String(result).slice(0, RESPONSE_MAX_CHARS) : "";
450
514
  const event = AuditEventSchema.parse({
451
515
  agent_id: client.agentId,
452
516
  agent_version: client.agentVersion,
@@ -454,16 +518,14 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
454
518
  task_id: taskId,
455
519
  session_id: client.sessionId,
456
520
  tool_name: toolName,
457
- tool_parameters: pseudonymizedParams,
458
- prompt: pseudonymizedPrompt,
521
+ tool_parameters: params,
522
+ prompt,
459
523
  inputs_hash: hashValue(args),
460
524
  outputs_hash: outputsHash,
461
- response,
525
+ response: responseRaw,
462
526
  status,
463
527
  error_code: errorCode,
464
- duration_ms: Math.round(performance.now() - start),
465
- encrypted_token_map: tokenizer.encryptTokenMap(),
466
- pseudonymization_version: "1.0"
528
+ duration_ms: Math.round(performance.now() - start)
467
529
  });
468
530
  client.record(event);
469
531
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wytness/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.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>",