cli-tunnel 1.2.0-beta.10 → 1.2.0-beta.11

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/index.js CHANGED
@@ -228,7 +228,7 @@ setInterval(() => {
228
228
  ticketRateLimits.delete(ip);
229
229
  }
230
230
  }, 60000);
231
- const server = http.createServer((req, res) => {
231
+ const server = http.createServer(async (req, res) => {
232
232
  const clientIp = req.socket.remoteAddress || 'unknown';
233
233
  // F-8: Rate limiting for HTTP endpoints
234
234
  if (req.url?.startsWith('/api/')) {
@@ -274,6 +274,40 @@ const server = http.createServer((req, res) => {
274
274
  return;
275
275
  }
276
276
  }
277
+ // Hub ticket proxy — fetch ticket from local session on behalf of grid client
278
+ if (hubMode && req.url?.startsWith('/api/proxy/ticket/') && req.method === 'POST') {
279
+ const targetPort = parseInt(req.url.replace('/api/proxy/ticket/', ''), 10);
280
+ if (!Number.isFinite(targetPort) || targetPort < 1 || targetPort > 65535) {
281
+ res.writeHead(400, { 'Content-Type': 'application/json' });
282
+ res.end(JSON.stringify({ error: 'Invalid port' }));
283
+ return;
284
+ }
285
+ // Find token for this port from session files
286
+ const localSessions = readLocalSessions();
287
+ const session = localSessions.find(s => s.port === targetPort);
288
+ if (!session) {
289
+ res.writeHead(404, { 'Content-Type': 'application/json' });
290
+ res.end(JSON.stringify({ error: 'Session not found' }));
291
+ return;
292
+ }
293
+ try {
294
+ const ticketResp = await fetch(`http://127.0.0.1:${targetPort}/api/auth/ticket`, {
295
+ method: 'POST', headers: { 'Authorization': `Bearer ${session.token}` },
296
+ signal: AbortSignal.timeout(3000),
297
+ });
298
+ if (!ticketResp.ok)
299
+ throw new Error('Ticket request failed');
300
+ const ticketData = await ticketResp.json();
301
+ res.writeHead(200, { 'Content-Type': 'application/json' });
302
+ res.end(JSON.stringify({ ticket: ticketData.ticket, port: targetPort }));
303
+ }
304
+ catch {
305
+ res.writeHead(502, { 'Content-Type': 'application/json' });
306
+ res.end(JSON.stringify({ error: 'Session unreachable' }));
307
+ return;
308
+ }
309
+ return;
310
+ }
277
311
  // Sessions API
278
312
  if ((req.url === '/api/sessions' || req.url?.startsWith('/api/sessions?')) && req.method === 'GET') {
279
313
  try {
@@ -383,7 +417,7 @@ const server = http.createServer((req, res) => {
383
417
  'Content-Type': mimes[ext] || 'application/octet-stream',
384
418
  'X-Frame-Options': 'DENY',
385
419
  'X-Content-Type-Options': 'nosniff',
386
- 'Content-Security-Policy': "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; connect-src 'self' ws://localhost:* wss://*.devtunnels.ms;",
420
+ 'Content-Security-Policy': "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://127.0.0.1:* wss://*.devtunnels.ms https://*.devtunnels.ms;",
387
421
  'Referrer-Policy': 'no-referrer',
388
422
  'Cache-Control': 'no-store',
389
423
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-tunnel",
3
- "version": "1.2.0-beta.10",
3
+ "version": "1.2.0-beta.11",
4
4
  "description": "Tunnel any CLI app to your phone — PTY + devtunnel + xterm.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/remote-ui/app.js CHANGED
@@ -117,7 +117,9 @@
117
117
 
118
118
  async function loadSessions() {
119
119
  try {
120
- const resp = await fetch('/api/sessions');
120
+ const tokenParam = new URLSearchParams(window.location.search).get('token');
121
+ const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
122
+ const resp = await fetch('/api/sessions', { headers });
121
123
  const data = await resp.json();
122
124
  renderDashboard(data.sessions || []);
123
125
  } catch (err) {
@@ -187,17 +189,21 @@
187
189
  };
188
190
 
189
191
  window.cleanOffline = async () => {
190
- const resp = await fetch('/api/sessions');
192
+ const tokenParam = new URLSearchParams(window.location.search).get('token');
193
+ const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
194
+ const resp = await fetch('/api/sessions', { headers });
191
195
  const data = await resp.json();
192
196
  const offline = (data.sessions || []).filter(s => !s.online);
193
197
  for (const s of offline) {
194
- await fetch('/api/sessions/' + s.id, { method: 'DELETE' });
198
+ await fetch('/api/sessions/' + s.id, { method: 'DELETE', headers });
195
199
  }
196
200
  loadSessions();
197
201
  };
198
202
 
199
203
  window.deleteSession = async (id) => {
200
- await fetch('/api/sessions/' + id, { method: 'DELETE' });
204
+ const tokenParam = new URLSearchParams(window.location.search).get('token');
205
+ const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
206
+ await fetch('/api/sessions/' + id, { method: 'DELETE', headers });
201
207
  loadSessions();
202
208
  };
203
209
 
@@ -265,20 +271,24 @@
265
271
  // Delay fit to ensure container has size
266
272
  setTimeout(function() { panelFit.fit(); }, 100);
267
273
 
268
- // Connect WebSocket to this session via ticket auth
274
+ // Connect WebSocket to this session
269
275
  var statusDot = header.querySelector('.grid-panel-status');
270
276
  var panelWs = null;
271
277
 
272
278
  (function connectPanel() {
273
- var sessionOrigin = new URL(s.url).origin;
274
- fetch(sessionOrigin + '/api/auth/ticket', {
279
+ // Use hub's proxy endpoint to get a ticket for the session
280
+ var tokenParam = new URLSearchParams(window.location.search).get('token');
281
+ var proxyUrl = '/api/proxy/ticket/' + s.port;
282
+ var wsBase = s.isLocal ? 'ws://127.0.0.1:' + s.port : s.url.replace('https://', 'wss://');
283
+
284
+ fetch(proxyUrl, {
275
285
  method: 'POST',
276
- headers: { 'Authorization': 'Bearer ' + s.token }
286
+ headers: { 'Authorization': 'Bearer ' + tokenParam }
277
287
  }).then(function(resp) {
278
288
  if (!resp.ok) throw new Error('Auth failed');
279
289
  return resp.json();
280
290
  }).then(function(data) {
281
- panelWs = new WebSocket(sessionOrigin.replace('https://', 'wss://') + '?ticket=' + encodeURIComponent(data.ticket));
291
+ panelWs = new WebSocket(wsBase + '?ticket=' + encodeURIComponent(data.ticket));
282
292
 
283
293
  panelWs.onopen = function() {
284
294
  if (statusDot) { statusDot.style.color = 'var(--green)'; statusDot.title = 'Connected'; }
@@ -731,7 +741,7 @@
731
741
  // F-5: Event delegation for key-bar buttons (no inline onclick)
732
742
  const keyBar = document.getElementById('key-bar');
733
743
  if (keyBar) {
734
- const keyMap: Record<string, string> = {
744
+ var keyMap = {
735
745
  '\\x1b[A': '\x1b[A', '\\x1b[B': '\x1b[B', '\\x1b[C': '\x1b[C', '\\x1b[D': '\x1b[D',
736
746
  '\\t': '\t', '\\r': '\r', '\\x1b': '\x1b', '\\x03': '\x03', ' ': ' ', '\\x7f': '\x7f',
737
747
  };