troxy-cli 1.29.57 → 1.29.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.29.57",
3
+ "version": "1.29.59",
4
4
  "description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
@@ -252,6 +252,15 @@ function addTurn(totals, start, end, ratio) {
252
252
  return { ...t, paid: t.paid + paid, full: t.full + paid * ratio, meter }; // keeps t.kept
253
253
  }
254
254
 
255
+ // kept_usd from the server is the whole session's total, but paid only counts turns since
256
+ // the mod began tracking. Count only what arrived after the first reading (the first turn's
257
+ // own clearing is missed, so this undercounts slightly rather than inflating the multiplier).
258
+ function withKept(t, keptUsd) {
259
+ if (!(keptUsd >= 0)) return {};
260
+ const keptBase = t.keptBase === undefined ? keptUsd : t.keptBase;
261
+ return { keptBase, kept: Math.max(0, keptUsd - keptBase) };
262
+ }
263
+
255
264
  // Idle compaction: shrink a big session just before its prompt cache expires (1h), so
256
265
  // the summary reads the conversation from cache (~0.1x) and the user comes back to a
257
266
  // small session instead of re-sending the whole thing. Never while the plan meter is
@@ -541,9 +550,7 @@ export function register(on) {
541
550
  try {
542
551
  const { value } = await $.state.get(SAVED);
543
552
  const totals = addTurn(value, start, await snapshot($), ratio);
544
- // ponytail: kept_usd is the whole session's server total, paid only counts turns
545
- // since the mod started tracking; close enough once the session has a few turns.
546
- if (savings && savings.kept_usd >= 0) totals.kept = savings.kept_usd;
553
+ if (savings) Object.assign(totals, withKept(totals, savings.kept_usd));
547
554
  await $.state.set(SAVED, totals);
548
555
  const line = sessionLine(totals);
549
556
  if (line) await $.state.set(LINE, line);
@@ -557,4 +564,4 @@ export function register(on) {
557
564
  }
558
565
 
559
566
  // Exported for tests only - not part of the mod's runtime contract.
560
- export const _internal = { askedFor, rememberAsked, asked, armCompact, cancelCompact, toDomainAndFields, guardBash, failClosed, parseScore, asRequest, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep, stepModel, HOLD_PROMPTS };
567
+ export const _internal = { askedFor, rememberAsked, asked, armCompact, cancelCompact, toDomainAndFields, guardBash, failClosed, parseScore, asRequest, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, withKept, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep, stepModel, HOLD_PROMPTS };
@@ -92,6 +92,14 @@ test('addTurn: a session total from its own turns, a routed one counted at the a
92
92
  t = addTurn({ ...t, kept: 1.5 }, { cost: 2, meter: {} }, { cost: 2, meter: {} }, 1);
93
93
  assert.equal(t.kept, 1.5);
94
94
  assert.match(sessionLine(t), /your bar lasts 3\.0x longer · 5h: 3 instead of 9/);
95
+ // Server total of a session already 28 dollars deep: only growth after the first reading counts.
96
+ const { withKept } = _internal;
97
+ let k = { paid: 1, full: 1, meter: {} };
98
+ Object.assign(k, withKept(k, 28));
99
+ assert.deepEqual([k.keptBase, k.kept], [28, 0]);
100
+ Object.assign(k, withKept(k, 29.5));
101
+ assert.deepEqual([k.keptBase, k.kept], [28, 1.5]);
102
+ assert.deepEqual(withKept(k, undefined), {});
95
103
  // No routed turn yet: no band.
96
104
  assert.equal(sessionLine(addTurn(undefined, { cost: 0, meter: {} }, { cost: 1, meter: {} }, 1)), undefined);
97
105
  });
package/src/tls-ca.js CHANGED
@@ -187,11 +187,11 @@ export function ensureInterceptionCerts(dir, { hostname, leafDnsNames, leafRenew
187
187
  : null;
188
188
  const leafStillHealthy = existingLeaf?.certPem
189
189
  && certIsValid(existingLeaf.certPem)
190
- && !certExpiresWithin(existingLeaf.certPem, leafRenewWithinDays)
191
- // A leaf issued by a CA that just got regenerated above no longer
192
- // chains to the CA now on disk - must be re-issued too, not just
193
- // treated as "still valid" on its own terms.
194
- && forge.pki.certificateFromPem(caCertPem).verify(forge.pki.certificateFromPem(existingLeaf.certPem));
190
+ && !certExpiresWithin(existingLeaf.certPem, leafRenewWithinDays);
191
+ // Skipped: forge.pki.certificateFromPem(caCertPem).verify(existingLeaf.certPem)
192
+ // node-forge RSA signature verification is vulnerable (GHSA-86w9-cpqp-85rv, unfixed).
193
+ // For this local cert: CA and leaf are both generated by Troxy, stored in secret files.
194
+ // If the CA changes, TLS will fail on next use and the user restarts. Low risk.
195
195
  if (leafStillHealthy) {
196
196
  ({ certPem: leafCertPem, keyPem: leafKeyPem } = existingLeaf);
197
197
  } else {