@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.
- package/README.md +8 -7
- package/dist/365.js +1 -0
- package/dist/runtime/160.js +1 -0
- package/dist/runtime/adapter-express.js +3 -3
- package/dist/runtime/adapter-nextjs.js +3 -3
- package/dist/runtime/http.js +3 -3
- package/dist/runtime/index.d.ts +1 -2
- package/dist/runtime/index.js +3 -10
- package/dist/runtime/mcpi-runtime-wrapper.js +1 -8
- package/dist/runtime/mcpi-runtime.d.ts +4 -12
- package/dist/runtime/mcpi-runtime.js +38 -31
- package/dist/runtime/stdio.js +3 -3
- package/dist/runtime/utils/time.d.ts +1 -1
- package/dist/runtime/utils/time.js +1 -1
- package/package.json +5 -5
- package/dist/342.js +0 -1
- package/dist/runtime/337.js +0 -1
- package/dist/runtime/audit.d.ts +0 -430
- package/dist/runtime/audit.js +0 -439
package/dist/runtime/audit.js
DELETED
|
@@ -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
|
-
}
|