@wytness/sdk 0.2.1 → 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");
@@ -232,6 +285,14 @@ var AuditClient = class {
232
285
  console.error(`wytness: failed to record event: ${e}`);
233
286
  }
234
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();
295
+ }
235
296
  };
236
297
 
237
298
  // src/decorator.ts
@@ -316,6 +377,7 @@ function auditTool(client, fnOrTaskId, options) {
316
377
  createFileEmitter,
317
378
  createHttpEmitter,
318
379
  generateKeypair,
380
+ hashValue,
319
381
  signEvent,
320
382
  verifyChain,
321
383
  verifyEvent
package/dist/index.d.cts CHANGED
@@ -122,8 +122,15 @@ declare class AuditClient {
122
122
  get sessionId(): string;
123
123
  /** Record an audit event. Never throws — errors are logged and swallowed. */
124
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>;
125
131
  }
126
132
 
133
+ declare function hashValue(value: unknown): string;
127
134
  interface AuditToolOptions {
128
135
  toolName?: string;
129
136
  taskId?: string;
@@ -142,4 +149,4 @@ interface AuditToolOptions {
142
149
  */
143
150
  declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
144
151
 
145
- 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
@@ -122,8 +122,15 @@ declare class AuditClient {
122
122
  get sessionId(): string;
123
123
  /** Record an audit event. Never throws — errors are logged and swallowed. */
124
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>;
125
131
  }
126
132
 
133
+ declare function hashValue(value: unknown): string;
127
134
  interface AuditToolOptions {
128
135
  toolName?: string;
129
136
  taskId?: string;
@@ -142,4 +149,4 @@ interface AuditToolOptions {
142
149
  */
143
150
  declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
144
151
 
145
- 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;
@@ -151,14 +203,14 @@ var AuditClient = class {
151
203
  this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
152
204
  } else {
153
205
  const keyPath = options.signingKeyPath ?? "./keys/signing.key";
154
- if (existsSync(keyPath)) {
155
- const raw = readFileSync(keyPath);
206
+ if (existsSync2(keyPath)) {
207
+ const raw = readFileSync2(keyPath);
156
208
  this._secretKey = new Uint8Array(raw);
157
209
  } else {
158
210
  const kp = generateKeypair();
159
211
  this._secretKey = kp.secretKey;
160
212
  mkdirSync2(dirname2(keyPath), { recursive: true });
161
- writeFileSync(keyPath, Buffer.from(kp.secretKey));
213
+ writeFileSync2(keyPath, Buffer.from(kp.secretKey));
162
214
  }
163
215
  }
164
216
  const fallback = options.fallbackLogPath ?? "./audit_fallback.jsonl";
@@ -187,6 +239,14 @@ var AuditClient = class {
187
239
  console.error(`wytness: failed to record event: ${e}`);
188
240
  }
189
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();
249
+ }
190
250
  };
191
251
 
192
252
  // src/decorator.ts
@@ -270,6 +330,7 @@ export {
270
330
  createFileEmitter,
271
331
  createHttpEmitter,
272
332
  generateKeypair,
333
+ hashValue,
273
334
  signEvent,
274
335
  verifyChain,
275
336
  verifyEvent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wytness/sdk",
3
- "version": "0.2.1",
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>",