@wytness/sdk 0.1.0 → 0.2.1
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 +1 -1
- package/dist/index.cjs +120 -97
- package/dist/index.d.cts +24 -5
- package/dist/index.d.ts +24 -5
- package/dist/index.js +119 -105
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Send events directly to the Wytness API using your API key:
|
|
|
44
44
|
const client = new AuditClient({
|
|
45
45
|
agentId: "my-agent",
|
|
46
46
|
humanOperatorId: "user-123",
|
|
47
|
-
httpEndpoint: "https://api.wytness.
|
|
47
|
+
httpEndpoint: "https://api.wytness.dev",
|
|
48
48
|
httpApiKey: "aa_live_...",
|
|
49
49
|
});
|
|
50
50
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,8 @@ __export(src_exports, {
|
|
|
34
34
|
AuditEventSchema: () => AuditEventSchema,
|
|
35
35
|
auditTool: () => auditTool,
|
|
36
36
|
computeEventHash: () => computeEventHash,
|
|
37
|
-
|
|
37
|
+
createFileEmitter: () => createFileEmitter,
|
|
38
|
+
createHttpEmitter: () => createHttpEmitter,
|
|
38
39
|
generateKeypair: () => generateKeypair,
|
|
39
40
|
signEvent: () => signEvent,
|
|
40
41
|
verifyChain: () => verifyChain,
|
|
@@ -142,43 +143,33 @@ function fileEmit(path, eventDict) {
|
|
|
142
143
|
(0, import_fs.mkdirSync)((0, import_path.dirname)(path), { recursive: true });
|
|
143
144
|
(0, import_fs.appendFileSync)(path, JSON.stringify(eventDict) + "\n");
|
|
144
145
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.error(`Kafka error: ${e}`);
|
|
173
|
-
fileEmit(fallbackPath, eventDict);
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
doSend().catch(() => fileEmit(fallbackPath, eventDict));
|
|
177
|
-
};
|
|
178
|
-
} catch (e) {
|
|
179
|
-
console.warn(`Kafka unavailable (${e}), using file fallback`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
146
|
+
function createHttpEmitter(apiUrl, apiKey, fallbackPath) {
|
|
147
|
+
return (eventDict) => {
|
|
148
|
+
const doSend = async () => {
|
|
149
|
+
try {
|
|
150
|
+
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
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error(`HTTP ingest error: ${e}`);
|
|
166
|
+
fileEmit(fallbackPath, eventDict);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
doSend().catch(() => fileEmit(fallbackPath, eventDict));
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function createFileEmitter(fallbackPath) {
|
|
182
173
|
return (eventDict) => {
|
|
183
174
|
fileEmit(fallbackPath, eventDict);
|
|
184
175
|
};
|
|
@@ -201,31 +192,45 @@ var AuditClient = class {
|
|
|
201
192
|
this.agentVersion = options.agentVersion ?? "0.1.0";
|
|
202
193
|
this.humanOperatorId = options.humanOperatorId ?? "unknown";
|
|
203
194
|
this._sessionId = (0, import_crypto3.randomUUID)();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const raw = (0, import_fs2.readFileSync)(keyPath);
|
|
207
|
-
this._secretKey = new Uint8Array(raw);
|
|
195
|
+
if (options.signingKey) {
|
|
196
|
+
this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
|
|
208
197
|
} else {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
198
|
+
const keyPath = options.signingKeyPath ?? "./keys/signing.key";
|
|
199
|
+
if ((0, import_fs2.existsSync)(keyPath)) {
|
|
200
|
+
const raw = (0, import_fs2.readFileSync)(keyPath);
|
|
201
|
+
this._secretKey = new Uint8Array(raw);
|
|
202
|
+
} else {
|
|
203
|
+
const kp = generateKeypair();
|
|
204
|
+
this._secretKey = kp.secretKey;
|
|
205
|
+
(0, import_fs2.mkdirSync)((0, import_path2.dirname)(keyPath), { recursive: true });
|
|
206
|
+
(0, import_fs2.writeFileSync)(keyPath, Buffer.from(kp.secretKey));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const fallback = options.fallbackLogPath ?? "./audit_fallback.jsonl";
|
|
210
|
+
if (options.httpApiKey) {
|
|
211
|
+
this._emit = createHttpEmitter(
|
|
212
|
+
options.httpEndpoint ?? "https://api.wytness.dev",
|
|
213
|
+
options.httpApiKey,
|
|
214
|
+
fallback
|
|
215
|
+
);
|
|
216
|
+
} else {
|
|
217
|
+
this._emit = createFileEmitter(fallback);
|
|
213
218
|
}
|
|
214
|
-
this._emit = createEmitter(
|
|
215
|
-
options.kafkaBootstrapServers ?? null,
|
|
216
|
-
options.kafkaTopic ?? "wytness-events",
|
|
217
|
-
options.fallbackLogPath ?? "./audit_fallback.jsonl"
|
|
218
|
-
);
|
|
219
219
|
}
|
|
220
220
|
get sessionId() {
|
|
221
221
|
return this._sessionId;
|
|
222
222
|
}
|
|
223
|
+
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
223
224
|
record(event) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
try {
|
|
226
|
+
const d = { ...event };
|
|
227
|
+
d["prev_event_hash"] = this._prevEventHash;
|
|
228
|
+
d["signature"] = signEvent(d, this._secretKey);
|
|
229
|
+
this._prevEventHash = computeEventHash(d);
|
|
230
|
+
this._emit(d);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error(`wytness: failed to record event: ${e}`);
|
|
233
|
+
}
|
|
229
234
|
}
|
|
230
235
|
};
|
|
231
236
|
|
|
@@ -242,47 +247,64 @@ function sanitise(params) {
|
|
|
242
247
|
function hashValue(value) {
|
|
243
248
|
return (0, import_crypto4.createHash)("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
|
|
244
249
|
}
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
function recordEvent(client, toolName, taskId, args, start, status, errorCode, result) {
|
|
251
|
+
const params = {};
|
|
252
|
+
args.forEach((arg, i) => {
|
|
253
|
+
params[`arg${i}`] = arg;
|
|
254
|
+
});
|
|
255
|
+
const event = AuditEventSchema.parse({
|
|
256
|
+
agent_id: client.agentId,
|
|
257
|
+
agent_version: client.agentVersion,
|
|
258
|
+
human_operator_id: client.humanOperatorId,
|
|
259
|
+
task_id: taskId,
|
|
260
|
+
session_id: client.sessionId,
|
|
261
|
+
tool_name: toolName,
|
|
262
|
+
tool_parameters: sanitise(params),
|
|
263
|
+
inputs_hash: hashValue(args),
|
|
264
|
+
outputs_hash: result != null ? hashValue(result) : "",
|
|
265
|
+
status,
|
|
266
|
+
error_code: errorCode,
|
|
267
|
+
duration_ms: Math.round(performance.now() - start)
|
|
268
|
+
});
|
|
269
|
+
client.record(event);
|
|
270
|
+
}
|
|
271
|
+
function wrapFn(client, fn, toolName, taskId) {
|
|
272
|
+
const wrapped = function(...args) {
|
|
273
|
+
const start = performance.now();
|
|
274
|
+
let result;
|
|
275
|
+
try {
|
|
276
|
+
result = fn.apply(this, args);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
|
|
279
|
+
throw e;
|
|
280
|
+
}
|
|
281
|
+
if (result && typeof result.then === "function") {
|
|
282
|
+
return result.then((resolved) => {
|
|
283
|
+
recordEvent(client, toolName, taskId, args, start, "success", null, resolved);
|
|
284
|
+
return resolved;
|
|
285
|
+
}).catch((e) => {
|
|
286
|
+
recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
|
|
259
287
|
throw e;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
client.record(event);
|
|
281
|
-
}
|
|
282
|
-
return result;
|
|
283
|
-
};
|
|
284
|
-
Object.defineProperty(wrapped, "name", { value: name });
|
|
285
|
-
return wrapped;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
recordEvent(client, toolName, taskId, args, start, "success", null, result);
|
|
291
|
+
return result;
|
|
292
|
+
};
|
|
293
|
+
Object.defineProperty(wrapped, "name", { value: toolName });
|
|
294
|
+
return wrapped;
|
|
295
|
+
}
|
|
296
|
+
function auditTool(client, fnOrTaskId, options) {
|
|
297
|
+
if (typeof fnOrTaskId === "function") {
|
|
298
|
+
const fn = fnOrTaskId;
|
|
299
|
+
const opts = typeof options === "object" ? options : {};
|
|
300
|
+
const toolName = opts.toolName ?? fn.name ?? "anonymous";
|
|
301
|
+
const taskId2 = opts.taskId ?? "default";
|
|
302
|
+
return wrapFn(client, fn, toolName, taskId2);
|
|
303
|
+
}
|
|
304
|
+
const taskId = typeof fnOrTaskId === "string" ? fnOrTaskId : "default";
|
|
305
|
+
return function(fn, fnName) {
|
|
306
|
+
const toolName = fnName ?? fn.name ?? "anonymous";
|
|
307
|
+
return wrapFn(client, fn, toolName, taskId);
|
|
286
308
|
};
|
|
287
309
|
}
|
|
288
310
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -291,7 +313,8 @@ function auditTool(client, taskId = "default") {
|
|
|
291
313
|
AuditEventSchema,
|
|
292
314
|
auditTool,
|
|
293
315
|
computeEventHash,
|
|
294
|
-
|
|
316
|
+
createFileEmitter,
|
|
317
|
+
createHttpEmitter,
|
|
295
318
|
generateKeypair,
|
|
296
319
|
signEvent,
|
|
297
320
|
verifyChain,
|
package/dist/index.d.cts
CHANGED
|
@@ -97,16 +97,18 @@ declare function verifyChain(events: Record<string, unknown>[]): {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
type EmitFn = (eventDict: Record<string, unknown>) => void;
|
|
100
|
-
declare function
|
|
100
|
+
declare function createHttpEmitter(apiUrl: string, apiKey: string, fallbackPath: string): EmitFn;
|
|
101
|
+
declare function createFileEmitter(fallbackPath: string): EmitFn;
|
|
101
102
|
|
|
102
103
|
interface AuditClientOptions {
|
|
103
104
|
agentId: string;
|
|
104
105
|
agentVersion?: string;
|
|
105
106
|
humanOperatorId?: string;
|
|
107
|
+
signingKey?: string;
|
|
106
108
|
signingKeyPath?: string;
|
|
107
|
-
kafkaBootstrapServers?: string | null;
|
|
108
|
-
kafkaTopic?: string;
|
|
109
109
|
fallbackLogPath?: string;
|
|
110
|
+
httpApiKey?: string;
|
|
111
|
+
httpEndpoint?: string;
|
|
110
112
|
}
|
|
111
113
|
declare class AuditClient {
|
|
112
114
|
readonly agentId: string;
|
|
@@ -118,9 +120,26 @@ declare class AuditClient {
|
|
|
118
120
|
private _emit;
|
|
119
121
|
constructor(options: AuditClientOptions);
|
|
120
122
|
get sessionId(): string;
|
|
123
|
+
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
121
124
|
record(event: AuditEvent): void;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
interface AuditToolOptions {
|
|
128
|
+
toolName?: string;
|
|
129
|
+
taskId?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wrap a function with audit logging. Works with both sync and async functions.
|
|
133
|
+
*
|
|
134
|
+
* Supports two calling styles:
|
|
135
|
+
*
|
|
136
|
+
* 1. Direct (documented API):
|
|
137
|
+
* auditTool(client, fn, { toolName: "name", taskId: "task" })
|
|
138
|
+
*
|
|
139
|
+
* 2. Curried (legacy):
|
|
140
|
+
* auditTool(client)(fn, "name")
|
|
141
|
+
* auditTool(client, "taskId")(fn, "name")
|
|
142
|
+
*/
|
|
143
|
+
declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
|
|
125
144
|
|
|
126
|
-
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash,
|
|
145
|
+
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, signEvent, verifyChain, verifyEvent };
|
package/dist/index.d.ts
CHANGED
|
@@ -97,16 +97,18 @@ declare function verifyChain(events: Record<string, unknown>[]): {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
type EmitFn = (eventDict: Record<string, unknown>) => void;
|
|
100
|
-
declare function
|
|
100
|
+
declare function createHttpEmitter(apiUrl: string, apiKey: string, fallbackPath: string): EmitFn;
|
|
101
|
+
declare function createFileEmitter(fallbackPath: string): EmitFn;
|
|
101
102
|
|
|
102
103
|
interface AuditClientOptions {
|
|
103
104
|
agentId: string;
|
|
104
105
|
agentVersion?: string;
|
|
105
106
|
humanOperatorId?: string;
|
|
107
|
+
signingKey?: string;
|
|
106
108
|
signingKeyPath?: string;
|
|
107
|
-
kafkaBootstrapServers?: string | null;
|
|
108
|
-
kafkaTopic?: string;
|
|
109
109
|
fallbackLogPath?: string;
|
|
110
|
+
httpApiKey?: string;
|
|
111
|
+
httpEndpoint?: string;
|
|
110
112
|
}
|
|
111
113
|
declare class AuditClient {
|
|
112
114
|
readonly agentId: string;
|
|
@@ -118,9 +120,26 @@ declare class AuditClient {
|
|
|
118
120
|
private _emit;
|
|
119
121
|
constructor(options: AuditClientOptions);
|
|
120
122
|
get sessionId(): string;
|
|
123
|
+
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
121
124
|
record(event: AuditEvent): void;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
interface AuditToolOptions {
|
|
128
|
+
toolName?: string;
|
|
129
|
+
taskId?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wrap a function with audit logging. Works with both sync and async functions.
|
|
133
|
+
*
|
|
134
|
+
* Supports two calling styles:
|
|
135
|
+
*
|
|
136
|
+
* 1. Direct (documented API):
|
|
137
|
+
* auditTool(client, fn, { toolName: "name", taskId: "task" })
|
|
138
|
+
*
|
|
139
|
+
* 2. Curried (legacy):
|
|
140
|
+
* auditTool(client)(fn, "name")
|
|
141
|
+
* auditTool(client, "taskId")(fn, "name")
|
|
142
|
+
*/
|
|
143
|
+
declare function auditTool(client: AuditClient, fnOrTaskId?: ((...args: unknown[]) => unknown) | string, options?: AuditToolOptions | string): any;
|
|
125
144
|
|
|
126
|
-
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash,
|
|
145
|
+
export { AuditClient, type AuditClientOptions, type AuditEvent, AuditEventSchema, auditTool, computeEventHash, createFileEmitter, createHttpEmitter, generateKeypair, signEvent, verifyChain, verifyEvent };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
1
|
// src/models.ts
|
|
10
2
|
import { z } from "zod";
|
|
11
3
|
import { randomUUID } from "crypto";
|
|
@@ -106,43 +98,33 @@ function fileEmit(path, eventDict) {
|
|
|
106
98
|
mkdirSync(dirname(path), { recursive: true });
|
|
107
99
|
appendFileSync(path, JSON.stringify(eventDict) + "\n");
|
|
108
100
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.error(`Kafka error: ${e}`);
|
|
137
|
-
fileEmit(fallbackPath, eventDict);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
doSend().catch(() => fileEmit(fallbackPath, eventDict));
|
|
141
|
-
};
|
|
142
|
-
} catch (e) {
|
|
143
|
-
console.warn(`Kafka unavailable (${e}), using file fallback`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
101
|
+
function createHttpEmitter(apiUrl, apiKey, fallbackPath) {
|
|
102
|
+
return (eventDict) => {
|
|
103
|
+
const doSend = async () => {
|
|
104
|
+
try {
|
|
105
|
+
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
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(`HTTP ingest error: ${e}`);
|
|
121
|
+
fileEmit(fallbackPath, eventDict);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
doSend().catch(() => fileEmit(fallbackPath, eventDict));
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function createFileEmitter(fallbackPath) {
|
|
146
128
|
return (eventDict) => {
|
|
147
129
|
fileEmit(fallbackPath, eventDict);
|
|
148
130
|
};
|
|
@@ -150,7 +132,7 @@ function createEmitter(bootstrapServers, topic, fallbackPath) {
|
|
|
150
132
|
|
|
151
133
|
// src/client.ts
|
|
152
134
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
153
|
-
import { existsSync, readFileSync, writeFileSync
|
|
135
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
154
136
|
import { dirname as dirname2 } from "path";
|
|
155
137
|
var AuditClient = class {
|
|
156
138
|
agentId;
|
|
@@ -165,31 +147,45 @@ var AuditClient = class {
|
|
|
165
147
|
this.agentVersion = options.agentVersion ?? "0.1.0";
|
|
166
148
|
this.humanOperatorId = options.humanOperatorId ?? "unknown";
|
|
167
149
|
this._sessionId = randomUUID2();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const raw = readFileSync(keyPath);
|
|
171
|
-
this._secretKey = new Uint8Array(raw);
|
|
150
|
+
if (options.signingKey) {
|
|
151
|
+
this._secretKey = new Uint8Array(Buffer.from(options.signingKey, "base64"));
|
|
172
152
|
} else {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
153
|
+
const keyPath = options.signingKeyPath ?? "./keys/signing.key";
|
|
154
|
+
if (existsSync(keyPath)) {
|
|
155
|
+
const raw = readFileSync(keyPath);
|
|
156
|
+
this._secretKey = new Uint8Array(raw);
|
|
157
|
+
} else {
|
|
158
|
+
const kp = generateKeypair();
|
|
159
|
+
this._secretKey = kp.secretKey;
|
|
160
|
+
mkdirSync2(dirname2(keyPath), { recursive: true });
|
|
161
|
+
writeFileSync(keyPath, Buffer.from(kp.secretKey));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const fallback = options.fallbackLogPath ?? "./audit_fallback.jsonl";
|
|
165
|
+
if (options.httpApiKey) {
|
|
166
|
+
this._emit = createHttpEmitter(
|
|
167
|
+
options.httpEndpoint ?? "https://api.wytness.dev",
|
|
168
|
+
options.httpApiKey,
|
|
169
|
+
fallback
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
this._emit = createFileEmitter(fallback);
|
|
177
173
|
}
|
|
178
|
-
this._emit = createEmitter(
|
|
179
|
-
options.kafkaBootstrapServers ?? null,
|
|
180
|
-
options.kafkaTopic ?? "wytness-events",
|
|
181
|
-
options.fallbackLogPath ?? "./audit_fallback.jsonl"
|
|
182
|
-
);
|
|
183
174
|
}
|
|
184
175
|
get sessionId() {
|
|
185
176
|
return this._sessionId;
|
|
186
177
|
}
|
|
178
|
+
/** Record an audit event. Never throws — errors are logged and swallowed. */
|
|
187
179
|
record(event) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
180
|
+
try {
|
|
181
|
+
const d = { ...event };
|
|
182
|
+
d["prev_event_hash"] = this._prevEventHash;
|
|
183
|
+
d["signature"] = signEvent(d, this._secretKey);
|
|
184
|
+
this._prevEventHash = computeEventHash(d);
|
|
185
|
+
this._emit(d);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error(`wytness: failed to record event: ${e}`);
|
|
188
|
+
}
|
|
193
189
|
}
|
|
194
190
|
};
|
|
195
191
|
|
|
@@ -206,47 +202,64 @@ function sanitise(params) {
|
|
|
206
202
|
function hashValue(value) {
|
|
207
203
|
return createHash2("sha256").update(JSON.stringify(value, null, 0)).digest("hex");
|
|
208
204
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
205
|
+
function recordEvent(client, toolName, taskId, args, start, status, errorCode, result) {
|
|
206
|
+
const params = {};
|
|
207
|
+
args.forEach((arg, i) => {
|
|
208
|
+
params[`arg${i}`] = arg;
|
|
209
|
+
});
|
|
210
|
+
const event = AuditEventSchema.parse({
|
|
211
|
+
agent_id: client.agentId,
|
|
212
|
+
agent_version: client.agentVersion,
|
|
213
|
+
human_operator_id: client.humanOperatorId,
|
|
214
|
+
task_id: taskId,
|
|
215
|
+
session_id: client.sessionId,
|
|
216
|
+
tool_name: toolName,
|
|
217
|
+
tool_parameters: sanitise(params),
|
|
218
|
+
inputs_hash: hashValue(args),
|
|
219
|
+
outputs_hash: result != null ? hashValue(result) : "",
|
|
220
|
+
status,
|
|
221
|
+
error_code: errorCode,
|
|
222
|
+
duration_ms: Math.round(performance.now() - start)
|
|
223
|
+
});
|
|
224
|
+
client.record(event);
|
|
225
|
+
}
|
|
226
|
+
function wrapFn(client, fn, toolName, taskId) {
|
|
227
|
+
const wrapped = function(...args) {
|
|
228
|
+
const start = performance.now();
|
|
229
|
+
let result;
|
|
230
|
+
try {
|
|
231
|
+
result = fn.apply(this, args);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
|
|
234
|
+
throw e;
|
|
235
|
+
}
|
|
236
|
+
if (result && typeof result.then === "function") {
|
|
237
|
+
return result.then((resolved) => {
|
|
238
|
+
recordEvent(client, toolName, taskId, args, start, "success", null, resolved);
|
|
239
|
+
return resolved;
|
|
240
|
+
}).catch((e) => {
|
|
241
|
+
recordEvent(client, toolName, taskId, args, start, "failure", e.constructor.name, null);
|
|
223
242
|
throw e;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
client.record(event);
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
};
|
|
248
|
-
Object.defineProperty(wrapped, "name", { value: name });
|
|
249
|
-
return wrapped;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
recordEvent(client, toolName, taskId, args, start, "success", null, result);
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
248
|
+
Object.defineProperty(wrapped, "name", { value: toolName });
|
|
249
|
+
return wrapped;
|
|
250
|
+
}
|
|
251
|
+
function auditTool(client, fnOrTaskId, options) {
|
|
252
|
+
if (typeof fnOrTaskId === "function") {
|
|
253
|
+
const fn = fnOrTaskId;
|
|
254
|
+
const opts = typeof options === "object" ? options : {};
|
|
255
|
+
const toolName = opts.toolName ?? fn.name ?? "anonymous";
|
|
256
|
+
const taskId2 = opts.taskId ?? "default";
|
|
257
|
+
return wrapFn(client, fn, toolName, taskId2);
|
|
258
|
+
}
|
|
259
|
+
const taskId = typeof fnOrTaskId === "string" ? fnOrTaskId : "default";
|
|
260
|
+
return function(fn, fnName) {
|
|
261
|
+
const toolName = fnName ?? fn.name ?? "anonymous";
|
|
262
|
+
return wrapFn(client, fn, toolName, taskId);
|
|
250
263
|
};
|
|
251
264
|
}
|
|
252
265
|
export {
|
|
@@ -254,7 +267,8 @@ export {
|
|
|
254
267
|
AuditEventSchema,
|
|
255
268
|
auditTool,
|
|
256
269
|
computeEventHash,
|
|
257
|
-
|
|
270
|
+
createFileEmitter,
|
|
271
|
+
createHttpEmitter,
|
|
258
272
|
generateKeypair,
|
|
259
273
|
signEvent,
|
|
260
274
|
verifyChain,
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wytness/sdk",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "TypeScript SDK for Wytness — audit logging for AI agents with cryptographic signing and chain integrity",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "Wytness <hello@wytness.
|
|
6
|
+
"author": "Wytness <hello@wytness.dev>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/wytness/wytness-sdk",
|
|
10
10
|
"directory": "sdk-typescript"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://wytness.
|
|
12
|
+
"homepage": "https://wytness.dev",
|
|
13
13
|
"keywords": ["audit", "logging", "ai", "agents", "compliance", "security", "typescript"],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "./dist/index.cjs",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"test": "vitest run"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"kafkajs": "2.2.4",
|
|
35
34
|
"tweetnacl": "1.0.3",
|
|
36
35
|
"tweetnacl-util": "0.15.1",
|
|
37
36
|
"zod": "3.23.6"
|