tunli 0.0.25 → 0.0.27

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/README.md CHANGED
@@ -126,6 +126,8 @@ Tunli relies on the following packages:
126
126
 
127
127
  For development purposes, you can start the application using nodemon to automatically restart it on file changes:
128
128
 
129
+ _$ TUNLI_API_SERVER_URL=http://127.0.0.1:10000/api TUNLI_DASHBOARD=off TUNLI_SERVER=http://127.0.0.1:10000 TUNLI_PROXY_URL='http://127.0.0.1:10000/proxy/{{ uuid }}' node client.js register -f
130
+
129
131
  ```bash
130
132
  npm run dev
131
133
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tunli",
3
3
  "description": "Node.js application for creating HTTP tunnels to make local software projects accessible over the internet.",
4
- "version": "0.0.25",
4
+ "version": "0.0.27",
5
5
  "main": "bin/tunli",
6
6
  "bin": {
7
7
  "tunli": "bin/tunli"
@@ -24,14 +24,15 @@
24
24
  },
25
25
  "type": "module",
26
26
  "dependencies": {
27
- "axios": "^1.7.2",
27
+ "axios": "^1.11.0",
28
28
  "blessed": "^0.1.81",
29
- "chalk": "^5.3.0",
30
- "commander": "^12.1.0",
31
- "socket.io-client": "^4.7.5"
29
+ "chalk": "^5.5.0",
30
+ "commander": "^14.0.0",
31
+ "qrcode": "^1.5.4",
32
+ "socket.io-client": "^4.8.1"
32
33
  },
33
34
  "devDependencies": {
34
- "nodemon": "^3.1.3"
35
+ "nodemon": "^3.1.10"
35
36
  },
36
37
  "repository": {
37
38
  "type": "git",
@@ -6,6 +6,7 @@ import chalk from "chalk";
6
6
  import {getLatestVersion} from "#lib/Flow/getLatestVersion";
7
7
  import {exec} from 'child_process'
8
8
  import {checkGlobalInstallation, checkLocalInstallation} from "#src/utils/npmFunctions";
9
+ import QRCode from 'qrcode'
9
10
 
10
11
  export class Dashboard {
11
12
 
@@ -82,6 +83,13 @@ export class Dashboard {
82
83
  const lastBlockedIp = ref('')
83
84
  const availableUpdate = ref('')
84
85
 
86
+ screen.key('C-q', () => {
87
+ QRCode.toString(forwardingUrl.value, {type: 'terminal'}, (err, url) => {
88
+ if (err) throw err
89
+ screen.newFullScreenModal(url)
90
+ })
91
+ })
92
+
85
93
  getLatestVersion().then((version) => {
86
94
  if (version && version !== packageJson.version) {
87
95
  availableUpdate.value = chalk.yellow(`update available (version ${version}, Ctrl-U to update)`)
@@ -182,6 +190,7 @@ export class Dashboard {
182
190
  infoList.row(chalk.yellow('Update'), availableUpdate).if(() => availableUpdate)
183
191
  infoList.row('Profile', config.profile)
184
192
  infoList.row('Config', config.configPath)
193
+ infoList.row('QR-Code', 'Ctrl-Q')
185
194
 
186
195
  if (allowedCidr || deniedCidr) infoList.row('')
187
196
  if (allowedCidr) infoList.row('Allowed', allowedCidr)
@@ -0,0 +1,63 @@
1
+ import blessed from "blessed";
2
+
3
+ const centerContent = (text, boxWidth) => {
4
+ const lines = text.split('\n')
5
+ return lines.map(line => {
6
+ const pad = Math.floor((boxWidth - line.length) / 2)
7
+ return ' '.repeat(Math.max(pad, 0)) + line
8
+ }).join('\n')
9
+ }
10
+ export const newFullScreenModal = (screen, text) => {
11
+ // Backdrop zum Blocken von Inputs „unter“ dem Modal
12
+ const backdrop = blessed.box({
13
+ top: 0, left: 0, width: '100%', height: '100%',
14
+ style: {bg: 'black'},
15
+ mouse: true, clickable: true
16
+ })
17
+
18
+ // Größe anhand des Inhalts bestimmen (mit Padding & Border)
19
+ const lines = text.split('\n')
20
+ const contentWidth = Math.max(...lines.map(l => l.length))
21
+ const contentHeight = lines.length
22
+ const boxWidth = Math.min(contentWidth + 4, screen.width - 2)
23
+ //const centeredQr = centerContent(text, screen.width - 2) // -2 wegen Padding/Border
24
+
25
+ const qrBox = blessed.box({
26
+ top: 'center',
27
+ left: 'center',
28
+ width: boxWidth,
29
+ height: Math.min(contentHeight + 2, screen.height - 2),
30
+ padding: {left: 1, right: 1},
31
+ tags: false,
32
+ scrollable: true,
33
+ content: text,
34
+ border: 'line',
35
+ style: {
36
+ fg: 'white',
37
+ bg: 'black',
38
+ border: {fg: 'cyan'}
39
+ },
40
+ keys: true,
41
+ mouse: true,
42
+ alwaysScroll: true
43
+ })
44
+
45
+ // Einfügen und nach vorne bringen
46
+ screen.append(backdrop)
47
+ screen.append(qrBox)
48
+ qrBox.focus()
49
+ screen.render()
50
+
51
+ const close = () => {
52
+ backdrop.detach()
53
+ qrBox.detach()
54
+ screen.render()
55
+ }
56
+
57
+ // Schließen über Tastatur/Click
58
+ qrBox.key(['escape', 'q', 'C-q'], close)
59
+ backdrop.on('click', close)
60
+
61
+ // Bei Resize neu zentriert blessed automatisch; nur neu rendern
62
+ screen.on('resize', () => screen.render())
63
+ }
@@ -4,6 +4,7 @@ import {Row} from "#src/cli-app/elements/Row";
4
4
  import {ElementNode} from "#src/cli-app/elements/ElementNode";
5
5
  import {arrayRemoveEntry} from "#src/utils/arrayFunctions";
6
6
  import {Line} from "#src/cli-app/elements/Line";
7
+ import {newFullScreenModal} from "#src/cli-app/Screen/FullScreenModal";
7
8
 
8
9
  export class Screen extends EventEmitter {
9
10
 
@@ -52,6 +53,10 @@ export class Screen extends EventEmitter {
52
53
  return this.#screen.height
53
54
  }
54
55
 
56
+ newFullScreenModal(text) {
57
+ return newFullScreenModal(this, text)
58
+ }
59
+
55
60
  /**
56
61
  * @param {string|array} key
57
62
  * @param {keypressEventListener} callback
@@ -1,5 +1,6 @@
1
1
  import {securedHttpClient} from "#lib/HttpClient";
2
- import {SERVER_HOST} from "#lib/defs";
2
+ import {TUNLI_PROXY_URL} from "#lib/defs";
3
+ import {replaceTemplatePlaceholders} from "#src/utils/stringFunctions";
3
4
 
4
5
  export const requestNewProxyUrl = async (token) => {
5
6
 
@@ -10,7 +11,9 @@ export const requestNewProxyUrl = async (token) => {
10
11
  process.exit(1)
11
12
  }
12
13
 
13
- return `https://${data}.${SERVER_HOST}`
14
+ return replaceTemplatePlaceholders(TUNLI_PROXY_URL, {
15
+ uuid: data
16
+ })
14
17
  }
