@kya-os/mcp-i 1.11.1 → 1.12.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.
@@ -1,439 +0,0 @@
1
- "use strict";
2
- /**
3
- * Audit Logging System for MCP-I Runtime
4
- *
5
- * TODO: Update AuditLogger to implement IAuditLogger from @kya-os/mcp-i-core/runtime/audit-logger.
6
- * Currently using Node.js-specific implementation. This will allow the interface to be used
7
- * across all platform implementations while maintaining platform-specific crypto implementations.
8
- *
9
- * Handles audit record generation and logging with frozen format
10
- * according to requirements 5.4, 5.5, and 6.7 (configurable retention).
11
- *
12
- * ## Privacy Guarantees
13
- *
14
- * This system is designed with privacy-first principles:
15
- *
16
- * **NEVER Logged:**
17
- * - Request/response bodies (only SHA-256 hashes)
18
- * - Secrets (private keys, API keys, tokens, nonces)
19
- * - PII (names, emails, addresses, phone numbers)
20
- * - Key material (only key IDs are logged)
21
- *
22
- * **ALWAYS Logged:**
23
- * - Metadata only (DIDs, key IDs, timestamps)
24
- * - SHA-256 hashes (not plaintext data)
25
- * - Session IDs (for correlation, not sensitive)
26
- * - Verification results (yes/no only)
27
- * - Scope identifiers (application-level)
28
- *
29
- * ## Frozen Format
30
- *
31
- * The audit.v1 format is **frozen** and will not change:
32
- *
33
- * ```
34
- * audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|->
35
- * ```
36
- *
37
- * ## Event-Driven Rotation
38
- *
39
- * This implementation uses **event-driven rotation** (not timers):
40
- * - Rotation checks happen on each `logAuditRecord()` call
41
- * - No setInterval/setTimeout (works in Cloudflare Workers)
42
- * - No cleanup needed (no timers to clear)
43
- * - Predictable behavior (rotation on activity)
44
- *
45
- * @module audit
46
- * @see {@link https://github.com/kya-os/xmcp-i/docs/concepts/audit-logging.md | Audit Logging Documentation}
47
- */
48
- Object.defineProperty(exports, "__esModule", { value: true });
49
- exports.defaultAuditLogger = exports.AuditLogger = void 0;
50
- exports.logKeyRotationAudit = logKeyRotationAudit;
51
- exports.parseAuditLine = parseAuditLine;
52
- exports.validateAuditRecord = validateAuditRecord;
53
- const time_1 = require("./utils/time");
54
- const crypto_1 = require("crypto");
55
- /**
56
- * Audit logger class with event-driven rotation support
57
- *
58
- * Privacy Guarantees:
59
- * - NEVER logs request/response bodies (only SHA-256 hashes)
60
- * - NEVER logs secrets (private keys, API keys, tokens, nonces)
61
- * - NEVER logs PII (personal information)
62
- * - Only logs metadata: DIDs, key IDs, timestamps, hashes, session IDs
63
- * - Frozen format: audit.v1 with fixed field order
64
- *
65
- * Rotation Strategy:
66
- * - Event-driven (no timers) - rotation checks on each logAuditRecord() call
67
- * - Works in all environments (Node.js, Cloudflare Workers, Vercel Edge)
68
- * - No cleanup needed (no timers to clear)
69
- */
70
- class AuditLogger {
71
- config;
72
- sessionAuditLog = new Set(); // Track first call per session
73
- totalRecordsLogged = 0; // Total records logged (for count rotation)
74
- currentLogSize = 0; // Current log size in bytes (for size rotation)
75
- lastRotationTime = Date.now(); // Last rotation timestamp (for time rotation)
76
- destroyed = false; // Track if logger has been destroyed
77
- constructor(config = {}) {
78
- // Merge rotation config carefully to preserve defaults
79
- const rotationConfig = config.rotation
80
- ? {
81
- strategy: "custom",
82
- ...config.rotation,
83
- }
84
- : undefined;
85
- this.config = {
86
- enabled: true,
87
- logFunction: console.log,
88
- includePayloads: false, // Keep identity/proof data out by default
89
- ...config,
90
- rotation: rotationConfig,
91
- };
92
- }
93
- /**
94
- * Emit audit record on first call per session
95
- * Requirements: 5.4, 5.5
96
- *
97
- * This method:
98
- * 1. Checks if logger is destroyed
99
- * 2. Logs audit record (first call per session)
100
- * 3. Checks if rotation is needed (event-driven)
101
- * 4. Triggers rotation hooks if threshold met
102
- */
103
- async logAuditRecord(context) {
104
- if (this.destroyed) {
105
- throw new Error("AuditLogger has been destroyed");
106
- }
107
- if (!this.config.enabled) {
108
- return;
109
- }
110
- // Check if this is the first call for this session
111
- const sessionKey = `${context.session.sessionId}:${context.session.audience}`;
112
- if (this.sessionAuditLog.has(sessionKey)) {
113
- return; // Already logged for this session
114
- }
115
- // Mark session as logged
116
- this.sessionAuditLog.add(sessionKey);
117
- // Create audit record
118
- const auditRecord = {
119
- version: "audit.v1",
120
- ts: Math.floor(Date.now() / 1000),
121
- session: context.session.sessionId,
122
- audience: context.session.audience,
123
- did: context.identity.did,
124
- kid: context.identity.kid,
125
- reqHash: context.requestHash,
126
- resHash: context.responseHash,
127
- verified: context.verified,
128
- scope: context.scopeId || "-", // Use "-" for no scope
129
- };
130
- // Format as frozen audit line
131
- const auditLine = this.formatAuditLine(auditRecord);
132
- // Track size in bytes (UTF-8)
133
- const sizeBytes = Buffer.byteLength(auditLine, "utf8");
134
- this.currentLogSize += sizeBytes;
135
- this.totalRecordsLogged++;
136
- // Emit audit record
137
- this.config.logFunction(auditLine);
138
- // Check if rotation is needed (event-driven)
139
- await this.checkRotation();
140
- }
141
- /**
142
- * Log an event using the frozen audit format WITHOUT session deduplication.
143
- *
144
- * Unlike logAuditRecord(), this method ALWAYS logs the event, regardless
145
- * of whether an event has already been logged for this session. This is
146
- * necessary for consent events where multiple events occur in the same session.
147
- *
148
- * The event still uses the frozen audit.v1 format for consistency, but
149
- * bypasses the "once per session" constraint.
150
- *
151
- * @param context - Event context including eventType, identity, session, and eventData
152
- */
153
- async logEvent(context) {
154
- if (this.destroyed) {
155
- throw new Error("AuditLogger has been destroyed");
156
- }
157
- if (!this.config.enabled) {
158
- return;
159
- }
160
- // Generate event hash
161
- const eventHash = this.hashEvent(context.eventType, context.eventData);
162
- // Create audit record (same format as regular audit logs)
163
- const auditRecord = {
164
- version: "audit.v1",
165
- ts: Math.floor(Date.now() / 1000),
166
- session: context.session.sessionId,
167
- audience: context.session.audience,
168
- did: context.identity.did,
169
- kid: context.identity.kid,
170
- reqHash: `sha256:${eventHash}`,
171
- resHash: `sha256:${eventHash}`, // Same hash for events
172
- verified: "yes",
173
- scope: context.eventType, // Use eventType as scope
174
- };
175
- // Format and log (NO session deduplication check)
176
- const auditLine = this.formatAuditLine(auditRecord);
177
- // Track size and count
178
- const sizeBytes = Buffer.byteLength(auditLine, "utf8");
179
- this.currentLogSize += sizeBytes;
180
- this.totalRecordsLogged++;
181
- // Emit audit record
182
- this.config.logFunction(auditLine);
183
- // Check rotation
184
- await this.checkRotation();
185
- }
186
- /**
187
- * Generate deterministic hash for event
188
- */
189
- hashEvent(type, data) {
190
- const content = JSON.stringify({
191
- type,
192
- data,
193
- ts: Date.now(),
194
- nonce: (0, crypto_1.randomBytes)(16).toString("hex"),
195
- });
196
- return (0, crypto_1.createHash)("sha256").update(content).digest("hex");
197
- }
198
- /**
199
- * Format audit record as frozen audit line
200
- * Format: audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|->
201
- */
202
- formatAuditLine(record) {
203
- const fields = [
204
- `${record.version}`,
205
- `ts=${record.ts}`,
206
- `session=${record.session}`,
207
- `audience=${record.audience}`,
208
- `did=${record.did}`,
209
- `kid=${record.kid}`,
210
- `reqHash=${record.reqHash}`,
211
- `resHash=${record.resHash}`,
212
- `verified=${record.verified}`,
213
- `scope=${record.scope}`,
214
- ];
215
- return fields.join(" ");
216
- }
217
- /**
218
- * Check if rotation is needed and trigger if necessary (event-driven)
219
- *
220
- * This method is called on each logAuditRecord() call.
221
- * No timers are used - rotation is checked based on current state.
222
- */
223
- async checkRotation() {
224
- if (!this.config.rotation) {
225
- return;
226
- }
227
- const { strategy, sizeLimit, timeInterval, countThreshold, hooks } = this.config.rotation;
228
- let shouldRotate = false;
229
- let trigger = "";
230
- // Size-based rotation
231
- if (strategy === "size" && sizeLimit && this.currentLogSize >= sizeLimit) {
232
- shouldRotate = true;
233
- trigger = "size-limit";
234
- await hooks?.onSizeLimit?.(this.currentLogSize, sizeLimit);
235
- }
236
- // Time-based rotation (event-driven, not timer-based)
237
- if (strategy === "time" &&
238
- timeInterval &&
239
- Date.now() - this.lastRotationTime >= timeInterval) {
240
- shouldRotate = true;
241
- trigger = "time-interval";
242
- const interval = (0, time_1.formatTimeInterval)(timeInterval);
243
- await hooks?.onTimeBased?.(interval);
244
- }
245
- // Count-based rotation
246
- if (strategy === "count" &&
247
- countThreshold &&
248
- this.totalRecordsLogged >= countThreshold) {
249
- shouldRotate = true;
250
- trigger = "count-threshold";
251
- await hooks?.onCountThreshold?.(this.totalRecordsLogged, countThreshold);
252
- }
253
- // Trigger rotation if needed
254
- if (shouldRotate) {
255
- await this.rotateNow(trigger);
256
- }
257
- }
258
- /**
259
- * Rotate audit log now (manually triggered)
260
- *
261
- * This method:
262
- * 1. Calls onRotation hook (for user to archive/rotate log file)
263
- * 2. Resets rotation counters (size, time, count)
264
- *
265
- * @param trigger - Reason for rotation (e.g., "manual", "size-limit")
266
- */
267
- async rotateNow(trigger = "manual") {
268
- if (!this.config.rotation?.hooks?.onRotation) {
269
- return;
270
- }
271
- const context = {
272
- strategy: this.config.rotation.strategy || "custom",
273
- trigger,
274
- recordsLogged: this.totalRecordsLogged,
275
- timestamp: Date.now(),
276
- };
277
- // Call rotation hook (user implements log file rotation)
278
- await this.config.rotation.hooks.onRotation(context);
279
- // Reset rotation counters
280
- this.currentLogSize = 0;
281
- this.lastRotationTime = Date.now();
282
- this.totalRecordsLogged = 0;
283
- }
284
- /**
285
- * Destroy audit logger and cleanup resources
286
- *
287
- * This method:
288
- * 1. Clears session tracking (memory)
289
- * 2. Resets statistics
290
- * 3. Marks logger as destroyed
291
- *
292
- * Note: No timers to clear (event-driven rotation)
293
- */
294
- destroy() {
295
- if (this.destroyed) {
296
- return; // Idempotent
297
- }
298
- this.sessionAuditLog.clear();
299
- this.totalRecordsLogged = 0;
300
- this.currentLogSize = 0;
301
- this.destroyed = true;
302
- }
303
- /**
304
- * Clear session audit log (useful for testing)
305
- */
306
- clearSessionLog() {
307
- this.sessionAuditLog.clear();
308
- }
309
- /**
310
- * Get audit statistics
311
- */
312
- getStats() {
313
- return {
314
- enabled: this.config.enabled,
315
- sessionsLogged: this.sessionAuditLog.size,
316
- includePayloads: this.config.includePayloads,
317
- totalRecordsLogged: this.totalRecordsLogged,
318
- currentLogSize: this.currentLogSize,
319
- lastRotationTime: this.lastRotationTime,
320
- };
321
- }
322
- /**
323
- * Update configuration
324
- */
325
- updateConfig(config) {
326
- this.config = { ...this.config, ...config };
327
- }
328
- }
329
- exports.AuditLogger = AuditLogger;
330
- /**
331
- * Log key rotation audit record
332
- * Format: keys.rotate.v1 ts=<unix> did=<did> oldKid=<kid> newKid=<kid> mode=dev|prod delegated=yes|no force=yes|no
333
- */
334
- function logKeyRotationAudit(context, logFunction = console.log) {
335
- const fields = [
336
- "keys.rotate.v1",
337
- `ts=${Math.floor(Date.now() / 1000)}`,
338
- `did=${context.identity.did}`,
339
- `oldKid=${context.oldKeyId}`,
340
- `newKid=${context.newKeyId}`,
341
- `mode=${context.mode}`,
342
- `delegated=${context.delegated}`,
343
- `force=${context.force}`,
344
- ];
345
- const auditLine = fields.join(" ");
346
- logFunction(auditLine);
347
- }
348
- /**
349
- * Default audit logger instance
350
- */
351
- exports.defaultAuditLogger = new AuditLogger();
352
- /**
353
- * Utility functions
354
- */
355
- /**
356
- * Parse audit line back to record (for testing/analysis)
357
- */
358
- function parseAuditLine(line) {
359
- try {
360
- const parts = line.split(" ");
361
- if (parts.length < 10 || !parts[0].startsWith("audit.v")) {
362
- return null;
363
- }
364
- const record = {
365
- version: parts[0],
366
- };
367
- // Parse key=value pairs
368
- for (let i = 1; i < parts.length; i++) {
369
- const [key, value] = parts[i].split("=", 2);
370
- if (!key || value === undefined)
371
- continue;
372
- switch (key) {
373
- case "ts":
374
- record.ts = parseInt(value, 10);
375
- break;
376
- case "session":
377
- record.session = value;
378
- break;
379
- case "audience":
380
- record.audience = value;
381
- break;
382
- case "did":
383
- record.did = value;
384
- break;
385
- case "kid":
386
- record.kid = value;
387
- break;
388
- case "reqHash":
389
- record.reqHash = value;
390
- break;
391
- case "resHash":
392
- record.resHash = value;
393
- break;
394
- case "verified":
395
- record.verified = value;
396
- break;
397
- case "scope":
398
- record.scope = value;
399
- break;
400
- }
401
- }
402
- // Validate required fields
403
- if (record.version &&
404
- record.ts &&
405
- record.session &&
406
- record.audience &&
407
- record.did &&
408
- record.kid &&
409
- record.reqHash &&
410
- record.resHash &&
411
- record.verified &&
412
- record.scope !== undefined) {
413
- return record;
414
- }
415
- return null;
416
- }
417
- catch {
418
- return null;
419
- }
420
- }
421
- /**
422
- * Validate audit record format
423
- */
424
- function validateAuditRecord(record) {
425
- return (typeof record === "object" &&
426
- record !== null &&
427
- record.version === "audit.v1" &&
428
- typeof record.ts === "number" &&
429
- typeof record.session === "string" &&
430
- typeof record.audience === "string" &&
431
- typeof record.did === "string" &&
432
- typeof record.kid === "string" &&
433
- typeof record.reqHash === "string" &&
434
- record.reqHash.startsWith("sha256:") &&
435
- typeof record.resHash === "string" &&
436
- record.resHash.startsWith("sha256:") &&
437
- (record.verified === "yes" || record.verified === "no") &&
438
- typeof record.scope === "string");
439
- }