codebot-ai 1.7.0 → 1.9.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/dist/cli.js CHANGED
@@ -49,7 +49,10 @@ const scheduler_1 = require("./scheduler");
49
49
  const audit_1 = require("./audit");
50
50
  const policy_1 = require("./policy");
51
51
  const sandbox_1 = require("./sandbox");
52
- const VERSION = '1.7.0';
52
+ const replay_1 = require("./replay");
53
+ const risk_1 = require("./risk");
54
+ const sarif_1 = require("./sarif");
55
+ const VERSION = '1.9.0';
53
56
  const C = {
54
57
  reset: '\x1b[0m',
55
58
  bold: '\x1b[1m',
@@ -168,6 +171,78 @@ async function main() {
168
171
  console.log(` Network: ${info.defaults.network ? 'enabled' : 'disabled'} by default`);
169
172
  return;
170
173
  }
174
+ // --replay: Replay a saved session
175
+ if (args.replay) {
176
+ const replayId = typeof args.replay === 'string'
177
+ ? args.replay
178
+ : history_1.SessionManager.latest();
179
+ if (!replayId) {
180
+ console.log(c('No session to replay. Specify an ID or ensure a previous session exists.', 'yellow'));
181
+ return;
182
+ }
183
+ const data = (0, replay_1.loadSessionForReplay)(replayId);
184
+ if (!data) {
185
+ console.log(c(`Session ${replayId} not found or empty.`, 'red'));
186
+ return;
187
+ }
188
+ console.log(c(`\nReplaying session ${replayId.substring(0, 12)}...`, 'cyan'));
189
+ console.log(c(` ${data.messages.length} messages (${data.userMessages.length} user, ${data.assistantMessages.length} assistant)`, 'dim'));
190
+ const replayProvider = new replay_1.ReplayProvider(data.assistantMessages);
191
+ const config = await resolveConfig(args);
192
+ const agent = new agent_1.Agent({
193
+ provider: replayProvider,
194
+ model: config.model,
195
+ providerName: 'replay',
196
+ autoApprove: true,
197
+ });
198
+ // Collect recorded tool results in order for sequential comparison
199
+ const recordedResults = Array.from(data.toolResults.values());
200
+ let resultIndex = 0;
201
+ let divergences = 0;
202
+ for (const userMsg of data.userMessages) {
203
+ console.log(c(`\n> ${truncate(userMsg.content, 100)}`, 'cyan'));
204
+ for await (const event of agent.run(userMsg.content)) {
205
+ if (event.type === 'tool_result' && event.toolResult && !event.toolResult.is_error) {
206
+ const recorded = recordedResults[resultIndex++];
207
+ if (recorded !== undefined) {
208
+ const diff = (0, replay_1.compareOutputs)(recorded, event.toolResult.result);
209
+ if (diff) {
210
+ divergences++;
211
+ console.log(c(` ⚠ Divergence in ${event.toolResult.name || 'tool'}:`, 'yellow'));
212
+ console.log(c(` ${diff.split('\n').join('\n ')}`, 'dim'));
213
+ }
214
+ else {
215
+ console.log(c(` ✓ ${event.toolResult.name || 'tool'} — output matches`, 'green'));
216
+ }
217
+ }
218
+ }
219
+ else if (event.type === 'text') {
220
+ process.stdout.write(c('.', 'dim'));
221
+ }
222
+ }
223
+ }
224
+ console.log(c(`\n\nReplay complete.`, 'bold'));
225
+ if (divergences === 0) {
226
+ console.log(c(' All tool outputs match — session is reproducible.', 'green'));
227
+ }
228
+ else {
229
+ console.log(c(` ${divergences} divergence(s) detected — environment may have changed.`, 'yellow'));
230
+ }
231
+ return;
232
+ }
233
+ // --export-audit sarif: Export audit log as SARIF 2.1.0
234
+ if (args['export-audit'] === 'sarif' || args['export-audit'] === true) {
235
+ const logger = new audit_1.AuditLogger();
236
+ const sessionId = typeof args['session'] === 'string' ? args['session'] : undefined;
237
+ const entries = sessionId ? logger.query({ sessionId }) : logger.query();
238
+ if (entries.length === 0) {
239
+ console.error(c('No audit entries found.', 'yellow'));
240
+ process.exit(1);
241
+ }
242
+ const sarif = (0, sarif_1.exportSarif)(entries, { version: VERSION, sessionId });
243
+ process.stdout.write((0, sarif_1.sarifToString)(sarif) + '\n');
244
+ return;
245
+ }
171
246
  // First run: auto-launch setup if nothing is configured
172
247
  if ((0, setup_1.isFirstRun)() && process.stdin.isTTY && !args.message) {
173
248
  console.log(c('Welcome! No configuration found — launching setup...', 'cyan'));
@@ -177,6 +252,11 @@ async function main() {
177
252
  }
178
253
  const config = await resolveConfig(args);
179
254
  const provider = createProvider(config);
255
+ // Deterministic mode: set temperature=0
256
+ if (args.deterministic) {
257
+ provider.temperature = 0;
258
+ console.log(c(' Deterministic mode: temperature=0', 'dim'));
259
+ }
180
260
  // Session management
181
261
  let resumeId;
182
262
  if (args.continue) {
@@ -234,7 +314,7 @@ async function main() {
234
314
  // Cleanup scheduler on exit
235
315
  scheduler.stop();
236
316
  }
237
- /** Print session summary with tokens, cost, tool calls, files modified */
317
+ /** Print session summary with tokens, cost, tool calls, files modified, metrics */
238
318
  function printSessionSummary(agent) {
239
319
  const tracker = agent.getTokenTracker();
240
320
  tracker.saveUsage();
@@ -249,6 +329,27 @@ function printSessionSummary(agent) {
249
329
  console.log(` Requests: ${summary.requestCount}`);
250
330
  console.log(` Tools: ${summary.toolCalls} calls`);
251
331
  console.log(` Files: ${summary.filesModified} modified`);
332
+ // v1.9.0: Per-tool breakdown from MetricsCollector
333
+ const metrics = agent.getMetrics();
334
+ const snap = metrics.snapshot();
335
+ const toolCounters = snap.counters.filter(c => c.name === 'tool_calls_total');
336
+ if (toolCounters.length > 0) {
337
+ console.log(c(' Per-tool:', 'dim'));
338
+ for (const tc of toolCounters.sort((a, b) => b.value - a.value)) {
339
+ const hist = snap.histograms.find(h => h.name === 'tool_latency_seconds' && h.labels.tool === tc.labels.tool);
340
+ const avg = hist && hist.count > 0 ? (hist.sum / hist.count * 1000).toFixed(0) : '?';
341
+ console.log(c(` ${tc.labels.tool}: ${tc.value} calls (avg ${avg}ms)`, 'dim'));
342
+ }
343
+ }
344
+ // Risk summary
345
+ const riskScorer = agent.getRiskScorer();
346
+ const riskAvg = riskScorer.getSessionAverage();
347
+ if (riskScorer.getHistory().length > 0) {
348
+ console.log(` Risk: avg ${riskAvg}/100`);
349
+ }
350
+ // Save metrics
351
+ metrics.save();
352
+ metrics.exportOtel();
252
353
  }
253
354
  function createProvider(config) {
254
355
  if (config.provider === 'anthropic') {
@@ -328,8 +429,14 @@ function renderEvent(event, agent) {
328
429
  process.stdout.write('\n');
329
430
  isThinking = false;
330
431
  }
331
- console.log(c(`\n⚡ ${event.toolCall?.name}`, 'yellow') +
332
- c(`(${formatArgs(event.toolCall?.args)})`, 'dim'));
432
+ {
433
+ const riskStr = event.risk
434
+ ? ' ' + risk_1.RiskScorer.formatIndicator({ score: event.risk.score, level: event.risk.level, factors: [] })
435
+ : '';
436
+ console.log(c(`\n⚡ ${event.toolCall?.name}`, 'yellow') +
437
+ c(`(${formatArgs(event.toolCall?.args)})`, 'dim') +
438
+ riskStr);
439
+ }
333
440
  break;
334
441
  case 'tool_result':
335
442
  if (event.toolResult?.is_error) {
@@ -402,6 +509,8 @@ function handleSlashCommand(input, agent, config) {
402
509
  /undo Undo last file edit (/undo [path])
403
510
  /usage Show token usage & cost for this session
404
511
  /cost Show running cost
512
+ /metrics Show session metrics (counters + histograms)
513
+ /risk Show risk assessment summary
405
514
  /policy Show current security policy
406
515
  /audit Verify audit chain for this session
407
516
  /config Show current config
@@ -478,6 +587,26 @@ function handleSlashCommand(input, agent, config) {
478
587
  console.log(c(` ${tracker.formatStatusLine()}`, 'dim'));
479
588
  break;
480
589
  }
590
+ case '/metrics': {
591
+ const metricsOutput = agent.getMetrics().formatSummary();
592
+ console.log('\n' + metricsOutput);
593
+ break;
594
+ }
595
+ case '/risk': {
596
+ const riskHistory = agent.getRiskScorer().getHistory();
597
+ if (riskHistory.length === 0) {
598
+ console.log(c('No risk assessments yet.', 'dim'));
599
+ }
600
+ else {
601
+ const avg = agent.getRiskScorer().getSessionAverage();
602
+ console.log(c(`\nRisk Summary: ${riskHistory.length} assessments, avg ${avg}/100`, 'bold'));
603
+ const last5 = riskHistory.slice(-5);
604
+ for (const a of last5) {
605
+ console.log(` ${risk_1.RiskScorer.formatIndicator(a)}`);
606
+ }
607
+ }
608
+ break;
609
+ }
481
610
  case '/policy': {
482
611
  const policy = agent.getPolicyEnforcer().getPolicy();
483
612
  console.log(c('\nCurrent Policy:', 'bold'));
@@ -637,6 +766,32 @@ function parseArgs(argv) {
637
766
  }
638
767
  continue;
639
768
  }
769
+ if (arg === '--export-audit') {
770
+ const next = argv[i + 1];
771
+ if (next && !next.startsWith('--')) {
772
+ result['export-audit'] = next;
773
+ i++;
774
+ }
775
+ else {
776
+ result['export-audit'] = true;
777
+ }
778
+ continue;
779
+ }
780
+ if (arg === '--replay') {
781
+ const next = argv[i + 1];
782
+ if (next && !next.startsWith('--')) {
783
+ result['replay'] = next;
784
+ i++;
785
+ }
786
+ else {
787
+ result['replay'] = true; // replay latest
788
+ }
789
+ continue;
790
+ }
791
+ if (arg === '--deterministic') {
792
+ result['deterministic'] = true;
793
+ continue;
794
+ }
640
795
  if (arg.startsWith('--')) {
641
796
  const key = arg.slice(2);
642
797
  const next = argv[i + 1];
@@ -690,8 +845,13 @@ ${c('Options:', 'bold')}
690
845
  ${c('Security & Policy:', 'bold')}
691
846
  --init-policy Generate default .codebot/policy.json
692
847
  --verify-audit [id] Verify audit log hash chain integrity
848
+ --export-audit sarif Export audit log as SARIF 2.1.0 JSON
693
849
  --sandbox-info Show Docker sandbox status
694
850
 
851
+ ${c('Debugging & Replay:', 'bold')}
852
+ --replay [id] Replay a session, re-execute tools, compare outputs
853
+ --deterministic Set temperature=0 for reproducible outputs
854
+
695
855
  ${c('Supported Providers:', 'bold')}
696
856
  Local: Ollama, LM Studio, vLLM (auto-detected)
697
857
  Anthropic: Claude Opus/Sonnet/Haiku (ANTHROPIC_API_KEY)
@@ -711,6 +871,7 @@ ${c('Examples:', 'bold')}
711
871
  codebot --autonomous "refactor src/" Full auto, no prompts
712
872
  codebot --init-policy Create security policy
713
873
  codebot --verify-audit Check audit integrity
874
+ codebot --export-audit sarif > r.sarif Export SARIF report
714
875
 
715
876
  ${c('Interactive Commands:', 'bold')}
716
877
  /help Show commands
@@ -722,6 +883,8 @@ ${c('Interactive Commands:', 'bold')}
722
883
  /compact Force context compaction
723
884
  /usage Show token usage & cost
724
885
  /cost Show running cost
886
+ /metrics Show session metrics
887
+ /risk Show risk assessment summary
725
888
  /policy Show security policy
726
889
  /audit Verify session audit chain
727
890
  /config Show configuration
package/dist/history.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Message } from './types';
2
+ import { IntegrityResult } from './integrity';
2
3
  export interface SessionMeta {
3
4
  id: string;
4
5
  model: string;
@@ -11,14 +12,17 @@ export declare class SessionManager {
11
12
  private sessionId;
12
13
  private filePath;
13
14
  private model;
15
+ private integrityKey;
14
16
  constructor(model: string, sessionId?: string);
15
17
  getId(): string;
16
- /** Append a message to the session file */
18
+ /** Append a message to the session file (HMAC signed) */
17
19
  save(message: Message): void;
18
- /** Save all messages (atomic overwrite via temp file + rename) */
20
+ /** Save all messages (atomic overwrite, HMAC signed) */
19
21
  saveAll(messages: Message[]): void;
20
- /** Load messages from a session file (skips malformed lines) */
22
+ /** Load messages from a session file (verifies HMAC, drops tampered) */
21
23
  load(): Message[];
24
+ /** Verify integrity of all messages in this session. */
25
+ verifyIntegrity(): IntegrityResult;
22
26
  /** List recent sessions */
23
27
  static list(limit?: number): SessionMeta[];
24
28
  /** Get the most recent session ID */
package/dist/history.js CHANGED
@@ -38,38 +38,50 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const os = __importStar(require("os"));
40
40
  const crypto = __importStar(require("crypto"));
41
+ const integrity_1 = require("./integrity");
41
42
  const SESSIONS_DIR = path.join(os.homedir(), '.codebot', 'sessions');
42
43
  class SessionManager {
43
44
  sessionId;
44
45
  filePath;
45
46
  model;
47
+ integrityKey;
46
48
  constructor(model, sessionId) {
47
49
  this.model = model;
48
50
  this.sessionId = sessionId || crypto.randomUUID();
51
+ this.integrityKey = (0, integrity_1.deriveSessionKey)(this.sessionId);
49
52
  fs.mkdirSync(SESSIONS_DIR, { recursive: true });
50
53
  this.filePath = path.join(SESSIONS_DIR, `${this.sessionId}.jsonl`);
51
54
  }
52
55
  getId() {
53
56
  return this.sessionId;
54
57
  }
55
- /** Append a message to the session file */
58
+ /** Append a message to the session file (HMAC signed) */
56
59
  save(message) {
57
60
  try {
58
- const line = JSON.stringify({
61
+ const record = {
59
62
  ...message,
60
63
  _ts: new Date().toISOString(),
61
64
  _model: this.model,
62
- });
63
- fs.appendFileSync(this.filePath, line + '\n');
65
+ };
66
+ record._sig = (0, integrity_1.signMessage)(record, this.integrityKey);
67
+ fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n');
64
68
  }
65
69
  catch {
66
70
  // Don't crash on write failure — session persistence is best-effort
67
71
  }
68
72
  }
69
- /** Save all messages (atomic overwrite via temp file + rename) */
73
+ /** Save all messages (atomic overwrite, HMAC signed) */
70
74
  saveAll(messages) {
71
75
  try {
72
- const lines = messages.map(m => JSON.stringify({ ...m, _ts: new Date().toISOString(), _model: this.model }));
76
+ const lines = messages.map(m => {
77
+ const record = {
78
+ ...m,
79
+ _ts: new Date().toISOString(),
80
+ _model: this.model,
81
+ };
82
+ record._sig = (0, integrity_1.signMessage)(record, this.integrityKey);
83
+ return JSON.stringify(record);
84
+ });
73
85
  const tmpPath = this.filePath + '.tmp';
74
86
  fs.writeFileSync(tmpPath, lines.join('\n') + '\n');
75
87
  fs.renameSync(tmpPath, this.filePath);
@@ -78,7 +90,7 @@ class SessionManager {
78
90
  // Don't crash — session persistence is best-effort
79
91
  }
80
92
  }
81
- /** Load messages from a session file (skips malformed lines) */
93
+ /** Load messages from a session file (verifies HMAC, drops tampered) */
82
94
  load() {
83
95
  if (!fs.existsSync(this.filePath))
84
96
  return [];
@@ -92,20 +104,55 @@ class SessionManager {
92
104
  if (!content)
93
105
  return [];
94
106
  const messages = [];
107
+ let dropped = 0;
95
108
  for (const line of content.split('\n')) {
96
109
  try {
97
110
  const obj = JSON.parse(line);
111
+ // Verify integrity if signature present (backward compat: unsigned messages pass)
112
+ if (obj._sig) {
113
+ if (!(0, integrity_1.verifyMessage)(obj, this.integrityKey)) {
114
+ dropped++;
115
+ continue; // Drop tampered message
116
+ }
117
+ }
98
118
  delete obj._ts;
99
119
  delete obj._model;
120
+ delete obj._sig;
100
121
  messages.push(obj);
101
122
  }
102
123
  catch {
103
- // Skip malformed line — don't crash the whole load
104
124
  continue;
105
125
  }
106
126
  }
127
+ if (dropped > 0) {
128
+ console.warn(`Warning: Dropped ${dropped} tampered message(s) from session ${this.sessionId.substring(0, 8)}.`);
129
+ }
107
130
  return messages;
108
131
  }
132
+ /** Verify integrity of all messages in this session. */
133
+ verifyIntegrity() {
134
+ if (!fs.existsSync(this.filePath)) {
135
+ return { valid: 0, tampered: 0, unsigned: 0, tamperedIndices: [] };
136
+ }
137
+ try {
138
+ const content = fs.readFileSync(this.filePath, 'utf-8').trim();
139
+ if (!content)
140
+ return { valid: 0, tampered: 0, unsigned: 0, tamperedIndices: [] };
141
+ const records = [];
142
+ for (const line of content.split('\n')) {
143
+ try {
144
+ records.push(JSON.parse(line));
145
+ }
146
+ catch {
147
+ continue;
148
+ }
149
+ }
150
+ return (0, integrity_1.verifyMessages)(records, this.integrityKey);
151
+ }
152
+ catch {
153
+ return { valid: 0, tampered: 0, unsigned: 0, tamperedIndices: [] };
154
+ }
155
+ }
109
156
  /** List recent sessions */
110
157
  static list(limit = 10) {
111
158
  if (!fs.existsSync(SESSIONS_DIR))
package/dist/index.d.ts CHANGED
@@ -11,5 +11,17 @@ export { loadPlugins } from './plugins';
11
11
  export { loadMCPTools } from './mcp';
12
12
  export { MODEL_REGISTRY, PROVIDER_DEFAULTS, getModelInfo, detectProvider } from './providers/registry';
13
13
  export type { ModelInfo } from './providers/registry';
14
+ export { CapabilityChecker } from './capabilities';
15
+ export type { ToolCapabilities, CapabilityConfig } from './capabilities';
16
+ export { deriveSessionKey, signMessage, verifyMessage, verifyMessages } from './integrity';
17
+ export type { IntegrityResult } from './integrity';
18
+ export { ReplayProvider, loadSessionForReplay, compareOutputs, listReplayableSessions } from './replay';
19
+ export type { SessionReplayData, ReplayDivergence } from './replay';
20
+ export { MetricsCollector } from './metrics';
21
+ export type { MetricsSnapshot, CounterValue, HistogramValue } from './metrics';
22
+ export { RiskScorer } from './risk';
23
+ export type { RiskAssessment, RiskFactor } from './risk';
24
+ export { exportSarif, sarifToString } from './sarif';
25
+ export type { SarifLog, SarifResult, SarifRule } from './sarif';
14
26
  export * from './types';
15
27
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = void 0;
17
+ exports.sarifToString = exports.exportSarif = exports.RiskScorer = exports.MetricsCollector = exports.listReplayableSessions = exports.compareOutputs = exports.loadSessionForReplay = exports.ReplayProvider = exports.verifyMessages = exports.verifyMessage = exports.signMessage = exports.deriveSessionKey = exports.CapabilityChecker = exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = void 0;
18
18
  var agent_1 = require("./agent");
19
19
  Object.defineProperty(exports, "Agent", { enumerable: true, get: function () { return agent_1.Agent; } });
20
20
  var openai_1 = require("./providers/openai");
@@ -42,5 +42,24 @@ Object.defineProperty(exports, "MODEL_REGISTRY", { enumerable: true, get: functi
42
42
  Object.defineProperty(exports, "PROVIDER_DEFAULTS", { enumerable: true, get: function () { return registry_1.PROVIDER_DEFAULTS; } });
43
43
  Object.defineProperty(exports, "getModelInfo", { enumerable: true, get: function () { return registry_1.getModelInfo; } });
44
44
  Object.defineProperty(exports, "detectProvider", { enumerable: true, get: function () { return registry_1.detectProvider; } });
45
+ var capabilities_1 = require("./capabilities");
46
+ Object.defineProperty(exports, "CapabilityChecker", { enumerable: true, get: function () { return capabilities_1.CapabilityChecker; } });
47
+ var integrity_1 = require("./integrity");
48
+ Object.defineProperty(exports, "deriveSessionKey", { enumerable: true, get: function () { return integrity_1.deriveSessionKey; } });
49
+ Object.defineProperty(exports, "signMessage", { enumerable: true, get: function () { return integrity_1.signMessage; } });
50
+ Object.defineProperty(exports, "verifyMessage", { enumerable: true, get: function () { return integrity_1.verifyMessage; } });
51
+ Object.defineProperty(exports, "verifyMessages", { enumerable: true, get: function () { return integrity_1.verifyMessages; } });
52
+ var replay_1 = require("./replay");
53
+ Object.defineProperty(exports, "ReplayProvider", { enumerable: true, get: function () { return replay_1.ReplayProvider; } });
54
+ Object.defineProperty(exports, "loadSessionForReplay", { enumerable: true, get: function () { return replay_1.loadSessionForReplay; } });
55
+ Object.defineProperty(exports, "compareOutputs", { enumerable: true, get: function () { return replay_1.compareOutputs; } });
56
+ Object.defineProperty(exports, "listReplayableSessions", { enumerable: true, get: function () { return replay_1.listReplayableSessions; } });
57
+ var metrics_1 = require("./metrics");
58
+ Object.defineProperty(exports, "MetricsCollector", { enumerable: true, get: function () { return metrics_1.MetricsCollector; } });
59
+ var risk_1 = require("./risk");
60
+ Object.defineProperty(exports, "RiskScorer", { enumerable: true, get: function () { return risk_1.RiskScorer; } });
61
+ var sarif_1 = require("./sarif");
62
+ Object.defineProperty(exports, "exportSarif", { enumerable: true, get: function () { return sarif_1.exportSarif; } });
63
+ Object.defineProperty(exports, "sarifToString", { enumerable: true, get: function () { return sarif_1.sarifToString; } });
45
64
  __exportStar(require("./types"), exports);
46
65
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Session History Integrity for CodeBot v1.8.0
3
+ *
4
+ * HMAC-SHA256 signing/verification for session messages.
5
+ * Key: SHA-256(sessionId + machineId) where machineId = hostname:username.
6
+ *
7
+ * This is tamper-DETECTION, not tamper-prevention. The key is deterministic
8
+ * per session+machine so verification works without storing keys separately.
9
+ *
10
+ * Uses Node's built-in crypto module — zero runtime dependencies.
11
+ */
12
+ /** Derive a per-session HMAC key. Deterministic for same session+machine. */
13
+ export declare function deriveSessionKey(sessionId: string): Buffer;
14
+ /**
15
+ * Compute HMAC-SHA256 signature for a message object.
16
+ * Excludes the _sig field from the signing input.
17
+ */
18
+ export declare function signMessage(message: Record<string, unknown>, key: Buffer): string;
19
+ /**
20
+ * Verify a message's HMAC signature.
21
+ * Returns false if no signature present or if it doesn't match.
22
+ */
23
+ export declare function verifyMessage(message: Record<string, unknown>, key: Buffer): boolean;
24
+ export interface IntegrityResult {
25
+ valid: number;
26
+ tampered: number;
27
+ unsigned: number;
28
+ tamperedIndices: number[];
29
+ }
30
+ /**
31
+ * Verify an array of signed messages.
32
+ * Unsigned messages (pre-v1.8.0) are counted but not flagged as tampered.
33
+ */
34
+ export declare function verifyMessages(messages: Array<Record<string, unknown>>, key: Buffer): IntegrityResult;
35
+ //# sourceMappingURL=integrity.d.ts.map
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * Session History Integrity for CodeBot v1.8.0
4
+ *
5
+ * HMAC-SHA256 signing/verification for session messages.
6
+ * Key: SHA-256(sessionId + machineId) where machineId = hostname:username.
7
+ *
8
+ * This is tamper-DETECTION, not tamper-prevention. The key is deterministic
9
+ * per session+machine so verification works without storing keys separately.
10
+ *
11
+ * Uses Node's built-in crypto module — zero runtime dependencies.
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.deriveSessionKey = deriveSessionKey;
48
+ exports.signMessage = signMessage;
49
+ exports.verifyMessage = verifyMessage;
50
+ exports.verifyMessages = verifyMessages;
51
+ const crypto = __importStar(require("crypto"));
52
+ const os = __importStar(require("os"));
53
+ // ── Key Derivation ──
54
+ /** Derive a per-session HMAC key. Deterministic for same session+machine. */
55
+ function deriveSessionKey(sessionId) {
56
+ const machineId = os.hostname() + ':' + getUserName();
57
+ return crypto.createHash('sha256')
58
+ .update(sessionId + machineId)
59
+ .digest();
60
+ }
61
+ /** Get username safely. */
62
+ function getUserName() {
63
+ try {
64
+ return os.userInfo().username;
65
+ }
66
+ catch {
67
+ return 'unknown';
68
+ }
69
+ }
70
+ // ── Signing & Verification ──
71
+ /**
72
+ * Compute HMAC-SHA256 signature for a message object.
73
+ * Excludes the _sig field from the signing input.
74
+ */
75
+ function signMessage(message, key) {
76
+ const payload = canonicalize(message);
77
+ return crypto.createHmac('sha256', key)
78
+ .update(payload)
79
+ .digest('hex');
80
+ }
81
+ /**
82
+ * Verify a message's HMAC signature.
83
+ * Returns false if no signature present or if it doesn't match.
84
+ */
85
+ function verifyMessage(message, key) {
86
+ const sig = message._sig;
87
+ if (!sig)
88
+ return false;
89
+ const expected = signMessage(message, key);
90
+ try {
91
+ return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'));
92
+ }
93
+ catch {
94
+ return false; // Different lengths or invalid hex
95
+ }
96
+ }
97
+ /**
98
+ * Canonical JSON representation for signing.
99
+ * Excludes _sig field. Keys are sorted for determinism.
100
+ */
101
+ function canonicalize(obj) {
102
+ const sorted = {};
103
+ for (const key of Object.keys(obj).sort()) {
104
+ if (key === '_sig')
105
+ continue;
106
+ sorted[key] = obj[key];
107
+ }
108
+ return JSON.stringify(sorted);
109
+ }
110
+ /**
111
+ * Verify an array of signed messages.
112
+ * Unsigned messages (pre-v1.8.0) are counted but not flagged as tampered.
113
+ */
114
+ function verifyMessages(messages, key) {
115
+ let valid = 0;
116
+ let tampered = 0;
117
+ let unsigned = 0;
118
+ const tamperedIndices = [];
119
+ for (let i = 0; i < messages.length; i++) {
120
+ const msg = messages[i];
121
+ if (!msg._sig) {
122
+ unsigned++;
123
+ continue;
124
+ }
125
+ if (verifyMessage(msg, key)) {
126
+ valid++;
127
+ }
128
+ else {
129
+ tampered++;
130
+ tamperedIndices.push(i);
131
+ }
132
+ }
133
+ return { valid, tampered, unsigned, tamperedIndices };
134
+ }
135
+ //# sourceMappingURL=integrity.js.map