troxy-cli 1.29.4 → 1.29.5

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.4",
3
+ "version": "1.29.5",
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": {
@@ -73,6 +73,21 @@ export function parseConnectTarget(url) {
73
73
  // meaningless (or wrong) to forward on either outbound leg.
74
74
  const _HOP_BY_HOP_HEADERS = new Set(['host', 'connection', 'content-length', 'transfer-encoding']);
75
75
 
76
+ /** 'desktop' | 'terminal' | null. Verified live 2026-09-11 against two real
77
+ * captured user-agents from this same machine: terminal sends
78
+ * "claude-cli/2.1.236 (external, cli)", desktop sends "claude-cli/2.1.260
79
+ * (external, claude-desktop, agent-sdk/0.3.260)" - the literal substring
80
+ * "claude-desktop" is the one stable, explicit signal found; everything
81
+ * else in the string (version numbers) will drift across releases. null
82
+ * (never a guess) when the client sends no recognizable claude-cli
83
+ * user-agent at all - some other tool on this port, or a future format
84
+ * change this hasn't been updated for. */
85
+ export function detectSurface(incomingHeaders) {
86
+ const ua = incomingHeaders['user-agent'] || '';
87
+ if (!/^claude-cli\//.test(ua)) return null;
88
+ return ua.includes('claude-desktop') ? 'desktop' : 'terminal';
89
+ }
90
+
76
91
  /** Headers for the leg going to Troxy: the client's own Authorization (an
77
92
  * Anthropic API key or OAuth token) is never sent to Troxy as-is - it
78
93
  * rides the side channel app.py already knows how to consume
@@ -87,6 +102,8 @@ export function buildTroxyForwardHeaders(incomingHeaders, troxyKey) {
87
102
  if (incomingHeaders.authorization) {
88
103
  headers['x-troxy-upstream-authorization'] = incomingHeaders.authorization;
89
104
  }
105
+ const surface = detectSurface(incomingHeaders);
106
+ if (surface) headers['x-troxy-surface'] = surface;
90
107
  headers.authorization = `Bearer ${troxyKey}`;
91
108
  return headers;
92
109
  }
@@ -21,6 +21,7 @@ import { format } from 'node:util';
21
21
 
22
22
  import {
23
23
  parseConnectTarget,
24
+ detectSurface,
24
25
  buildTroxyForwardHeaders,
25
26
  buildRelayHeaders,
26
27
  createInterceptor,
@@ -41,7 +42,43 @@ describe('parseConnectTarget', () => {
41
42
  });
42
43
  });
43
44
 
45
+ describe('detectSurface', () => {
46
+ // Real user-agents captured live 2026-09-11 from this same machine's own
47
+ // terminal and desktop-app traffic through this interceptor.
48
+ it('recognizes a real desktop-app user-agent', () => {
49
+ assert.equal(
50
+ detectSurface({ 'user-agent': 'claude-cli/2.1.260 (external, claude-desktop, agent-sdk/0.3.260)' }),
51
+ 'desktop',
52
+ );
53
+ });
54
+
55
+ it('recognizes a real terminal user-agent', () => {
56
+ assert.equal(detectSurface({ 'user-agent': 'claude-cli/2.1.236 (external, cli)' }), 'terminal');
57
+ });
58
+
59
+ it('returns null when there is no user-agent at all', () => {
60
+ assert.equal(detectSurface({}), null);
61
+ });
62
+
63
+ it('returns null for a user-agent that is not claude-cli - never a guess', () => {
64
+ assert.equal(detectSurface({ 'user-agent': 'curl/8.4.0' }), null);
65
+ });
66
+ });
67
+
44
68
  describe('buildTroxyForwardHeaders', () => {
69
+ it('sets x-troxy-surface from a recognized user-agent', () => {
70
+ const headers = buildTroxyForwardHeaders(
71
+ { 'user-agent': 'claude-cli/2.1.260 (external, claude-desktop, agent-sdk/0.3.260)' },
72
+ 'txy-key',
73
+ );
74
+ assert.equal(headers['x-troxy-surface'], 'desktop');
75
+ });
76
+
77
+ it('omits x-troxy-surface entirely when the surface cannot be determined', () => {
78
+ const headers = buildTroxyForwardHeaders({ 'user-agent': 'curl/8.4.0' }, 'txy-key');
79
+ assert.equal(headers['x-troxy-surface'], undefined);
80
+ });
81
+
45
82
  it('translates the client\'s own Authorization into X-Troxy-Upstream-Authorization, ' +
46
83
  'and sets Authorization to the Troxy key instead', () => {
47
84
  const headers = buildTroxyForwardHeaders(