troxy-cli 1.2.8 → 1.3.1

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.2.8",
3
+ "version": "1.3.1",
4
4
  "description": "AI payment control — protect your agent's payments with policies",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -24,9 +24,12 @@ async function request(method, path, { apiKey, jwt, body } = {}) {
24
24
 
25
25
  export const api = {
26
26
  // Auth
27
- health: () => request('GET', '/health'),
28
- magicLink: (email) => request('POST', '/auth/magic-link', { body: { email } }),
29
- verify: (token) => request('POST', '/auth/verify', { body: { token } }),
27
+ health: () => request('GET', '/health'),
28
+ magicLink: (email) => request('POST', '/auth/magic-link', { body: { email } }),
29
+ verify: (token) => request('POST', '/auth/verify', { body: { token } }),
30
+ cliStart: () => request('POST', '/auth/cli/start', {}),
31
+ cliAuthorize: (jwt, session_id) => request('POST', '/auth/cli/authorize', { jwt, body: { session_id } }),
32
+ cliExchange: (session_id, code) => request('POST', '/auth/cli/exchange', { body: { session_id, code } }),
30
33
 
31
34
  // Cards
32
35
  listCards: (jwt) => request('GET', '/cards', { jwt }),
package/src/auth.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
4
  import readline from 'readline';
5
+ import { exec } from 'child_process';
5
6
  import { api } from './api.js';
6
7
 
7
8
  const SESSION_FILE = path.join(os.homedir(), '.troxy', 'session.json');
@@ -66,34 +67,57 @@ function loadConfig() {
66
67
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
67
68
  }
68
69
 
69
- /** Interactive magic-link login flow. */
70
- export async function runLogin({ email } = {}) {
71
- if (!email) {
72
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
73
- email = await new Promise(resolve => rl.question(' Email: ', ans => { rl.close(); resolve(ans.trim()); }));
74
- }
70
+ function _openBrowser(url) {
71
+ const isHeadless = process.platform === 'linux'
72
+ && !process.env.DISPLAY
73
+ && !process.env.WAYLAND_DISPLAY;
74
+ if (isHeadless) return;
75
+ const cmd = process.platform === 'darwin' ? `open "${url}"`
76
+ : process.platform === 'win32' ? `start "" "${url}"`
77
+ : `xdg-open "${url}"`;
78
+ exec(cmd, () => {});
79
+ }
75
80
 
76
- process.stdout.write(`\n Sending magic link to ${email}... `);
81
+ /** Device-code login flow — opens browser, user copies code back to CLI. */
82
+ export async function runLogin() {
83
+ // 1. Start a CLI auth session
84
+ let session;
77
85
  try {
78
- await api.magicLink(email);
79
- console.log('✓');
86
+ session = await api.cliStart();
80
87
  } catch (err) {
81
- console.log('✗');
82
- console.error(` Error: ${err.message}\n`);
88
+ console.error(`\n Error starting login: ${err.message}\n`);
83
89
  process.exit(1);
84
90
  }
85
91
 
92
+ // 2. Open browser (or print URL on headless servers)
93
+ const isHeadless = process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
94
+ if (isHeadless) {
95
+ console.log('\n Open this URL in your browser to get a login code:\n');
96
+ console.log(` ${session.url}\n`);
97
+ } else {
98
+ console.log('\n Opening browser to complete login...');
99
+ console.log(` If it didn't open, visit:\n ${session.url}\n`);
100
+ _openBrowser(session.url);
101
+ }
102
+
103
+ // 3. Prompt for the code shown in the browser
86
104
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
87
- const token = await new Promise(resolve =>
88
- rl.question(' Enter the code from your email: ', ans => { rl.close(); resolve(ans.trim()); })
105
+ const code = await new Promise(resolve =>
106
+ rl.question(' Paste the code from your browser: ', ans => { rl.close(); resolve(ans.trim()); })
89
107
  );
90
108
 
109
+ if (!code) {
110
+ console.error('\n No code entered. Run troxy login to try again.\n');
111
+ process.exit(1);
112
+ }
113
+
114
+ // 4. Exchange code for JWT
91
115
  process.stdout.write(' Verifying... ');
92
116
  try {
93
- const result = await api.verify(token);
94
- saveSession({ jwt: result.access_token, email });
117
+ const result = await api.cliExchange(session.session_id, code);
118
+ saveSession({ jwt: result.access_token, email: result.email });
95
119
  console.log('✓');
96
- console.log(`\n Logged in as ${email}\n`);
120
+ console.log(`\n Logged in as ${result.email} (session valid for 12 hours)\n`);
97
121
  } catch (err) {
98
122
  console.log('✗');
99
123
  console.error(` Error: ${err.message}\n`);
package/src/policies.js CHANGED
@@ -126,9 +126,11 @@ export async function runPolicies([sub, ...args], flags) {
126
126
  }
127
127
  }
128
128
 
129
+ const _isAny = x => !x.field || x.field === 'any' || x.operator === 'any';
130
+
129
131
  function _condSummary(p) {
130
- const c = p.conditions || [];
131
- const o = p.or_conditions || [];
132
+ const c = (p.conditions || []).filter(x => !_isAny(x));
133
+ const o = (p.or_conditions || []).filter(row => (row.conditions || []).some(x => !_isAny(x)));
132
134
  const total = c.length + o.length;
133
135
  if (total === 0) return 'always';
134
136
  return `${total} condition${total > 1 ? 's' : ''}`;
@@ -138,13 +140,15 @@ function _condDetail(p) {
138
140
  const c = p.conditions || [];
139
141
  const or = p.or_conditions || [];
140
142
  const parts = [];
141
- if (c.length) {
142
- parts.push(c.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND '));
143
+ const real = c.filter(x => !_isAny(x));
144
+ if (real.length) {
145
+ parts.push(real.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND '));
143
146
  }
144
147
  if (or.length) {
145
148
  or.forEach(row => {
146
- const conds = (row.conditions || []).map(x => `${x.field} ${x.operator} ${x.value || ''}`).join(' AND ');
147
- parts.push(`${row.action}${conds ? ' if ' + conds : ''}`);
149
+ const realConds = (row.conditions || []).filter(x => !_isAny(x));
150
+ const conds = realConds.map(x => `${x.field} ${x.operator} ${x.value || ''}`).join(' AND ');
151
+ parts.push(`${row.action || ''}${conds ? ' if ' + conds : ''}`);
148
152
  });
149
153
  }
150
154
  return parts.length ? parts.join('\n ') : 'none (always matches)';