pinokiod 8.2.25 → 8.2.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/kernel/anywhere/index.js
CHANGED
|
@@ -671,9 +671,21 @@ class Anywhere {
|
|
|
671
671
|
await this.loadConfig()
|
|
672
672
|
const installed = await this.installed()
|
|
673
673
|
const enabled = !!this.config.enabled
|
|
674
|
+
const info = this.kernel.sysinfo || {}
|
|
674
675
|
const base = {
|
|
675
676
|
installed,
|
|
676
677
|
enabled,
|
|
678
|
+
// Display metadata only; the existing Tailscale identity/address stays intact.
|
|
679
|
+
machineName: os.hostname(),
|
|
680
|
+
system: {
|
|
681
|
+
platform: this.platform,
|
|
682
|
+
arch: this.kernel.arch || os.arch(),
|
|
683
|
+
os: info.osInfo?.distro,
|
|
684
|
+
cpu: info.cpu?.brand,
|
|
685
|
+
gpu: info.gpu_model,
|
|
686
|
+
ram: info.ram,
|
|
687
|
+
vram: info.vram,
|
|
688
|
+
},
|
|
677
689
|
error: this.error,
|
|
678
690
|
serveError: this.lastServeError,
|
|
679
691
|
}
|
package/package.json
CHANGED
|
@@ -49,6 +49,14 @@ body.dark .anywhere-card { border-color: rgba(255,255,255,0.1); background: rgba
|
|
|
49
49
|
}
|
|
50
50
|
body.dark .anywhere-address { background: rgba(255,255,255,0.08); }
|
|
51
51
|
.anywhere-address a { color: inherit; text-decoration: none; }
|
|
52
|
+
.anywhere-copy { min-height: 44px; }
|
|
53
|
+
.anywhere-copy:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
|
|
54
|
+
.anywhere-copy-manual { width: 100%; min-width: 0; }
|
|
55
|
+
.anywhere-copy-manual input {
|
|
56
|
+
box-sizing: border-box; width: 100%; min-height: 44px; padding: 8px;
|
|
57
|
+
font-size: 16px; color: inherit; background: transparent; border: 1px solid currentColor;
|
|
58
|
+
border-radius: 8px; user-select: text; -webkit-user-select: text;
|
|
59
|
+
}
|
|
52
60
|
.anywhere-btn {
|
|
53
61
|
display: inline-flex; align-items: center; gap: 8px;
|
|
54
62
|
padding: 10px 16px; border-radius: 8px; border: none; cursor: pointer;
|
|
@@ -201,6 +209,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
201
209
|
<div class='anywhere-qr'>
|
|
202
210
|
<img id='anywhere-qr' alt='QR code to connect' src=''>
|
|
203
211
|
<div class='anywhere-address'><a id='anywhere-address' href='#' target='_blank' rel='noopener'></a></div>
|
|
212
|
+
<button type='button' class='anywhere-btn secondary anywhere-copy' id='anywhere-copy' disabled><i class='fa-regular fa-copy' aria-hidden='true'></i> Copy URL</button>
|
|
213
|
+
<div id='anywhere-copy-status' class='anywhere-muted' role='status' aria-live='polite'></div>
|
|
214
|
+
<div id='anywhere-copy-manual' class='anywhere-copy-manual' hidden>
|
|
215
|
+
<input id='anywhere-copy-value' type='text' readonly aria-label='Remote URL' aria-describedby='anywhere-copy-status'>
|
|
216
|
+
</div>
|
|
204
217
|
<div class='anywhere-muted'>signed in as <b id='anywhere-account'></b></div>
|
|
205
218
|
<div class='anywhere-waiting' id='anywhere-waiting'><i class="fa-solid fa-circle-notch fa-spin"></i> Waiting for another device to join…</div>
|
|
206
219
|
</div>
|
|
@@ -254,8 +267,54 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
254
267
|
const switchEl = document.getElementById('anywhere-switch')
|
|
255
268
|
const switchLabel = document.getElementById('anywhere-switch-label')
|
|
256
269
|
const agent = document.body.getAttribute('data-agent')
|
|
270
|
+
const copyButton = document.getElementById('anywhere-copy')
|
|
271
|
+
const copyStatus = document.getElementById('anywhere-copy-status')
|
|
272
|
+
const copyManual = document.getElementById('anywhere-copy-manual')
|
|
273
|
+
const copyValue = document.getElementById('anywhere-copy-value')
|
|
257
274
|
let current = <%- JSON.stringify(status || {}).replace(/</g, '\\u003c') %>
|
|
258
275
|
let busy = false
|
|
276
|
+
let copying = false
|
|
277
|
+
|
|
278
|
+
copyButton.addEventListener('click', async () => {
|
|
279
|
+
const url = current.address
|
|
280
|
+
if (copying || current.phase !== 'ready' || !url) return
|
|
281
|
+
const previousFocus = document.activeElement
|
|
282
|
+
copying = true
|
|
283
|
+
copyButton.disabled = true
|
|
284
|
+
copyStatus.textContent = 'Copying…'
|
|
285
|
+
const stillCurrent = () => current.phase === 'ready' && current.address === url
|
|
286
|
+
let copied = false
|
|
287
|
+
try {
|
|
288
|
+
// Call directly from the click: Safari requires a user gesture.
|
|
289
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
290
|
+
await navigator.clipboard.writeText(url)
|
|
291
|
+
copied = true
|
|
292
|
+
}
|
|
293
|
+
} catch (_) { /* Try selection-based copying when clipboard access is denied. */ }
|
|
294
|
+
if (stillCurrent()) {
|
|
295
|
+
if (!copied) {
|
|
296
|
+
copyValue.value = url
|
|
297
|
+
copyManual.hidden = false
|
|
298
|
+
copyStatus.textContent = 'Automatic copy is unavailable. Touch and hold the URL to copy it, or press Ctrl+C / ⌘C.'
|
|
299
|
+
try {
|
|
300
|
+
copyValue.focus({ preventScroll: true })
|
|
301
|
+
copyValue.select()
|
|
302
|
+
copyValue.setSelectionRange(0, url.length)
|
|
303
|
+
copied = document.execCommand('copy') === true
|
|
304
|
+
} catch (_) { /* Leave the visible URL available for manual copying. */ }
|
|
305
|
+
}
|
|
306
|
+
if (copied) {
|
|
307
|
+
copyManual.hidden = true
|
|
308
|
+
copyStatus.textContent = 'URL copied.'
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
copying = false
|
|
312
|
+
copyButton.disabled = current.phase !== 'ready' || !current.address
|
|
313
|
+
if (copied && stillCurrent() && previousFocus &&
|
|
314
|
+
(document.activeElement === copyValue || document.activeElement === document.body)) {
|
|
315
|
+
previousFocus.focus({ preventScroll: true })
|
|
316
|
+
}
|
|
317
|
+
})
|
|
259
318
|
|
|
260
319
|
const esc = (value) => String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
261
320
|
const openExternal = (href) => {
|
|
@@ -292,7 +351,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
292
351
|
}
|
|
293
352
|
|
|
294
353
|
const render = (status) => {
|
|
354
|
+
if (current.address !== (status || {}).address || current.phase !== (status || {}).phase) {
|
|
355
|
+
copyManual.hidden = true
|
|
356
|
+
copyStatus.textContent = ''
|
|
357
|
+
}
|
|
295
358
|
current = status || {}
|
|
359
|
+
copyButton.disabled = copying || current.phase !== 'ready' || !current.address
|
|
296
360
|
const phase = current.phase && current.phase !== 'setup' ? current.phase : 'starting'
|
|
297
361
|
const on = !!current.enabled && phase !== 'off' && phase !== 'setup'
|
|
298
362
|
switchEl.dataset.on = on ? 'true' : 'false'
|
|
@@ -13,7 +13,7 @@ const readyAnywhere = () => {
|
|
|
13
13
|
anywhere.config = { enabled: true }
|
|
14
14
|
anywhere.lastStatus = { Self: { DNSName: 'pinokio.tail1.ts.net.' } }
|
|
15
15
|
anywhere.lastServe = buildServeConfig({ dnsName: 'pinokio.tail1.ts.net', targets: [{ listen: 43000, target: 7860 }] })
|
|
16
|
-
anywhere.snapshot = async () => ({ phase: 'ready', dnsName: 'pinokio.tail1.ts.net', serve: describeServeConfig(anywhere.lastServe) })
|
|
16
|
+
anywhere.snapshot = async () => ({ phase: 'ready', machineName: 'OS-HOST', system: { platform: 'linux', arch: 'x64', cpu: 'Test CPU', gpu: 'Test GPU', ram: 64, vram: 24 }, dnsName: 'pinokio.tail1.ts.net', serve: describeServeConfig(anywhere.lastServe) })
|
|
17
17
|
return anywhere
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -36,10 +36,14 @@ test('status carries app ids and complete remote addresses, and no installed cou
|
|
|
36
36
|
]) {
|
|
37
37
|
readyUrl = url
|
|
38
38
|
const status = await Server.prototype.composeAnywhereStatus.call(fake, {})
|
|
39
|
+
assert.equal(status.machineName, 'OS-HOST')
|
|
40
|
+
assert.equal(status.system.gpu, 'Test GPU')
|
|
39
41
|
assert.equal(status.installed, undefined)
|
|
40
42
|
assert.deepEqual(status.apps.map((a) => [a.id, a.state, a.remote]), [['comfy', 'ready', expected], ['forge', 'starting', null], ['unrouted', 'publishing', null]])
|
|
41
43
|
}
|
|
42
44
|
const light = await Server.prototype.composeAnywhereStatus.call(fake, { light: true })
|
|
45
|
+
assert.equal(light.machineName, 'OS-HOST')
|
|
46
|
+
assert.equal(light.system.ram, 64)
|
|
43
47
|
assert.equal(light.apps, undefined, 'the light status is unchanged')
|
|
44
48
|
})
|
|
45
49
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
const ejs = require('ejs')
|
|
6
|
+
const { JSDOM, VirtualConsole } = require('jsdom')
|
|
7
|
+
|
|
8
|
+
const filename = path.resolve(__dirname, '../server/views/anywhere.ejs')
|
|
9
|
+
const template = fs.readFileSync(filename, 'utf8')
|
|
10
|
+
const address = 'https://pinokio-1.tail123.ts.net'
|
|
11
|
+
const tick = () => new Promise((resolve) => setImmediate(resolve))
|
|
12
|
+
|
|
13
|
+
function createHarness(t, options = {}) {
|
|
14
|
+
const state = { status: { phase: 'ready', enabled: true, address }, timers: [], opened: [] }
|
|
15
|
+
Object.assign(state.status, options.status)
|
|
16
|
+
const html = ejs.render(template, {
|
|
17
|
+
theme: 'light', agent: options.agent || 'browser', status: state.status
|
|
18
|
+
}, { filename, includer: () => ({ template: ' ' }) })
|
|
19
|
+
const virtualConsole = new VirtualConsole()
|
|
20
|
+
virtualConsole.on('jsdomError', (error) => { throw error })
|
|
21
|
+
const dom = new JSDOM(html, {
|
|
22
|
+
url: options.origin || 'http://127.0.0.1:42000/',
|
|
23
|
+
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole,
|
|
24
|
+
beforeParse(window) {
|
|
25
|
+
if (options.clipboard) {
|
|
26
|
+
Object.defineProperty(window.navigator, 'clipboard', { value: options.clipboard })
|
|
27
|
+
}
|
|
28
|
+
window.document.execCommand = options.execCommand
|
|
29
|
+
window.setTimeout = (callback, delay) => state.timers.push({ callback, delay })
|
|
30
|
+
window.fetch = async (url) => {
|
|
31
|
+
assert.equal(url, '/anywhere/status', 'copying must not use the server clipboard')
|
|
32
|
+
return { ok: true, json: async () => ({ ...state.status }) }
|
|
33
|
+
}
|
|
34
|
+
window.open = (...args) => state.opened.push(args)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
t.after(() => dom.window.close())
|
|
38
|
+
const document = dom.window.document
|
|
39
|
+
return {
|
|
40
|
+
state, document,
|
|
41
|
+
button: document.getElementById('anywhere-copy'),
|
|
42
|
+
feedback: document.getElementById('anywhere-copy-status'),
|
|
43
|
+
manual: document.getElementById('anywhere-copy-manual'),
|
|
44
|
+
input: document.getElementById('anywhere-copy-value'),
|
|
45
|
+
poll: () => state.timers.shift().callback()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const agent of ['browser', 'electron']) {
|
|
50
|
+
test(`Remote copies the full URL directly from the ${agent} click without opening a window`, async (t) => {
|
|
51
|
+
const copied = []
|
|
52
|
+
const h = createHarness(t, {
|
|
53
|
+
agent,
|
|
54
|
+
clipboard: { writeText: async (text) => { copied.push(text) } },
|
|
55
|
+
execCommand: () => assert.fail('modern clipboard succeeded')
|
|
56
|
+
})
|
|
57
|
+
h.button.focus()
|
|
58
|
+
h.button.click()
|
|
59
|
+
assert.deepEqual(copied, [address], 'clipboard is invoked synchronously in the click')
|
|
60
|
+
assert.equal(h.button.disabled, true)
|
|
61
|
+
h.button.click()
|
|
62
|
+
await tick()
|
|
63
|
+
assert.deepEqual(copied, [address], 'pending copies are not duplicated')
|
|
64
|
+
assert.equal(h.feedback.textContent, 'URL copied.')
|
|
65
|
+
assert.equal(h.manual.hidden, true)
|
|
66
|
+
assert.equal(h.button.disabled, false)
|
|
67
|
+
assert.equal(h.document.activeElement, h.button)
|
|
68
|
+
assert.deepEqual(h.state.opened, [])
|
|
69
|
+
assert.equal(h.document.getElementById('anywhere-address').getAttribute('href'), address)
|
|
70
|
+
assert.equal(h.feedback.getAttribute('role'), 'status')
|
|
71
|
+
if (agent === 'electron') {
|
|
72
|
+
h.document.getElementById('anywhere-address').click()
|
|
73
|
+
assert.deepEqual(h.state.opened, [[address + '/', '_blank', 'browser']])
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const scenario of ['insecure LAN', 'denied', 'synchronous failure', 'electron']) {
|
|
79
|
+
test(`Remote falls back to selected text copying: ${scenario}`, async (t) => {
|
|
80
|
+
const copied = []
|
|
81
|
+
let h
|
|
82
|
+
h = createHarness(t, {
|
|
83
|
+
agent: scenario === 'electron' ? 'electron' : 'browser',
|
|
84
|
+
origin: scenario === 'insecure LAN' ? 'http://192.168.1.10:42000/' : 'https://pinokio.tail123.ts.net/',
|
|
85
|
+
clipboard: scenario === 'denied'
|
|
86
|
+
? { writeText: async () => { throw new Error('NotAllowedError') } }
|
|
87
|
+
: scenario === 'synchronous failure'
|
|
88
|
+
? { writeText: () => { throw new Error('clipboard unavailable') } }
|
|
89
|
+
: undefined,
|
|
90
|
+
execCommand(command) {
|
|
91
|
+
assert.equal(command, 'copy')
|
|
92
|
+
assert.equal(h.document.activeElement, h.input)
|
|
93
|
+
assert.equal(h.input.readOnly, true)
|
|
94
|
+
assert.equal(h.manual.hidden, false)
|
|
95
|
+
copied.push(h.input.value.slice(h.input.selectionStart, h.input.selectionEnd))
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
h.button.focus()
|
|
100
|
+
h.button.click()
|
|
101
|
+
await tick()
|
|
102
|
+
assert.deepEqual(copied, [address])
|
|
103
|
+
assert.equal(h.feedback.textContent, 'URL copied.')
|
|
104
|
+
assert.equal(h.manual.hidden, true)
|
|
105
|
+
assert.equal(h.document.activeElement, h.button)
|
|
106
|
+
assert.deepEqual(h.state.opened, [])
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const failure of ['false', 'undefined', 'throws', 'unavailable']) {
|
|
111
|
+
test(`Remote offers selected manual copy without false success when execCommand ${failure}`, async (t) => {
|
|
112
|
+
const h = createHarness(t, {
|
|
113
|
+
clipboard: { writeText: async () => { throw new Error('denied') } },
|
|
114
|
+
execCommand: failure === 'unavailable' ? undefined : () => {
|
|
115
|
+
if (failure === 'throws') throw new Error('denied')
|
|
116
|
+
return failure === 'false' ? false : undefined
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
h.button.click()
|
|
120
|
+
await tick()
|
|
121
|
+
assert.equal(h.manual.hidden, false)
|
|
122
|
+
assert.equal(h.input.value, address)
|
|
123
|
+
assert.equal(h.input.selectionStart, 0)
|
|
124
|
+
assert.equal(h.input.selectionEnd, address.length)
|
|
125
|
+
assert.equal(h.document.activeElement, h.input)
|
|
126
|
+
assert.match(h.feedback.textContent, /Touch and hold.*Ctrl\+C \/ ⌘C/)
|
|
127
|
+
assert.doesNotMatch(h.feedback.textContent, /URL copied/)
|
|
128
|
+
assert.equal(h.button.disabled, false)
|
|
129
|
+
await h.poll()
|
|
130
|
+
assert.equal(h.manual.hidden, false, 'routine polling preserves the manual copy field')
|
|
131
|
+
assert.equal(h.input.selectionEnd, address.length)
|
|
132
|
+
h.state.status.address = 'https://renamed.tail123.ts.net'
|
|
133
|
+
await h.poll()
|
|
134
|
+
assert.equal(h.manual.hidden, true, 'a changed URL clears the stale manual field')
|
|
135
|
+
assert.equal(h.feedback.textContent, '')
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
test('Remote does not copy a placeholder or an unavailable address', async (t) => {
|
|
140
|
+
const h = createHarness(t, {
|
|
141
|
+
status: { address: '' },
|
|
142
|
+
clipboard: { writeText: () => assert.fail('no URL to copy') },
|
|
143
|
+
execCommand: () => assert.fail('no URL to copy')
|
|
144
|
+
})
|
|
145
|
+
assert.equal(h.button.disabled, true)
|
|
146
|
+
h.button.click()
|
|
147
|
+
await tick()
|
|
148
|
+
assert.equal(h.feedback.textContent, '')
|
|
149
|
+
h.state.status.address = address
|
|
150
|
+
await h.poll()
|
|
151
|
+
assert.equal(h.button.disabled, false)
|
|
152
|
+
h.state.status.phase = 'off'
|
|
153
|
+
await h.poll()
|
|
154
|
+
assert.equal(h.button.disabled, true)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('Remote restores keyboard focus after disabling the button during modern copy', async (t) => {
|
|
158
|
+
let h
|
|
159
|
+
h = createHarness(t, {
|
|
160
|
+
clipboard: { writeText: async () => { h.button.blur() } }
|
|
161
|
+
})
|
|
162
|
+
h.button.focus()
|
|
163
|
+
h.button.click()
|
|
164
|
+
await tick()
|
|
165
|
+
assert.equal(h.document.activeElement, h.button)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('Remote ignores a pending copy result if the address changes or Remote stops', async (t) => {
|
|
169
|
+
for (const change of [{ address: 'https://new.tail123.ts.net' }, { phase: 'off' }]) {
|
|
170
|
+
let rejectCopy
|
|
171
|
+
const h = createHarness(t, {
|
|
172
|
+
clipboard: { writeText: () => new Promise((resolve, reject) => { rejectCopy = reject }) },
|
|
173
|
+
execCommand: () => assert.fail('must not select a stale URL')
|
|
174
|
+
})
|
|
175
|
+
h.button.click()
|
|
176
|
+
Object.assign(h.state.status, change)
|
|
177
|
+
await h.poll()
|
|
178
|
+
rejectCopy(new Error('denied'))
|
|
179
|
+
await tick()
|
|
180
|
+
assert.equal(h.manual.hidden, true)
|
|
181
|
+
assert.equal(h.feedback.textContent, '')
|
|
182
|
+
assert.equal(h.button.disabled, change.phase === 'off')
|
|
183
|
+
}
|
|
184
|
+
})
|
package/test/anywhere.test.js
CHANGED
|
@@ -14,6 +14,37 @@ const { spawn } = require('node:child_process')
|
|
|
14
14
|
const { once, EventEmitter } = require('node:events')
|
|
15
15
|
const Setup = require('../kernel/bin/setup')
|
|
16
16
|
|
|
17
|
+
test('status reports os.hostname and cached system specs on every platform without spawning commands', async () => {
|
|
18
|
+
const source = fs.readFileSync(require.resolve('../kernel/anywhere'), 'utf8')
|
|
19
|
+
const baseRequire = require('node:module').createRequire(require.resolve('../kernel/anywhere'))
|
|
20
|
+
for (const platform of ['darwin', 'win32', 'linux']) {
|
|
21
|
+
const context = { module: { exports: {} }, process, console, require(name) {
|
|
22
|
+
if (name === 'os') return { hostname: () => 'OS-HOST', arch: () => 'x64' }
|
|
23
|
+
if (name === 'child_process') return { execFile() { assert.fail('metadata must not spawn commands') } }
|
|
24
|
+
return baseRequire(name)
|
|
25
|
+
} }
|
|
26
|
+
vm.runInNewContext(source, context)
|
|
27
|
+
const kernel = { platform, sysinfo: {
|
|
28
|
+
osInfo: { distro: 'Test OS' }, cpu: { brand: 'Test CPU' },
|
|
29
|
+
gpu_model: 'Test GPU', ram: 64, vram: 24,
|
|
30
|
+
} }
|
|
31
|
+
const anywhere = new context.module.exports(kernel)
|
|
32
|
+
anywhere.loadConfig = async () => { anywhere.config = { enabled: false, hostname: 'pinokio' } }
|
|
33
|
+
for (const installed of [false, true]) {
|
|
34
|
+
anywhere.installed = async () => installed
|
|
35
|
+
const status = await anywhere.snapshot()
|
|
36
|
+
assert.equal(status.machineName, 'OS-HOST')
|
|
37
|
+
assert.deepEqual(JSON.parse(JSON.stringify(status.system)), {
|
|
38
|
+
platform, arch: 'x64', os: 'Test OS', cpu: 'Test CPU', gpu: 'Test GPU', ram: 64, vram: 24,
|
|
39
|
+
})
|
|
40
|
+
assert.equal(status.phase, installed ? 'off' : 'setup')
|
|
41
|
+
assert.equal(anywhere.config.hostname, 'pinokio')
|
|
42
|
+
}
|
|
43
|
+
delete kernel.sysinfo
|
|
44
|
+
assert.deepEqual(JSON.parse(JSON.stringify((await anywhere.snapshot()).system)), { platform, arch: 'x64' }, 'startup before system info is ready')
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
17
48
|
test('serve config maps pinokiod to 443 and each app to its stable remote port', () => {
|
|
18
49
|
const config = buildServeConfig({
|
|
19
50
|
dnsName: 'pinokio.tail1234.ts.net.',
|