@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 +46 -7
- package/dist/index.cjs +76 -14
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +80 -19
- package/package.json +1 -1
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
|
-
- **
|
|
37
|
-
- **File fallback** — events are saved locally if the
|
|
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
|
-
| `
|
|
62
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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 (
|
|
155
|
-
const raw =
|
|
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
|
-
|
|
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