@wytness/sdk 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,8 +33,8 @@ await sendEmail("team@company.com", "Report", "Weekly summary");
33
33
  - **Cryptographic signing** — every event is signed with Ed25519 (tweetnacl)
34
34
  - **Hash chaining** — tamper-evident chain of events per agent session
35
35
  - **Automatic redaction** — secrets in parameters are automatically redacted
36
- - **Kafka or HTTP** — stream events via Kafka or HTTP API
37
- - **File fallback** — events are saved locally if the network is unavailable
36
+ - **HTTP transport** — stream events to api.wytness.dev over HTTPS
37
+ - **File fallback** — events are saved locally if the API is unreachable
38
38
 
39
39
  ## HTTP Emitter (Recommended)
40
40
 
@@ -58,17 +58,54 @@ const client = new AuditClient({
58
58
  | `agentId` | `string` | required | Unique identifier for the agent |
59
59
  | `agentVersion` | `string` | `"0.1.0"` | Version of the agent |
60
60
  | `humanOperatorId` | `string` | `"unknown"` | ID of the human overseeing the agent |
61
- | `signingKeyPath` | `string` | `"./keys/signing.key"` | Path to Ed25519 private key |
62
- | `kafkaBootstrapServers` | `string\|undefined` | `undefined` | Kafka bootstrap servers |
63
- | `kafkaTopic` | `string` | `"wytness-events"` | Kafka topic name |
64
- | `httpEndpoint` | `string\|undefined` | `undefined` | HTTP API endpoint URL |
61
+ | `signingKey` | `string\|undefined` | `undefined` | Base64-encoded Ed25519 private key (for serverless) |
62
+ | `signingKeyPath` | `string` | `"./keys/signing.key"` | Path to Ed25519 private key. Ignored when `signingKey` is set. |
65
63
  | `httpApiKey` | `string\|undefined` | `undefined` | API key for HTTP endpoint |
64
+ | `httpEndpoint` | `string` | `"https://api.wytness.dev"` | HTTP API endpoint URL |
66
65
  | `fallbackLogPath` | `string` | `"./audit_fallback.jsonl"` | Local fallback file path |
67
66
 
68
67
  ### `auditTool(client, fn, options)`
69
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
70
 
71
+ ### `client.sessionId`
72
+
73
+ Read-only property returning the UUID for this client instance.
74
+
75
+ ### `client.flush()`
76
+
77
+ Returns a Promise that resolves when all pending HTTP requests complete. Call before process exit in serverless/short-lived environments.
78
+
79
+ ```typescript
80
+ await client.flush();
81
+ ```
82
+
83
+ ### `hashValue(value)`
84
+
85
+ SHA-256 hash of any value. Use when recording events manually to compute `inputs_hash` / `outputs_hash`.
86
+
87
+ ```typescript
88
+ import { hashValue } from "@wytness/sdk";
89
+
90
+ const result = await myTool(data);
91
+ const outputsHash = hashValue(result);
92
+ ```
93
+
94
+ ### `AuditEventSchema`
95
+
96
+ A Zod schema that validates and provides defaults for audit events. Use it when recording events manually:
97
+
98
+ ```typescript
99
+ import { AuditEventSchema } from "@wytness/sdk";
100
+
101
+ const event = AuditEventSchema.parse({
102
+ agent_id: "my-agent",
103
+ tool_name: "my_tool",
104
+ // ... other required fields
105
+ // Optional fields get sensible defaults (event_id, timestamp, status, etc.)
106
+ });
107
+ ```
108
+
72
109
  ## Exports
73
110
 
74
111
  ```typescript
@@ -76,6 +113,7 @@ import {
76
113
  AuditClient,
77
114
  AuditClientOptions,
78
115
  auditTool,
116
+ hashValue,
79
117
  AuditEvent,
80
118
  AuditEventSchema,
81
119
  generateKeypair,
@@ -83,7 +121,8 @@ import {
83
121
  verifyEvent,
84
122
  computeEventHash,
85
123
  verifyChain,
86
- createEmitter,
124
+ createHttpEmitter,
125
+ createFileEmitter,
87
126
  } from "@wytness/sdk";
88
127
  ```
89
128
 
package/dist/index.cjs CHANGED
@@ -37,6 +37,7 @@ __export(src_exports, {
37
37
  createFileEmitter: () => createFileEmitter,
38
38
  createHttpEmitter: () => createHttpEmitter,
39
39
  generateKeypair: () => generateKeypair,
40
+ hashValue: () => hashValue,
40
41
  signEvent: () => signEvent,
41
42
  verifyChain: () => verifyChain,
42
43
  verifyEvent: () => verifyEvent
@@ -143,30 +144,79 @@ function fileEmit(path, eventDict) {
143
144
  (0, import_fs.mkdirSync)((0, import_path.dirname)(path), { recursive: true });
144
145
  (0, import_fs.appendFileSync)(path, JSON.stringify(eventDict) + "\n");
145
146
  }
147
+ var pendingRequests = /* @__PURE__ */ new Set();
148
+ var REPLAY_COOLDOWN_MS = 1e4;
149
+ async function sendOne(apiUrl, apiKey, body) {
150
+ const resp = await fetch(`${apiUrl}/ingest`, {
151
+ method: "POST",
152
+ headers: {
153
+ "Content-Type": "application/json",
154
+ "X-API-Key": apiKey
155
+ },
156
+ body,
157
+ signal: AbortSignal.timeout(1e4)
158
+ });
159
+ if (resp.status !== 201) {
160
+ throw new Error(`HTTP ${resp.status}`);
161
+ }
162
+ return true;
163
+ }
146
164
  function createHttpEmitter(apiUrl, apiKey, fallbackPath) {
165
+ let replaying = false;
166
+ let lastReplayAttempt = 0;
167
+ const tryReplay = async () => {
168
+ const now = Date.now();
169
+ if (now - lastReplayAttempt < REPLAY_COOLDOWN_MS)
170
+ return;
171
+ if (replaying)
172
+ return;
173
+ replaying = true;
174
+ try {
175
+ lastReplayAttempt = now;
176
+ if (!(0, import_fs.existsSync)(fallbackPath))
177
+ return;
178
+ const stat = (0, import_fs.statSync)(fallbackPath);
179
+ if (stat.size === 0)
180
+ return;
181
+ const lines = (0, import_fs.readFileSync)(fallbackPath, "utf-8").split("\n").filter((l) => l.trim());
182
+ if (lines.length === 0)
183
+ return;
184
+ const remaining = [];
185
+ for (const line of lines) {
186
+ try {
187
+ await sendOne(apiUrl, apiKey, line);
188
+ } catch {
189
+ remaining.push(line);
190
+ }
191
+ }
192
+ if (remaining.length > 0) {
193
+ (0, import_fs.writeFileSync)(fallbackPath, remaining.join("\n") + "\n");
194
+ } else {
195
+ (0, import_fs.unlinkSync)(fallbackPath);
196
+ }
197
+ const replayed = lines.length - remaining.length;
198
+ if (replayed > 0) {
199
+ console.log(`wytness: replayed ${replayed} fallback events`);
200
+ }
201
+ } catch {
202
+ } finally {
203
+ replaying = false;
204
+ }
205
+ };
147
206
  return (eventDict) => {
148
207
  const doSend = async () => {
149
208
  try {
150
209
  const body = JSON.stringify(eventDict);
151
- const resp = await fetch(`${apiUrl}/ingest`, {
152
- method: "POST",
153
- headers: {
154
- "Content-Type": "application/json",
155
- "X-API-Key": apiKey
156
- },
157
- body,
158
- signal: AbortSignal.timeout(1e4)
159
- });
160
- if (resp.status !== 201) {
161
- console.error(`HTTP ingest failed: ${resp.status}`);
162
- fileEmit(fallbackPath, eventDict);
163
- }
210
+ await sendOne(apiUrl, apiKey, body);
211
+ tryReplay();
164
212
  } catch (e) {
165
213
  console.error(`HTTP ingest error: ${e}`);
166
214
  fileEmit(fallbackPath, eventDict);
167
215
  }
168
216
  };
169
- doSend().catch(() => fileEmit(fallbackPath, eventDict));
217
+ const p = doSend();
218
+ pendingRequests.add(p);
219
+ p.finally(() => pendingRequests.delete(p));
170
220
  };
171
221
  }
172
222
  function createFileEmitter(fallbackPath) {
@@ -174,6 +224,9 @@ function createFileEmitter(fallbackPath) {
174
224
  fileEmit(fallbackPath, eventDict);
175
225
  };
176
226
  }
227
+ async function flushPending() {
228
+ await Promise.allSettled([...pendingRequests]);
229
+ }
177
230
 
178
231
  // src/client.ts
179
232
  var import_crypto3 = require("crypto");
@@ -192,15 +245,19 @@ var AuditClient = class {
192
245
  this.agentVersion = options.agentVersion ?? "0.1.0";
193
246
  this.humanOperatorId = options.humanOperatorId ?? "unknown";
194
247
  this._sessionId = (0, import_crypto3.randomUUID)();
195
- const keyPath = options.signingKeyPath ?? "./keys/signing.key";
196
- if ((0, import_fs2.existsSync)(keyPath)) {
197
- const raw = (0, import_fs2.readFileSync)(keyPath);
198
- this._secretKey = new Uint8Array(raw);
248
+ if (options.signingKey) {
249
+ this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
199
250
  } else {
200
- const kp = generateKeypair();
201
- this._secretKey = kp.secretKey;
202
- (0, import_fs2.mkdirSync)((0, import_path2.dirname)(keyPath), { recursive: true });
203
- (0, import_fs2.writeFileSync)(keyPath, Buffer.from(kp.secretKey));
251
+ const keyPath = options.signingKeyPath ?? "./keys/signing.key";
252
+ if ((0, import_fs2.existsSync)(keyPath)) {
253
+ const raw = (0, import_fs2.readFileSync)(keyPath);
254
+ this._secretKey = new Uint8Array(raw);
255
+ } else {
256
+ const kp = generateKeypair();
257
+ this._secretKey = kp.secretKey;
258
+ (0, import_fs2.mkdirSync)((0, import_path2.dirname)(keyPath), { recursive: true });
259
+ (0, import_fs2.writeFileSync)(keyPath, Buffer.from(kp.secretKey));
260
+ }
204
261
  }
205
262
  const fallback = options.fallbackLogPath ?? "./audit_fallback.jsonl";
206
263
  if (options.httpApiKey) {
@@ -216,12 +273,25 @@ var AuditClient = class {
216
273
  get sessionId() {
217
274
  return this._sessionId;
218
275
  }
276
+ /** Record an audit event. Never throws — errors are logged and swallowed. */
219
277
  record(event) {
220
- const d = { ...event };
221
- d["prev_event_hash"] = this._prevEventHash;
222
- d["signature"] = signEvent(d, this._secretKey);
223
- this._prevEventHash = computeEventHash(d);
224
- this._emit(d);
278
+ try {
279
+ const d = { ...event };
280
+ d["prev_event_hash"] = this._prevEventHash;
281
+ d["signature"] = signEvent(d, this._secretKey);
282
+ this._prevEventHash = computeEventHash(d);
283
+ this._emit(d);
284
+ } catch (e) {
285
+ console.error(`wytness: failed to record event: ${e}`);
286
+ }
287
+ }
288
+ /**
289
+ * Wait for all pending HTTP requests to complete.
290
+ * Call this before process exit in serverless/short-lived environments
291
+ * to ensure all events are delivered.
292
+ */
293
+ async flush() {
294
+ await flushPending();
225
295
  }
226
296
  };
227
297
 
@@ -238,41 +308,47 @@ function sanitise(params) {
238
308
  function hashValue(value) {
239
309
  return (0, import_crypto4.createHash)("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
240
310
  }
311
+ function recordEvent(client, toolName, taskId, args, start, status, errorCode, result) {
312
+ const params = {};
313
+ args.forEach((arg, i) => {
314
+ params[`arg${i}`] = arg;
315
+ });
316
+ const event = AuditEventSchema.parse({
317
+ agent_id: client.agentId,
318
+ agent_version: client.agentVersion,
319
+ human_operator_id: client.humanOperatorId,
320
+ task_id: taskId,
321
+ session_id: client.sessionId,
322
+ tool_name: toolName,
323
+ tool_parameters: sanitise(params),
324
+ inputs_hash: hashValue(args),
325
+ outputs_hash: result != null ? hashValue(result) : "",
326
+ status,
327
+ error_code: errorCode,
328
+ duration_ms: Math.round(performance.now() - start)
329
+ });
330
+ client.record(event);
331
+ }
241
332
  function wrapFn(client, fn, toolName, taskId) {
242
333
  const wrapped = function(...args) {
243
334
  const start = performance.now();
244
- let status = "success";
245
- let errorCode = null;
246
335
  let result;
247
- const inputsHash = hashValue(args);
248
336
  try {
249
337
  result = fn.apply(this, args);
250
338
  } catch (e) {
251
- status = "failure";
252
- errorCode = e.constructor.name;
339
+ recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
253
340
  throw e;
254
- } finally {
255
- const durationMs = Math.round(performance.now() - start);
256
- const params = {};
257
- args.forEach((arg, i) => {
258
- params[`arg${i}`] = arg;
259
- });
260
- const event = AuditEventSchema.parse({
261
- agent_id: client.agentId,
262
- agent_version: client.agentVersion,
263
- human_operator_id: client.humanOperatorId,
264
- task_id: taskId,
265
- session_id: client.sessionId,
266
- tool_name: toolName,
267
- tool_parameters: sanitise(params),
268
- inputs_hash: inputsHash,
269
- outputs_hash: result != null ? hashValue(result) : "",
270
- status,
271
- error_code: errorCode,
272
- duration_ms: durationMs
341
+ }
342
+ if (result && typeof result.then === "function") {
343
+ return result.then((resolved) => {
344
+ recordEvent(client, toolName, taskId, args, start, "success", null, resolved);
345
+ return resolved;
346
+ }).catch((e) => {
347
+ recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
348
+ throw e;
273
349
  });
274
- client.record(event);
275
350
  }
351
+ recordEvent(client, toolName, taskId, args, start, "success", null, result);
276
352
  return result;
277
353
  };
278
354
  Object.defineProperty(wrapped, "name", { value: toolName });
@@ -301,6 +377,7 @@ function auditTool(client, fnOrTaskId, options) {
301
377
  createFileEmitter,
302
378
  createHttpEmitter,
303
379
  generateKeypair,
380
+ hashValue,
304
381
  signEvent,
305
382
  verifyChain,
306
383
  verifyEvent
package/dist/index.d.cts CHANGED
@@ -104,6 +104,7 @@ interface AuditClientOptions {
104
104
  agentId: string;
105
105
  agentVersion?: string;
106
106
  humanOperatorId?: string;
107
+ signingKey?: string;
107
108
  signingKeyPath?: string;
108
109
  fallbackLogPath?: string;
109
110
  httpApiKey?: string;
@@ -119,15 +120,23 @@ declare class AuditClient {
119
120
  private _emit;
120
121
  constructor(options: AuditClientOptions);
121
122
  get sessionId(): string;
123
+ /** Record an audit event. Never throws — errors are logged and swallowed. */
122
124
  record(event: AuditEvent): void;
125
+ /**
126
+ * Wait for all pending HTTP requests to complete.
127
+ * Call this before process exit in serverless/short-lived environments
128
+ * to ensure all events are delivered.
129
+ */
130
+ flush(): Promise<void>;
123
131
  }
124
132
 
133
+ declare function hashValue(value: unknown): string;
125
134
  interface AuditToolOptions {
126
135
  toolName?: string;
127
136
  taskId?: string;
128
137
  }
129
138
  /**
130
- * Wrap a function with audit logging.
139
+ * Wrap a function with audit logging. Works with both sync and async functions.
131
140
  *
132
141
  * Supports two calling styles:
133
142
  *
@@ -140,4 +149,4 @@ interface AuditToolOptions {
140
149
  */
141
150
  declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
142
151
 
143
- export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, signEvent, verifyChain, verifyEvent };
152
+ export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, signEvent, verifyChain, verifyEvent };
package/dist/index.d.ts CHANGED
@@ -104,6 +104,7 @@ interface AuditClientOptions {
104
104
  agentId: string;
105
105
  agentVersion?: string;
106
106
  humanOperatorId?: string;
107
+ signingKey?: string;
107
108
  signingKeyPath?: string;
108
109
  fallbackLogPath?: string;
109
110
  httpApiKey?: string;
@@ -119,15 +120,23 @@ declare class AuditClient {
119
120
  private _emit;
120
121
  constructor(options: AuditClientOptions);
121
122
  get sessionId(): string;
123
+ /** Record an audit event. Never throws — errors are logged and swallowed. */
122
124
  record(event: AuditEvent): void;
125
+ /**
126
+ * Wait for all pending HTTP requests to complete.
127
+ * Call this before process exit in serverless/short-lived environments
128
+ * to ensure all events are delivered.
129
+ */
130
+ flush(): Promise<void>;
123
131
  }
124
132
 
133
+ declare function hashValue(value: unknown): string;
125
134
  interface AuditToolOptions {
126
135
  toolName?: string;
127
136
  taskId?: string;
128
137
  }
129
138
  /**
130
- * Wrap a function with audit logging.
139
+ * Wrap a function with audit logging. Works with both sync and async functions.
131
140
  *
132
141
  * Supports two calling styles:
133
142
  *
@@ -140,4 +149,4 @@ interface AuditToolOptions {
140
149
  */
141
150
  declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
142
151
 
143
- export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, signEvent, verifyChain, verifyEvent };
152
+ export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, hashValue, signEvent, verifyChain, verifyEvent };
package/dist/index.js CHANGED
@@ -92,36 +92,85 @@ function verifyChain(events) {
92
92
  }
93
93
 
94
94
  // src/emitter.ts
95
- import { mkdirSync, appendFileSync } from "fs";
95
+ import { mkdirSync, appendFileSync, existsSync, readFileSync, unlinkSync, writeFileSync, statSync } from "fs";
96
96
  import { dirname } from "path";
97
97
  function fileEmit(path, eventDict) {
98
98
  mkdirSync(dirname(path), { recursive: true });
99
99
  appendFileSync(path, JSON.stringify(eventDict) + "\n");
100
100
  }
101
+ var pendingRequests = /* @__PURE__ */ new Set();
102
+ var REPLAY_COOLDOWN_MS = 1e4;
103
+ async function sendOne(apiUrl, apiKey, body) {
104
+ const resp = await fetch(`${apiUrl}/ingest`, {
105
+ method: "POST",
106
+ headers: {
107
+ "Content-Type": "application/json",
108
+ "X-API-Key": apiKey
109
+ },
110
+ body,
111
+ signal: AbortSignal.timeout(1e4)
112
+ });
113
+ if (resp.status !== 201) {
114
+ throw new Error(`HTTP ${resp.status}`);
115
+ }
116
+ return true;
117
+ }
101
118
  function createHttpEmitter(apiUrl, apiKey, fallbackPath) {
119
+ let replaying = false;
120
+ let lastReplayAttempt = 0;
121
+ const tryReplay = async () => {
122
+ const now = Date.now();
123
+ if (now - lastReplayAttempt < REPLAY_COOLDOWN_MS)
124
+ return;
125
+ if (replaying)
126
+ return;
127
+ replaying = true;
128
+ try {
129
+ lastReplayAttempt = now;
130
+ if (!existsSync(fallbackPath))
131
+ return;
132
+ const stat = statSync(fallbackPath);
133
+ if (stat.size === 0)
134
+ return;
135
+ const lines = readFileSync(fallbackPath, "utf-8").split("\n").filter((l) => l.trim());
136
+ if (lines.length === 0)
137
+ return;
138
+ const remaining = [];
139
+ for (const line of lines) {
140
+ try {
141
+ await sendOne(apiUrl, apiKey, line);
142
+ } catch {
143
+ remaining.push(line);
144
+ }
145
+ }
146
+ if (remaining.length > 0) {
147
+ writeFileSync(fallbackPath, remaining.join("\n") + "\n");
148
+ } else {
149
+ unlinkSync(fallbackPath);
150
+ }
151
+ const replayed = lines.length - remaining.length;
152
+ if (replayed > 0) {
153
+ console.log(`wytness: replayed ${replayed} fallback events`);
154
+ }
155
+ } catch {
156
+ } finally {
157
+ replaying = false;
158
+ }
159
+ };
102
160
  return (eventDict) => {
103
161
  const doSend = async () => {
104
162
  try {
105
163
  const body = JSON.stringify(eventDict);
106
- const resp = await fetch(`${apiUrl}/ingest`, {
107
- method: "POST",
108
- headers: {
109
- "Content-Type": "application/json",
110
- "X-API-Key": apiKey
111
- },
112
- body,
113
- signal: AbortSignal.timeout(1e4)
114
- });
115
- if (resp.status !== 201) {
116
- console.error(`HTTP ingest failed: ${resp.status}`);
117
- fileEmit(fallbackPath, eventDict);
118
- }
164
+ await sendOne(apiUrl, apiKey, body);
165
+ tryReplay();
119
166
  } catch (e) {
120
167
  console.error(`HTTP ingest error: ${e}`);
121
168
  fileEmit(fallbackPath, eventDict);
122
169
  }
123
170
  };
124
- doSend().catch(() => fileEmit(fallbackPath, eventDict));
171
+ const p = doSend();
172
+ pendingRequests.add(p);
173
+ p.finally(() => pendingRequests.delete(p));
125
174
  };
126
175
  }
127
176
  function createFileEmitter(fallbackPath) {
@@ -129,10 +178,13 @@ function createFileEmitter(fallbackPath) {
129
178
  fileEmit(fallbackPath, eventDict);
130
179
  };
131
180
  }
181
+ async function flushPending() {
182
+ await Promise.allSettled([...pendingRequests]);
183
+ }
132
184
 
133
185
  // src/client.ts
134
186
  import { randomUUID as randomUUID2 } from "crypto";
135
- import { existsSync, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
187
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
136
188
  import { dirname as dirname2 } from "path";
137
189
  var AuditClient = class {
138
190
  agentId;
@@ -147,15 +199,19 @@ var AuditClient = class {
147
199
  this.agentVersion = options.agentVersion ?? "0.1.0";
148
200
  this.humanOperatorId = options.humanOperatorId ?? "unknown";
149
201
  this._sessionId = randomUUID2();
150
- const keyPath = options.signingKeyPath ?? "./keys/signing.key";
151
- if (existsSync(keyPath)) {
152
- const raw = readFileSync(keyPath);
153
- this._secretKey = new Uint8Array(raw);
202
+ if (options.signingKey) {
203
+ this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
154
204
  } else {
155
- const kp = generateKeypair();
156
- this._secretKey = kp.secretKey;
157
- mkdirSync2(dirname2(keyPath), { recursive: true });
158
- writeFileSync(keyPath, Buffer.from(kp.secretKey));
205
+ const keyPath = options.signingKeyPath ?? "./keys/signing.key";
206
+ if (existsSync2(keyPath)) {
207
+ const raw = readFileSync2(keyPath);
208
+ this._secretKey = new Uint8Array(raw);
209
+ } else {
210
+ const kp = generateKeypair();
211
+ this._secretKey = kp.secretKey;
212
+ mkdirSync2(dirname2(keyPath), { recursive: true });
213
+ writeFileSync2(keyPath, Buffer.from(kp.secretKey));
214
+ }
159
215
  }
160
216
  const fallback = options.fallbackLogPath ?? "./audit_fallback.jsonl";
161
217
  if (options.httpApiKey) {
@@ -171,12 +227,25 @@ var AuditClient = class {
171
227
  get sessionId() {
172
228
  return this._sessionId;
173
229
  }
230
+ /** Record an audit event. Never throws — errors are logged and swallowed. */
174
231
  record(event) {
175
- const d = { ...event };
176
- d["prev_event_hash"] = this._prevEventHash;
177
- d["signature"] = signEvent(d, this._secretKey);
178
- this._prevEventHash = computeEventHash(d);
179
- this._emit(d);
232
+ try {
233
+ const d = { ...event };
234
+ d["prev_event_hash"] = this._prevEventHash;
235
+ d["signature"] = signEvent(d, this._secretKey);
236
+ this._prevEventHash = computeEventHash(d);
237
+ this._emit(d);
238
+ } catch (e) {
239
+ console.error(`wytness: failed to record event: ${e}`);
240
+ }
241
+ }
242
+ /**
243
+ * Wait for all pending HTTP requests to complete.
244
+ * Call this before process exit in serverless/short-lived environments
245
+ * to ensure all events are delivered.
246
+ */
247
+ async flush() {
248
+ await flushPending();
180
249
  }
181
250
  };
182
251
 
@@ -193,41 +262,47 @@ function sanitise(params) {
193
262
  function hashValue(value) {
194
263
  return createHash2("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
195
264
  }
265
+ function recordEvent(client, toolName, taskId, args, start, status, errorCode, result) {
266
+ const params = {};
267
+ args.forEach((arg, i) => {
268
+ params[`arg${i}`] = arg;
269
+ });
270
+ const event = AuditEventSchema.parse({
271
+ agent_id: client.agentId,
272
+ agent_version: client.agentVersion,
273
+ human_operator_id: client.humanOperatorId,
274
+ task_id: taskId,
275
+ session_id: client.sessionId,
276
+ tool_name: toolName,
277
+ tool_parameters: sanitise(params),
278
+ inputs_hash: hashValue(args),
279
+ outputs_hash: result != null ? hashValue(result) : "",
280
+ status,
281
+ error_code: errorCode,
282
+ duration_ms: Math.round(performance.now() - start)
283
+ });
284
+ client.record(event);
285
+ }
196
286
  function wrapFn(client, fn, toolName, taskId) {
197
287
  const wrapped = function(...args) {
198
288
  const start = performance.now();
199
- let status = "success";
200
- let errorCode = null;
201
289
  let result;
202
- const inputsHash = hashValue(args);
203
290
  try {
204
291
  result = fn.apply(this, args);
205
292
  } catch (e) {
206
- status = "failure";
207
- errorCode = e.constructor.name;
293
+ recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
208
294
  throw e;
209
- } finally {
210
- const durationMs = Math.round(performance.now() - start);
211
- const params = {};
212
- args.forEach((arg, i) => {
213
- params[`arg${i}`] = arg;
214
- });
215
- const event = AuditEventSchema.parse({
216
- agent_id: client.agentId,
217
- agent_version: client.agentVersion,
218
- human_operator_id: client.humanOperatorId,
219
- task_id: taskId,
220
- session_id: client.sessionId,
221
- tool_name: toolName,
222
- tool_parameters: sanitise(params),
223
- inputs_hash: inputsHash,
224
- outputs_hash: result != null ? hashValue(result) : "",
225
- status,
226
- error_code: errorCode,
227
- duration_ms: durationMs
295
+ }
296
+ if (result && typeof result.then === "function") {
297
+ return result.then((resolved) => {
298
+ recordEvent(client, toolName, taskId, args, start, "success", null, resolved);
299
+ return resolved;
300
+ }).catch((e) => {
301
+ recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
302
+ throw e;
228
303
  });
229
- client.record(event);
230
304
  }
305
+ recordEvent(client, toolName, taskId, args, start, "success", null, result);
231
306
  return result;
232
307
  };
233
308
  Object.defineProperty(wrapped, "name", { value: toolName });
@@ -255,6 +330,7 @@ export {
255
330
  createFileEmitter,
256
331
  createHttpEmitter,
257
332
  generateKeypair,
333
+ hashValue,
258
334
  signEvent,
259
335
  verifyChain,
260
336
  verifyEvent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wytness/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
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>",