tunli 0.0.24 → 0.0.26

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/bin/tunli CHANGED
@@ -5,7 +5,7 @@ import {exit} from 'node:process'
5
5
  import {resolve} from "path";
6
6
  import {dirnameFromMeta} from "#src/core/FS/utils";
7
7
 
8
- setCursorVisibility(false)
8
+ if (process.env.TUNLI_DASHBOARD !== 'off') setCursorVisibility(false)
9
9
  const exitCode = await proxyChildProcess(resolve(dirnameFromMeta(import.meta), '../client.js'))
10
- setCursorVisibility(true)
10
+ if (process.env.TUNLI_DASHBOARD !== 'off') setCursorVisibility(true)
11
11
  exit(exitCode)
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.24",
4
+ "version": "0.0.26",
5
5
  "main": "bin/tunli",
6
6
  "bin": {
7
7
  "tunli": "bin/tunli"
@@ -24,15 +24,14 @@
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
- "https-proxy-agent": "^7.0.4",
32
- "socket.io-client": "^4.7.5"
29
+ "chalk": "^5.5.0",
30
+ "commander": "^14.0.0",
31
+ "socket.io-client": "^4.8.1"
33
32
  },
34
33
  "devDependencies": {
35
- "nodemon": "^3.1.3"
34
+ "nodemon": "^3.1.10"
36
35
  },
