troxy-cli 1.29.56 → 1.29.58

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.56",
3
+ "version": "1.29.58",
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": {
@@ -177,8 +177,10 @@ function savedLine(money, rateLimits = []) {
177
177
  return `${bar(1 / scale)} ${parts.join(' · ')}`;
178
178
  }
179
179
 
180
+ // t.kept: what the content Troxy cleared or compressed would have cost (server's
181
+ // session-savings kept_usd), on top of the model swaps in t.full.
180
182
  function sessionLine(t) {
181
- return t && savedLine({ cost_paid_usd: t.paid, dollars_saved_usd: t.full - t.paid },
183
+ return t && savedLine({ cost_paid_usd: t.paid, dollars_saved_usd: t.full - t.paid + (t.kept || 0) },
182
184
  Object.entries(t.meter).map(([kind, percentUsed]) => ({ kind, percentUsed })));
183
185
  }
184
186
 
@@ -247,7 +249,7 @@ function addTurn(totals, start, end, ratio) {
247
249
  for (const k of Object.keys(end.meter)) {
248
250
  if (k in start.meter) meter[k] = (meter[k] || 0) + Math.max(0, end.meter[k] - start.meter[k]);
249
251
  }
250
- return { paid: t.paid + paid, full: t.full + paid * ratio, meter };
252
+ return { ...t, paid: t.paid + paid, full: t.full + paid * ratio, meter }; // keeps t.kept
251
253
  }
252
254
 
253
255
  // Idle compaction: shrink a big session just before its prompt cache expires (1h), so
@@ -528,21 +530,25 @@ export function register(on) {
528
530
  // After the answer, so counting never delays a prompt.
529
531
  on('turn.complete', async ($, e, next) => {
530
532
  const r = await next(e);
533
+ let savings = null;
534
+ try {
535
+ savings = await fetchSavings($);
536
+ if (savings) await $.state.set(SAVINGS, savingsText(savings));
537
+ } catch {} // a missed fetch only leaves the band as it was
531
538
  if (turn) {
532
539
  const { start, ratio } = turn;
533
540
  turn = null;
534
541
  try {
535
542
  const { value } = await $.state.get(SAVED);
536
543
  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;
537
547
  await $.state.set(SAVED, totals);
538
548
  const line = sessionLine(totals);
539
549
  if (line) await $.state.set(LINE, line);
540
550
  } catch {} // a missed turn only leaves the band as it was
541
551
  }
542
- try {
543
- const text = savingsText(await fetchSavings($));
544
- if (text) await $.state.set(SAVINGS, text);
545
- } catch {} // a missed fetch only leaves the band as it was
546
552
  let stayAwake = false;
547
553
  try { stayAwake = await bigSession($); } catch {}
548
554
  armCompact($, COMPACT_IDLE_MS, { stayAwake, persist: true });
@@ -88,6 +88,10 @@ test('addTurn: a session total from its own turns, a routed one counted at the a
88
88
  t = addTurn(t, { cost: 1.5, meter: { five_hour: 13, seven_day: 5 } }, { cost: 2, meter: { five_hour: 1, seven_day: 6 } }, 1);
89
89
  assert.deepEqual(t, { paid: 1, full: 1.5, meter: { five_hour: 3, seven_day: 2 } });
90
90
  assert.equal(sessionLine(t), '[███████░░░] This session: your bar lasts 1.5x longer · 5h: 3 instead of 5 · 7d: 2 instead of 3 · estimate');
91
+ // Cleared/compressed content counts too, and survives the next turn: $1 paid, $0.5 swap + $1.5 kept = 3x.
92
+ t = addTurn({ ...t, kept: 1.5 }, { cost: 2, meter: {} }, { cost: 2, meter: {} }, 1);
93
+ assert.equal(t.kept, 1.5);
94
+ assert.match(sessionLine(t), /your bar lasts 3\.0x longer · 5h: 3 instead of 9/);
91
95
  // No routed turn yet: no band.
92
96
  assert.equal(sessionLine(addTurn(undefined, { cost: 0, meter: {} }, { cost: 1, meter: {} }, 1)), undefined);
93
97
  });
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 {