15
18
 
16
19
  export const renewProxyUrlRegistration = async (proxyUrl, token) => {
package/src/lib/defs.js CHANGED
@@ -2,9 +2,12 @@ import {resolve} from "path";
2
2
  import {homedir} from "os";
3
3
 
4
4
  export const SERVER_HOST = 'tunli.app'
5
+
6
+ export const TUNLI_PROXY_URL = process.env.TUNLI_PROXY_URL ?? 'https://{{ uuid }}.tunli.app'
7
+
5
8
  export const CONFIG_DIR_NAME = '.tunli'
6
9
 
7
- export const AUTH_SERVER_URL = 'https://api.tunli.app'
10
+ export const AUTH_SERVER_URL = process.env.TUNLI_API_SERVER_URL ?? 'https://api.tunli.app'
8
11
 
9
12
  export const GLOBAL_CONFIG_DIR = resolve(homedir(), CONFIG_DIR_NAME);
10
13
  export const CONFIG_FILENAME = 'default.json'
@@ -32,3 +32,28 @@ export const padEndIgnoreControlChars = (string, maxLength, fillString = ' ', au
32
32
  const padding = ''.padEnd(maxLength - stringWithoutCC.length, fillString)
33
33
  return `${string}${padding}`
34
34
  }
35
+
36
+ /**
37
+ * Replaces template placeholders in a string with corresponding values from a replacements object.
38
+ *
39
+ * @param {string} template - The string containing placeholders in the format {{ placeholder }}.
40
+ * @param {object} replacements - The object containing replacement values. Nested placeholders can be accessed using dot notation.
41
+ */
42
+ export const replaceTemplatePlaceholders = (template, replacements) => {
43
+
44
+ return template.replace(/{{\s*([^{}|]+)\s*(\|[^{}]+)*\s*}}/ig, (match, placeholder) => {
45
+
46
+ placeholder = placeholder.trim()
47
+ const keys = placeholder.split('.')
48
+
49
+ let value = replacements
50
+ for (const part of keys) {
51
+ if (!value) {
52
+ break
53
+ }
54
+ value = value [part]
55
+ }
56
+
57
+ return value ?? ''
58
+ })
59
+ }