37
36
  "repository": {
38
37
  "type": "git",
@@ -7,140 +7,205 @@ import {getLatestVersion} from "#lib/Flow/getLatestVersion";
7
7
  import {exec} from 'child_process'
8
8
  import {checkGlobalInstallation, checkLocalInstallation} from "#src/utils/npmFunctions";
9
9
 
10
+ export class Dashboard {
11
+
12
+ /**
13
+ * @type {TunnelClient}
14
+ */
15
+ #client
16
+ /**
17
+ * @type {tunnelClientOptions}
18
+ */
19
+ #options
20
+ /**
21
+ * @type {AppConfig}
22
+ */
23
+ #config
24
+ /**
25
+ * @type {Screen}
26
+ */
27
+ #screen
28
+ /**
29
+ * @type {Ref}
30
+ */
31
+ #forwardingUrl
32
+
33
+ /**
34
+ * @param {TunnelClient} client
35
+ * @param {tunnelClientOptions} options
36
+ * @param {AppConfig} config
37
+ */
38
+ constructor(client, options, config) {
39
+ this.#client = client
40
+ this.#options = options
41
+ this.#config = config
42
+ }
43
+
44
+ /**
45
+ * @param {string} value
46
+ */
47
+ set forwardingUrl(value) {
48
+ this.#forwardingUrl.value = value
49
+ }
50
+
51
+ /**
52
+ * @return {Screen}
53
+ */
54
+ get screen() {
55
+ return this.#screen
56
+ }
57
+
58
+ destroy() {
59
+ this.#screen.destroy()
60
+ }
61
+
62
+ init() {
63
+
64
+ const options = this.#options
65
+ const config = this.#config
66
+ const client = this.#client
67
+
68
+ const screen = createScreen()
69
+ this.#screen = screen
70
+ const packageJson = readJsonFile(searchFileInDirectoryTree('package.json', dirnameFromMeta(import.meta)))
71
+ const connectionStatus = ref(chalk.yellow('offline'))
72
+ const requestCount = ref(0)
73
+ const connectionDetails = ref('')
74
+ const forwardingUrl = ref(trimEnd(options.server, '/'))
75
+
76
+ this.#forwardingUrl = forwardingUrl
77
+
78
+ const allowedCidr = options.allowCidr.join(', ')
79
+ const deniedCidr = options.denyCidr.join(', ')
80
+
81
+ const blockedCount = ref(0)
82
+ const lastBlockedIp = ref('')
83
+ const availableUpdate = ref('')
84
+
85
+ getLatestVersion().then((version) => {
86
+ if (version && version !== packageJson.version) {
87
+ availableUpdate.value = chalk.yellow(`update available (version ${version}, Ctrl-U to update)`)
88
+
89
+ screen.onceKey('C-u', async (char, details) => {
90
+ availableUpdate.value = chalk.yellow('Updating...')
91
+ let modifier
92
+ if (await checkGlobalInstallation(packageJson.name)) {
93
+ modifier = ' -g'
94
+ } else if (!await checkLocalInstallation(packageJson.name)) {
95
+ availableUpdate.value = chalk.red('Update failed.')
96
+ return
97
+ }
98
+
99
+ const npmUpdateCommand = `npm${modifier} update ${packageJson.name} --registry https://registry.npmjs.org`
100
+
101
+ exec(npmUpdateCommand, (error, stdout, stderr) => {
102
+ if (error || stderr) {
103
+ availableUpdate.value = chalk.red('Update failed. Reason 2.')
104
+ return;
105
+ }
106
+ availableUpdate.value = chalk.green('Update done. Please restart tunli. (Ctrl+R to restart)')
107
+ })
108
+ })
109
+ }
110
+ })
111
+
112
+ screen.row(packageJson.name)
113
+ screen.row('(Ctrl+C to quit)', {
114
+ top: -1, right: 0
115
+ })
116
+
117
+ screen.row("")
118
+
119
+ const infoList = screen.list({minWidth: 30})
120
+
121
+ screen.row('HTTP Requests')
122
+ screen.line()
123
+
124
+ const accessLog = screen.list({
125
+ length: 30, reverse: true, minWidth: [undefined, 30], maxWidth: [undefined, screen.width - 35]
126
+ })
127
+
128
+ client.on('tunnel-connection-established', () => {
129
+ connectionStatus.value = chalk.bold(chalk.green('online'))
130
+ connectionDetails.value = ''
131
+ screen.render()
132
+ })
133
+
134
+ client.on('blocked', ip => {
135
+ blockedCount.value++
136
+ lastBlockedIp.value = ` (${ip})`
137
+ })
138
+ client.on('tunnel-connection-closed', () => {
139
+ connectionStatus.value = chalk.bold(chalk.red('offline'))
140
+ screen.render()
141
+ })
142
+
143
+ client.on('tunnel-connection-error', (error) => {
144
+ if (error.stopPropagation) {
145
+ return
146
+ }
147
+ connectionDetails.value = chalk.bold(chalk.red(` - ${error.message}`))
148
+ screen.render()
149
+ })
150
+
151
+ client.on('response', (res, req) => {
152
+
153
+ const code = res.statusCode
154
+ let rspMsg = `${res.statusCode} ${res.statusMessage}`
155
+ if (code >= 500) {
156
+ rspMsg = chalk.red(rspMsg)
157
+ } else if (code >= 400) {
158
+ rspMsg = chalk.blueBright(rspMsg)
159
+ } else {
160
+ rspMsg = chalk.green(rspMsg)
161
+ }
162
+
163
+ rspMsg = chalk.bold(rspMsg)
164
+
165
+ requestCount.value++
166
+ accessLog.row(req.method, req.path, rspMsg)
167
+ screen.render()
168
+ })
169
+
170
+ screen.key('C-c', (char, details) => {
171
+ process.exit(0);
172
+ })
173
+
174
+ screen.key('C-r', (char, details) => {
175
+ process.send('restart')
176
+ })
177
+
178
+ const target = new URL(`${options.protocol}://${options.host}:${options.port}`)
179
+
180
+ infoList.row('Tunnel', concat(connectionStatus, connectionDetails))
181
+ infoList.row('Version', packageJson.version)
182
+ infoList.row(chalk.yellow('Update'), availableUpdate).if(() => availableUpdate)
183
+ infoList.row('Profile', config.profile)
184
+ infoList.row('Config', config.configPath)
185
+
186
+ if (allowedCidr || deniedCidr) infoList.row('')
187
+ if (allowedCidr) infoList.row('Allowed', allowedCidr)
188
+ if (deniedCidr) infoList.row('Denied', deniedCidr)
189
+ if (allowedCidr || deniedCidr) infoList.row('Blocked', concat(blockedCount, lastBlockedIp))
190
+
191
+ infoList.row('')
192
+ infoList.row('Latency', concat(client.latency, 'ms'))
193
+ infoList.row('Forwarding', concat(forwardingUrl, ` -> ${trimEnd(target.toString(), '/')}`))
194
+ infoList.row('Connections', requestCount)
195
+
196
+ screen.render();
197
+
198
+ return this
199
+ }
200
+ }
201
+
10
202
  /**
11
- *
12
203
  * @param {TunnelClient} client
13
204
  * @param {tunnelClientOptions} options
14
205
  * @param {AppConfig} config
15
206
  */
16
207
  export const initDashboard = (client, options, config) => {
17
-
18
- const screen = createScreen()
19
- const packageJson = readJsonFile(searchFileInDirectoryTree('package.json', dirnameFromMeta(import.meta)))
20
- const connectionStatus = ref(chalk.yellow('offline'))
21
- const requestCount = ref(0)
22
- const connectionDetails = ref('')
23
-
24
- const allowedCidr = options.allowCidr.join(', ')
25
- const deniedCidr = options.denyCidr.join(', ')
26
-
27
- const blockedCount = ref(0)
28
- const lastBlockedIp = ref('')
29
- const availableUpdate = ref('')
30
-
31
- getLatestVersion().then((version) => {
32
- if (version && version !== packageJson.version) {
33
- availableUpdate.value = chalk.yellow(`update available (version ${version}, Ctrl-U to update)`)
34
-
35
- screen.onceKey('C-u', async (char, details) => {
36
- availableUpdate.value = chalk.yellow('Updating...')
37
- let modifier
38
- if (await checkGlobalInstallation(packageJson.name)) {
39
- modifier = ' -g'
40
- } else if (!await checkLocalInstallation(packageJson.name)) {
41
- availableUpdate.value = chalk.red('Update failed.')
42
- return
43
- }
44
-
45
- const npmUpdateCommand = `npm${modifier} update ${packageJson.name} --registry https://registry.npmjs.org`
46
-
47
- exec(npmUpdateCommand, (error, stdout, stderr) => {
48
- if (error || stderr) {
49
- availableUpdate.value = chalk.red('Update failed. Reason 2.')
50
- return;
51
- }
52
- availableUpdate.value = chalk.green('Update done. Please restart tunli. (Ctrl+R to restart)')
53
- })
54
- })
55
- }
56
- })
57
-
58
- screen.row(packageJson.name)
59
- screen.row('(Ctrl+C to quit)', {
60
- top: -1,
61
- right: 0
62
- })
63
-
64
- screen.row("")
65
-
66
- const infoList = screen.list({minWidth: 30})
67
-
68
- screen.row('HTTP Requests')
69
- screen.line()
70
-
71
- const accessLog = screen.list({
72
- length: 30,
73
- reverse: true,
74
- minWidth: [undefined, 30],
75
- maxWidth: [undefined, screen.width - 35]
76
- })
77
-
78
- client.on('tunnel-connection-established', () => {
79
- connectionStatus.value = chalk.bold(chalk.green('online'))
80
- connectionDetails.value = ''
81
- screen.render()
82
- })
83
-
84
- client.on('blocked', ip => {
85
- blockedCount.value++
86
- lastBlockedIp.value = ` (${ip})`
87
- })
88
- client.on('tunnel-connection-closed', () => {
89
- connectionStatus.value = chalk.bold(chalk.red('offline'))
90
- screen.render()
91
- })
92
-
93
- client.on('tunnel-connection-error', (error) => {
94
- connectionDetails.value = chalk.bold(chalk.red(` - ${error.message}`))
95
- screen.render()
96
- })
97
-
98
- client.on('response', (res, req) => {
99
-
100
- const code = res.statusCode
101
- let rspMsg = `${res.statusCode} ${res.statusMessage}`
102
- if (code >= 500) {
103
- rspMsg = chalk.red(rspMsg)
104
- } else if (code >= 400) {
105
- rspMsg = chalk.blueBright(rspMsg)
106
- } else {
107
- rspMsg = chalk.green(rspMsg)
108
- }
109
-
110
- rspMsg = chalk.bold(rspMsg)
111
-
112
- requestCount.value++
113
- accessLog.row(req.method, req.path, rspMsg)
114
- screen.render()
115
- })
116
-
117
- screen.key('C-c', (char, details) => {
118
- process.exit(0);
119
- })
120
-
121
- screen.key('C-r', (char, details) => {
122
- process.send('restart')
123
- })
124
-
125
- const target = new URL(`${options.protocol}://${options.host}:${options.port}`)
126
-
127
- infoList.row('Tunnel', concat(connectionStatus, connectionDetails))
128
- infoList.row('Version', packageJson.version)
129
- infoList.row(chalk.yellow('Update'), availableUpdate).if(() => availableUpdate)
130
- infoList.row('Profile', config.profile)
131
- infoList.row('Config', config.configPath)
132
-
133
- if (allowedCidr || deniedCidr) infoList.row('')
134
- if (allowedCidr) infoList.row('Allowed', allowedCidr)
135
- if (deniedCidr) infoList.row('Denied', deniedCidr)
136
- if (allowedCidr || deniedCidr) infoList.row('Blocked', concat(blockedCount, lastBlockedIp))
137
-
138
- infoList.row('')
139
- infoList.row('Latency', concat(client.latency, 'ms'))
140
- infoList.row('Forwarding', `${trimEnd(options.server, '/')} -> ${trimEnd(target.toString(), '/')}`)
141
- infoList.row('Connections', requestCount)
142
-
143
- screen.render();
144
-
145
- return screen
208
+ const dashboard = new Dashboard(client, options, config)
209
+ dashboard.init()
210
+ return dashboard
146
211
  }
@@ -10,7 +10,48 @@ import {renewProxyUrlRegistration, requestNewProxyUrl} from "#lib/Flow/proxyUrl"
10
10
  import {getCurrentIp} from "#lib/Flow/getCurrentIp";
11
11
  import {arrayUnique} from "#src/utils/arrayFunctions";
12
12
  import {initDashboard} from "#src/cli-app/Dashboard";
13
- import {proxy} from "#lib/Proxy";
13
+ import {md5} from "#src/utils/hashFunctions";
14
+
15
+
16
+ /**
17
+ *
18
+ * @param {AppConfig} config
19
+ * @param {tunnelClientOptions} options
20
+ */
21
+ const computeProxyURL = async (config, options) => {
22
+
23
+ // console.log(new URL(`${options.protocol}://${options.host ?? config.host}:${options.port ?? config.port}`).toString())
24
+
25
+ const targetUrlHash = md5(new URL(`${options.protocol}://${options.host ?? config.host}:${options.port ?? config.port}`))
26
+
27
+ if (config.profile === 'default') {
28
+ let proxyUrl = config.proxyURLs[targetUrlHash]
29
+
30
+ if (proxyUrl) {
31
+ proxyUrl = await renewProxyUrlRegistration(proxyUrl, config.authToken)
32
+ }
33
+
34
+ if (!proxyUrl) {
35
+ proxyUrl = await requestNewProxyUrl(config.authToken)
36
+ config.proxyURLs[targetUrlHash] = proxyUrl
37
+ config.update({proxyURLs: config.proxyURLs})
38
+ }
39
+
40
+ config.proxyURL = proxyUrl
41
+
42
+ console.log(config.proxyURL)
43
+ return
44
+ }
45
+
46
+ if (config.proxyURL) {
47
+ config.proxyURL = await renewProxyUrlRegistration(config.proxyURL, config.authToken)
48
+ }
49
+
50
+ if (!config.proxyURL) {
51
+ config.proxyURL = await requestNewProxyUrl(config.authToken)
52
+ config.update({proxyURL: config.proxyURL})
53
+ }
54
+ }
14
55
 
15
56
  /**
16
57
  * @callback httpCommandExec
@@ -48,15 +89,6 @@ const exec = (configRef, cmd, program) => {
48
89
  process.exit()
49
90
  }
50
91
 
51
- if (config.proxyURL) {
52
- config.proxyURL = await renewProxyUrlRegistration(config.proxyURL, config.authToken)
53
- }
54
-
55
- if (!config.proxyURL) {
56
- config.proxyURL = await requestNewProxyUrl(config.authToken)
57
- options.save ??= true
58
- }
59
-
60
92
  if (isSharedArg(port)) {
61
93
  protocol ??= port.value.url?.protocol
62
94
  host ??= port.value.host ?? port.value.url?.host
@@ -69,6 +101,8 @@ const exec = (configRef, cmd, program) => {
69
101
  options.host ??= host
70
102
  options.protocol ??= protocol ?? 'http'
71
103
 
104
+ await computeProxyURL(config, options)
105
+
72
106
  const save = options.save
73
107
  delete options.save
74
108
 
@@ -100,9 +134,23 @@ const exec = (configRef, cmd, program) => {
100
134
  protocol: options.protocol
101
135
  }
102
136
 
103
- const useDashboard = process.env.DASHBOARD !== 'off'
137
+ const useDashboard = process.env.TUNLI_DASHBOARD !== 'off'
104
138
 
105
139
  const client = new TunnelClient(clientOptions)
140
+
141
+ client.once('tunnel-connection-error', async (error) => {
142
+ error.stopPropagation = true
143
+ if (error.data?.connection_exists) {
144
+ clientOptions.server = await requestNewProxyUrl(config.authToken)
145
+
146
+ if (useDashboard) {
147
+ dashboard.forwardingUrl = clientOptions.server
148
+ }
149
+
150
+ await client.init()
151
+ }
152
+ })
153
+
106
154
  const dashboard = useDashboard ? initDashboard(client, clientOptions, config) : null
107
155
  await client.init(dashboard)
108
156
 
@@ -1,4 +1,4 @@
1
- import {existsSync, writeFileSync} from "fs"
1
+ import {existsSync, readFileSync, writeFileSync} from "fs"
2
2
  import {dirname} from 'path'
3
3
  import {ensureDirectoryExists} from "#src/core/FS/utils";
4
4
  import {PropertyConfig} from "#src/config/PropertyConfig";
@@ -268,6 +268,30 @@ export class ConfigAbstract {
268
268
  return this
269
269
  }
270
270
 
271
+ /**
272
+ * @param {{[p:string]: any}} value
273
+ * @return {ConfigAbstract}
274
+ */
275
+ update(value) {
276
+ ensureDirectoryExists(dirname(this.#path))
277
+
278
+ const isSystem = this.#data === this.#profileData.system
279
+ const profileData = JSON.parse(readFileSync(this.#path, 'utf-8'))
280
+ profileData.system ??= {}
281
+ profileData.profile ??= {}
282
+ profileData.profile[this.profile] ??= {}
283
+
284
+ const updateData = isSystem ? profileData.system : profileData.profile[this.profile]
285
+
286
+ for (const [k, v] of Object.entries(value)) {
287
+ updateData[k] = v
288
+ this[k] = v
289
+ }
290
+
291
+ writeFileSync(this.#path, JSON.stringify(profileData, null, 2) + "\n")
292
+ return this
293
+ }
294
+
271
295
  /**
272
296
  * Use the system configuration.
273
297
  * @returns {ConfigAbstract} - The instance of the configuration.
@@ -1,4 +1,4 @@
1
- import {checkHost, checkInArray, checkIpV4Cidr, checkPort, checkUrl} from "#src/utils/checkFunctions";
1
+ import {checkHost, checkInArray, checkIpV4Cidr, checkMd5, checkPort, checkUrl} from "#src/utils/checkFunctions";
2
2
  import {ConfigAbstract, VISIBILITY_PUBLIC} from "#src/config/ConfigAbstract";
3
3
  import {property} from "#src/config/PropertyConfig";
4
4
  import {ConfigManager} from "#src/config/ConfigManager";
@@ -20,6 +20,19 @@ export class GlobalLocalShardConfigAbstract extends ConfigAbstract {
20
20
  return val.map(checkIpV4Cidr)
21
21
  }
22
22
  }),
23
+ proxyURLs: property({
24
+ defaultValue: {},
25
+ visibility: VISIBILITY_PUBLIC,
26
+ writeable: true,
27
+ type: Array,
28
+ validate(val) {
29
+ const final = {}
30
+ for (const [hash, proxyURL] of Object.entries(val)) {
31
+ final[checkMd5(hash)] = checkUrl(proxyURL)
32
+ }
33
+ return final
34
+ }
35
+ }),
23
36
  proxyURL: property({
24
37
  visibility: VISIBILITY_PUBLIC,
25
38
  writeable: true,
@@ -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'
@@ -95,7 +95,7 @@ export class TunnelClient extends EventEmitter {
95
95
  }
96
96
 
97
97
  /**
98
- * @param [dashboard]
98
+ * @param {Dashboard} [dashboard]
99
99
  * @return {Promise<TunnelClient>}
100
100
  */
101
101
  async init(dashboard) {
@@ -107,6 +107,10 @@ export class TunnelClient extends EventEmitter {
107
107
  return this
108
108
  }
109
109
 
110
+ /**
111
+ * @param {Dashboard} dashboard
112
+ * @return {Promise<{path: any, transports: string[], auth: {token: string}, extraHeaders: {}}>}
113
+ */
110
114
  async #createParameters(dashboard) {
111
115
  const options = this.#options
112
116
  const webSocketCapturePath = await securedHttpClient(options.authToken).get('/capture_path')
@@ -170,4 +170,13 @@ export const checkInArray = (val, arr) => {
170
170
  }
171
171
 
172
172
  return val
173
+ }
174
+
175
+ export const checkMd5 = (hash) => {
176
+
177
+ if (!hash?.match(/^[0-9a-f]{32}$/i)) {
178
+ throw new InvalidArgumentError(`Value is not a valid md5 hash`)
179
+ }
180
+
181
+ return hash.toLowerCase()
173
182
  }
@@ -0,0 +1,9 @@
1
+ import {createHash} from 'crypto'
2
+
3
+ /**
4
+ * @param {string|Object<toString>} value
5
+ * @return {string}
6
+ */
7
+ export const md5 = (value) => {
8
+ return createHash('md5').update(value.toString()).digest('hex')
9
+ }
@@ -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
+ }
package/types/index.d.ts CHANGED
@@ -32,8 +32,8 @@ export type cliListOption = {
32
32
  minLength?: number
33
33
  length?: number
34
34
  reverse?: boolean
35
- minWidth?: number | Array<number|string|boolean>
36
- maxWidth?: number | Array<number|string|boolean>
35
+ minWidth?: number | Array<number | string | boolean>
36
+ maxWidth?: number | Array<number | string | boolean>
37
37
  }
38
38
 
39
39
  export type profileDump = {}
@@ -53,6 +53,7 @@ export interface AppConfig {
53
53
  host: string
54
54
  authToken: string
55
55
  proxyURL: proxyURL
56
+ proxyURLs: proxyURL[]
56
57
  path: undefined
57
58
  origin: string
58
59
  denyCidr?: ipCidr[]
@@ -70,6 +71,8 @@ export interface AppConfig {
70
71
 
71
72
  save(): this
72
73
 
74
+ update(value: { [p: string]: any }): this
75
+
73
76
  useSystem(): AppConfig
74
77
 
75
78
  use(profile: profileAlias): AppConfig
@@ -113,6 +116,4 @@ export interface keypressEventListener {
113
116
  }
114
117
 
115
118
 
116
- export type tunliProxyOptions = {
117
-
118
- }
119
+ export type tunliProxyOptions = {}