pinokiod 8.2.2 → 8.2.4

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.
@@ -0,0 +1,461 @@
1
+ const assert = require('node:assert/strict')
2
+ const test = require('node:test')
3
+ const http = require('node:http')
4
+ const os = require('node:os')
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
+
8
+ const { buildServeConfig, describeServeConfig, sameServeConfig } = require('../kernel/anywhere/serve')
9
+ const { assignPorts } = require('../kernel/anywhere/ports')
10
+ const { LocalAPI } = require('../kernel/anywhere/localapi')
11
+ const Anywhere = require('../kernel/anywhere')
12
+ const { spawn } = require('node:child_process')
13
+ const Tailscaled = require('../kernel/bin/tailscaled')
14
+ const Setup = require('../kernel/bin/setup')
15
+
16
+ test('serve config maps pinokiod to 443 and each app to its stable remote port', () => {
17
+ const config = buildServeConfig({
18
+ dnsName: 'pinokio.tail1234.ts.net.',
19
+ serverPort: 42000,
20
+ targets: [{ listen: 43001, target: 8188 }, { listen: 43000, target: 7860 }, { listen: 43000, target: 9999 }, { listen: 443, target: 1 }, { listen: 'nope', target: 1 }, { listen: 43002, target: 70000 }],
21
+ })
22
+ assert.deepEqual(Object.keys(config.TCP), ['443', '43000', '43001'])
23
+ assert.equal(config.TCP['43000'].HTTPS, true)
24
+ assert.equal(config.Web['pinokio.tail1234.ts.net:443'].Handlers['/'].Proxy, 'http://127.0.0.1:42000')
25
+ assert.equal(config.Web['pinokio.tail1234.ts.net:43000'].Handlers['/'].Proxy, 'http://127.0.0.1:7860')
26
+ assert.equal(config.Web['pinokio.tail1234.ts.net:43001'].Handlers['/'].Proxy, 'http://127.0.0.1:8188')
27
+ assert.equal(config.AllowFunnel, undefined, 'serve-only: never emits funnel entries')
28
+
29
+ const described = describeServeConfig(config, 'pinokio.tail1234.ts.net', { 43000: 'comfy' })
30
+ assert.deepEqual(described.map((d) => [d.url, d.local, d.name]), [
31
+ ['https://pinokio.tail1234.ts.net', 42000, null],
32
+ ['https://pinokio.tail1234.ts.net:43000', 7860, 'comfy'],
33
+ ['https://pinokio.tail1234.ts.net:43001', 8188, null],
34
+ ])
35
+ const same = buildServeConfig({ dnsName: 'pinokio.tail1234.ts.net', serverPort: 42000, targets: [{ listen: 43000, target: 7860 }, { listen: 43001, target: 8188 }] })
36
+ assert.equal(sameServeConfig(config, same), true, 'order of targets does not matter')
37
+ const relaunched = buildServeConfig({ dnsName: 'pinokio.tail1234.ts.net', serverPort: 42000, targets: [{ listen: 43000, target: 7861 }, { listen: 43001, target: 8188 }] })
38
+ assert.equal(sameServeConfig(config, relaunched), false, 'a new local port changes the mapping but not the address')
39
+ assert.equal(describeServeConfig(relaunched, 'pinokio.tail1234.ts.net')[1].url, 'https://pinokio.tail1234.ts.net:43000')
40
+ })
41
+
42
+ test('stable ports are allocated once, lowest free first, and never move', () => {
43
+ let r = assignPorts({}, ['comfy', 'forge'])
44
+ assert.deepEqual(r.map, { comfy: 43000, forge: 43001 })
45
+ assert.equal(r.changed, true)
46
+ r = assignPorts(r.map, ['forge', 'webui'])
47
+ assert.deepEqual(r.map, { comfy: 43000, forge: 43001, webui: 43002 })
48
+ r = assignPorts(r.map, ['forge'])
49
+ assert.equal(r.changed, false, 'known apps need no write')
50
+ assert.equal(assignPorts({ a: 43000, b: 43002 }, ['c']).map.c, 43001, 'gaps are reused')
51
+ })
52
+
53
+ test('anywhere preset requires tailscaled but not caddy', () => {
54
+ const preset = Setup.anywhere({})
55
+ const names = preset.requirements.map((r) => r.name)
56
+ assert.ok(names.includes('tailscaled'))
57
+ assert.ok(!names.includes('caddy'))
58
+ assert.ok(preset.conda_requirements.includes('tailscaled'))
59
+ assert.ok(!preset.conda_requirements.includes('caddy'))
60
+ })
61
+
62
+ test('tailscaled bin module pins 1.104.2 and checks the installed version', async () => {
63
+ const mod = new Tailscaled()
64
+ assert.equal(mod.cmd(), 'tailscaled=1.104.2')
65
+ mod.kernel = {
66
+ platform: 'darwin',
67
+ bin: {
68
+ installed: { conda: new Set(['tailscaled']), conda_versions: { tailscaled: '1.104.2' } },
69
+ path: (...segments) => segments.join('/'),
70
+ },
71
+ }
72
+ assert.equal(await mod.installed(), true)
73
+ mod.kernel.bin.installed.conda_versions.tailscaled = '1.104.1'
74
+ assert.equal(await mod.installed(), false)
75
+ mod.kernel.bin.installed.conda_versions.tailscaled = '1.104.2'
76
+ assert.equal(mod.binPath(), 'miniforge/bin/tailscaled')
77
+ mod.kernel.bin.installed.conda_versions.tailscaled = '1.98.5'
78
+ assert.equal(await mod.installed(), false, 'older than 1.104 (no --windows-mode=dev) is not accepted')
79
+ mod.kernel.bin.installed.conda = new Set()
80
+ assert.equal(await mod.installed(), false)
81
+ mod.kernel.platform = 'win32'
82
+ assert.equal(mod.binPath(), 'miniforge/Library/bin/tailscaled.exe')
83
+ })
84
+
85
+ function withMockDaemon(handler, fn) {
86
+ const socketPath = path.join(os.tmpdir(), `pk-ts-test-${process.pid}-${Date.now()}.sock`)
87
+ const server = http.createServer(handler)
88
+ return new Promise((resolve, reject) => {
89
+ server.listen(socketPath, async () => {
90
+ try {
91
+ resolve(await fn(socketPath))
92
+ } catch (e) {
93
+ reject(e)
94
+ } finally {
95
+ server.close()
96
+ fs.rmSync(socketPath, { force: true })
97
+ }
98
+ })
99
+ })
100
+ }
101
+
102
+ test('LocalAPI client talks plain HTTP over the socket with the Sec-Tailscale header', { skip: process.platform === 'win32' }, async () => {
103
+ const seen = []
104
+ await withMockDaemon((req, res) => {
105
+ let body = ''
106
+ req.on('data', (c) => body += c)
107
+ req.on('end', () => {
108
+ seen.push({ method: req.method, url: req.url, sec: req.headers['sec-tailscale'], ifMatch: req.headers['if-match'], body })
109
+ if (req.url === '/localapi/v0/status') {
110
+ res.setHeader('Content-Type', 'application/json')
111
+ res.end(JSON.stringify({ BackendState: 'Running', Self: { DNSName: 'pinokio.tail1.ts.net.' } }))
112
+ } else if (req.url === '/localapi/v0/serve-config' && req.method === 'GET') {
113
+ res.setHeader('Etag', 'abc')
114
+ res.end('null')
115
+ } else if (req.url === '/localapi/v0/serve-config' && req.method === 'POST') {
116
+ res.statusCode = 200
117
+ res.end()
118
+ } else if (req.url === '/localapi/v0/login-interactive') {
119
+ res.statusCode = 204
120
+ res.end()
121
+ } else {
122
+ res.statusCode = 404
123
+ res.end('nope')
124
+ }
125
+ })
126
+ }, async (socketPath) => {
127
+ const api = new LocalAPI({ socketPath })
128
+ const status = await api.status()
129
+ assert.equal(status.BackendState, 'Running')
130
+ const { etag } = await api.serveConfig()
131
+ assert.equal(etag, 'abc')
132
+ await api.setServeConfig({ TCP: {} }, etag)
133
+ await api.loginInteractive()
134
+ await assert.rejects(() => api.expect('GET', '/missing'), /404/)
135
+ })
136
+ const post = seen.find((s) => s.method === 'POST' && s.url === '/localapi/v0/serve-config')
137
+ assert.equal(post.sec, 'localapi')
138
+ assert.equal(post.ifMatch, 'abc')
139
+ assert.equal(post.body, JSON.stringify({ TCP: {} }))
140
+ })
141
+
142
+ function fakeKernel(tmp, { installed = true } = {}) {
143
+ return {
144
+ platform: 'darwin',
145
+ homedir: tmp,
146
+ server_port: 42000,
147
+ path: (...segments) => path.join(tmp, ...segments),
148
+ processes: {},
149
+ memory: { local: {} },
150
+ bin: {
151
+ path: (...segments) => path.join(tmp, 'bin', ...segments),
152
+ mod: { tailscaled: { installed: async () => installed, binPath: () => path.join(tmp, 'bin', 'tailscaled') } },
153
+ },
154
+ }
155
+ }
156
+
157
+ test('anywhere snapshot walks setup -> off -> login -> https -> ready', async () => {
158
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
159
+ try {
160
+ const kernel = fakeKernel(tmp, { installed: false })
161
+ const anywhere = new Anywhere(kernel)
162
+ assert.equal((await anywhere.snapshot()).phase, 'setup')
163
+
164
+ kernel.bin.mod.tailscaled.installed = async () => true
165
+ assert.equal((await anywhere.snapshot()).phase, 'off')
166
+
167
+ await anywhere.saveConfig({ enabled: true })
168
+ assert.equal(await anywhere.configured(), true)
169
+ assert.equal(JSON.parse(fs.readFileSync(path.join(tmp, 'anywhere', 'config.json'), 'utf8')).enabled, true)
170
+
171
+ // stub the daemon
172
+ let status = { BackendState: 'NeedsLogin', AuthURL: 'https://login.tailscale.com/a/abc', Self: {}, Peer: null, User: null }
173
+ let loginRequests = 0
174
+ let pushed = null
175
+ anywhere.pid = 123
176
+ anywhere.api = {
177
+ status: async () => status,
178
+ loginInteractive: async () => { loginRequests += 1 },
179
+ queryFeature: async () => ({ Complete: false, Text: 'approve', URL: 'https://login.tailscale.com/f/serve?node=1' }),
180
+ serveConfig: async () => ({ config: null, etag: 'e1' }),
181
+ setServeConfig: async (config) => { pushed = config },
182
+ }
183
+ let snap = await anywhere.snapshot()
184
+ assert.equal(snap.phase, 'login')
185
+ assert.equal(snap.loginUrl, 'https://login.tailscale.com/a/abc')
186
+ assert.equal(loginRequests, 0, 'uses the AuthURL the daemon already has')
187
+
188
+ anywhere.lastStatusAt = 0
189
+ status = { BackendState: 'NeedsLogin', AuthURL: '', Self: {}, Peer: null, User: null }
190
+ snap = await anywhere.snapshot()
191
+ assert.equal(loginRequests, 1, 'asks the daemon for a login URL when it has none')
192
+
193
+ anywhere.lastStatusAt = 0
194
+ status = {
195
+ BackendState: 'Running',
196
+ Version: '1.104.1',
197
+ TailscaleIPs: ['100.1.2.3'],
198
+ CertDomains: null,
199
+ Self: { DNSName: 'pinokio.tail1.ts.net.', UserID: 7, KeyExpiry: '2027-04-20T04:33:03Z' },
200
+ User: { '7': { LoginName: 'me@example.com', DisplayName: 'Me' } },
201
+ Peer: { a: { HostName: 'iphone', OS: 'iOS', Online: true, DNSName: 'iphone.tail1.ts.net.', TailscaleIPs: ['100.1.2.4'], LastSeen: '0001-01-01T00:00:00Z' } },
202
+ CurrentTailnet: { Name: 'me@example.com', MagicDNSSuffix: 'tail1.ts.net', MagicDNSEnabled: true },
203
+ }
204
+ snap = await anywhere.snapshot()
205
+ assert.equal(snap.phase, 'https')
206
+ assert.equal(snap.approval.url, 'https://login.tailscale.com/f/serve?node=1')
207
+ assert.equal(snap.address, 'https://pinokio.tail1.ts.net')
208
+
209
+ anywhere.lastStatusAt = 0
210
+ anywhere.featureAt = 0
211
+ status.CertDomains = ['pinokio.tail1.ts.net']
212
+ kernel.memory.local = { [path.join(tmp, 'api', 'comfy', 'start.json')]: { url: 'http://localhost:7860', other: 'not a url' } }
213
+ snap = await anywhere.snapshot()
214
+ assert.equal(snap.phase, 'ready')
215
+ assert.equal(snap.account.email, 'me@example.com')
216
+ assert.equal(snap.keyExpiry, '2027-04-20T04:33:03Z')
217
+ assert.equal(snap.mobilePeerPresent, true)
218
+ assert.equal(snap.mobilePeerOnline, true)
219
+ assert.equal(snap.connectUrl, 'https://pinokio.co/anywhere/connect#u=https%3A%2F%2Fpinokio.tail1.ts.net&a=me%40example.com')
220
+ assert.deepEqual(snap.serve.map((s) => [s.url, s.local, s.name]), [['https://pinokio.tail1.ts.net', 42000, null], ['https://pinokio.tail1.ts.net:43000', 7860, 'comfy']])
221
+ assert.deepEqual(JSON.parse(fs.readFileSync(path.join(tmp, 'anywhere', 'ports.json'), 'utf8')), { comfy: 43000 }, 'stable port persisted')
222
+
223
+ // menu links are rewritten only for requests addressed to the node itself
224
+ const menu = () => [
225
+ { href: 'http://localhost:7860/?x=1#t', src: 'http://localhost:7860/?x=1#t', target_full: 'http://localhost:7860/?x=1#t', menu: [{ href: 'http://127.0.0.1:42000/home' }] },
226
+ { href: 'http://localhost:9999/' },
227
+ { href: '/files/api/comfy' },
228
+ { href: 'https://example.com:7860/' },
229
+ ]
230
+ for (const host of ['localhost:42000', '192.168.1.5:42000', 'pinokio.p5.localhost', undefined]) {
231
+ assert.deepEqual(await anywhere.rewriteMenu(host, menu()), menu(), `untouched for host ${host}`)
232
+ }
233
+ for (const host of ['pinokio.tail1.ts.net', 'PINOKIO.TAIL1.TS.NET:443']) {
234
+ const rewritten = await anywhere.rewriteMenu(host, menu())
235
+ assert.equal(rewritten[0].href, 'https://pinokio.tail1.ts.net:43000/?x=1#t')
236
+ assert.equal(rewritten[0].src, 'https://pinokio.tail1.ts.net:43000/?x=1#t')
237
+ assert.equal(rewritten[0].target_full, 'https://pinokio.tail1.ts.net:43000/?x=1#t', 'the popout target follows the href')
238
+ assert.equal(rewritten[0].menu[0].href, 'https://pinokio.tail1.ts.net/home', 'pinokiod itself maps to the bare address')
239
+ assert.equal(rewritten[1].href, 'http://localhost:9999/', 'unpublished ports are left alone')
240
+ assert.equal(rewritten[2].href, '/files/api/comfy')
241
+ assert.equal(rewritten[3].href, 'https://example.com:7860/')
242
+ }
243
+ // sync pushes once, then only on changes
244
+ assert.equal(await anywhere.sync(), true)
245
+ assert.deepEqual(Object.keys(pushed.TCP), ['443', '43000'])
246
+ pushed = null
247
+ assert.equal(await anywhere.sync(), false)
248
+ assert.equal(pushed, null)
249
+
250
+ // an app that just published its URL gets its stable port at render time
251
+ // and its route is pushed right away, without waiting for the next sync tick
252
+ kernel.memory.local[path.join(tmp, 'api', 'forge', 'start.json')] = { url: 'http://127.0.0.1:8188' }
253
+ const fresh = await anywhere.rewriteMenu('pinokio.tail1.ts.net', [{ href: 'http://127.0.0.1:8188/' }])
254
+ assert.equal(fresh[0].href, 'https://pinokio.tail1.ts.net:43001/')
255
+ assert.deepEqual(JSON.parse(fs.readFileSync(path.join(tmp, 'anywhere', 'ports.json'), 'utf8')), { comfy: 43000, forge: 43001 })
256
+ await anywhere.syncing
257
+ assert.deepEqual(Object.keys(pushed.TCP), ['443', '43000', '43001'], 'route pushed at render time')
258
+ pushed = null
259
+ assert.equal(await anywhere.sync(), false)
260
+ kernel.memory.local = {}
261
+ assert.equal(await anywhere.sync(), true, 'stopped app removes its entry')
262
+ assert.deepEqual(Object.keys(pushed.TCP), ['443'])
263
+ } finally {
264
+ fs.rmSync(tmp, { recursive: true, force: true })
265
+ }
266
+ })
267
+
268
+ test('anywhere uses dev mode flags on windows and a short socket path on long homes', () => {
269
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
270
+ try {
271
+ const kernel = fakeKernel(tmp)
272
+ kernel.platform = 'win32'
273
+ const win = new Anywhere(kernel)
274
+ assert.ok(win.daemonArgs().includes('--windows-mode=dev'))
275
+ assert.ok(win.daemonArgs().includes('--tun=userspace-networking'))
276
+ assert.equal(win.socketPath(), '\\\\.\\pipe\\pinokio-tailscaled')
277
+
278
+ const longHome = path.join(tmp, 'x'.repeat(120))
279
+ const deep = new Anywhere(fakeKernel(longHome))
280
+ assert.ok(Buffer.byteLength(deep.socketPath()) <= 100, deep.socketPath())
281
+ assert.ok(deep.socketPath().startsWith(os.tmpdir()))
282
+ const shallow = new Anywhere(fakeKernel('/Users/me/pinokio'))
283
+ assert.equal(shallow.socketPath(), '/Users/me/pinokio/anywhere/tailscaled/tailscaled.sock')
284
+ } finally {
285
+ fs.rmSync(tmp, { recursive: true, force: true })
286
+ }
287
+ })
288
+
289
+ test('while Anywhere is off, sync and the menu rewrite are no-ops and never touch the daemon', async () => {
290
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
291
+ try {
292
+ const anywhere = new Anywhere(fakeKernel(tmp))
293
+ anywhere.api = new Proxy({}, { get() { throw new Error('LocalAPI must not be called while Anywhere is off') } })
294
+ assert.equal(await anywhere.sync(), false)
295
+ await anywhere.loadConfig()
296
+ assert.equal(await anywhere.sync(), false)
297
+ const menu = [{ href: 'http://localhost:7860/' }, { href: 'http://127.0.0.1:42000/home' }]
298
+ assert.deepEqual(await anywhere.rewriteMenu('pinokio.tail1.ts.net', JSON.parse(JSON.stringify(menu))), menu)
299
+ } finally {
300
+ fs.rmSync(tmp, { recursive: true, force: true })
301
+ }
302
+ })
303
+
304
+ test('serve targets follow the Home Server router rules for an app\'s URL variables', () => {
305
+ const tmp = path.join(os.tmpdir(), 'pk-home')
306
+ const kernel = fakeKernel(tmp)
307
+ kernel.router = {
308
+ localhost_variable_router: {
309
+ domain(name, key) {
310
+ if (name === 'comfy') return key === 'url' ? '@' : (key === 'api_url' ? 'api' : undefined)
311
+ throw new Error('no pinokio config for ' + name) // what Processor.domain does for an unknown app
312
+ },
313
+ },
314
+ }
315
+ kernel.memory.local = {
316
+ [path.join(tmp, 'api', 'comfy', 'start.json')]: {
317
+ url: 'http://localhost:7860',
318
+ api_url: 'http://127.0.0.1:7861',
319
+ other: 'http://localhost:7862', // not dns-mapped
320
+ remote: 'http://192.168.1.9:7863', // not loopback: cannot be proxied
321
+ noport: 'http://localhost',
322
+ count: 3,
323
+ },
324
+ [path.join(tmp, 'api', 'nocfg', 'start.json')]: { url: 'http://localhost:8000', other: 'http://localhost:8001' },
325
+ ['/elsewhere/start.json']: { url: 'http://localhost:9000' }, // outside ~/pinokio/api
326
+ }
327
+ const anywhere = new Anywhere(kernel)
328
+ assert.deepEqual(anywhere.appTargets(), [ // `url` first, then the other keys alphabetically
329
+ { key: 'comfy', name: 'comfy', local: 7860 },
330
+ { key: 'api.comfy', name: 'comfy', local: 7861 },
331
+ { key: 'nocfg', name: 'nocfg', local: 8000 },
332
+ ])
333
+ })
334
+
335
+ test('a failed https feature query keeps the page in starting instead of showing an approval', async () => {
336
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
337
+ try {
338
+ const anywhere = new Anywhere(fakeKernel(tmp))
339
+ await anywhere.saveConfig({ enabled: true })
340
+ anywhere.pid = 123
341
+ let feature = () => { throw new Error('503 no netmap') }
342
+ anywhere.api = {
343
+ status: async () => ({ BackendState: 'Running', CertDomains: null, Self: { DNSName: 'pinokio.tail1.ts.net.', UserID: 7 }, User: {}, Peer: null }),
344
+ queryFeature: async () => feature(),
345
+ }
346
+ let snap = await anywhere.snapshot()
347
+ assert.equal(snap.phase, 'starting')
348
+ assert.equal(snap.approval, null)
349
+ feature = () => ({ Complete: false, Text: 'approve', URL: 'https://login.tailscale.com/f/serve?node=1' })
350
+ anywhere.lastStatusAt = 0
351
+ snap = await anywhere.snapshot()
352
+ assert.equal(snap.phase, 'https')
353
+ assert.equal(snap.approval.url, 'https://login.tailscale.com/f/serve?node=1')
354
+ } finally {
355
+ fs.rmSync(tmp, { recursive: true, force: true })
356
+ }
357
+ })
358
+
359
+ // a stand-in daemon process we can safely signal
360
+ function spawnIdle() {
361
+ const child = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 30000)'], { stdio: 'ignore' }) // self-exits if a run is cut short
362
+ const exited = new Promise((resolve) => child.on('exit', () => resolve(true)))
363
+ return { child, exited: (ms = 3000) => Promise.race([exited, new Promise((resolve) => setTimeout(() => resolve(false), ms))]) }
364
+ }
365
+
366
+ test('adopting a daemon left by a previous run recovers its pid from the pid file', async () => {
367
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
368
+ const idle = spawnIdle()
369
+ try {
370
+ const kernel = fakeKernel(tmp)
371
+ const anywhere = new Anywhere(kernel)
372
+ fs.mkdirSync(anywhere.stateDir(), { recursive: true })
373
+ const dead = spawnIdle(); dead.child.kill('SIGKILL'); await dead.exited()
374
+ fs.writeFileSync(anywhere.pidPath(), String(dead.child.pid))
375
+ assert.equal(anywhere.readPid(), null, 'a pid that no longer exists is ignored')
376
+ fs.writeFileSync(anywhere.pidPath(), String(idle.child.pid))
377
+ await anywhere.saveConfig({ enabled: true })
378
+ anywhere.api = {
379
+ ping: async () => true,
380
+ patchPrefs: async () => ({}),
381
+ status: async () => ({ BackendState: 'NeedsLogin', AuthURL: 'https://login.tailscale.com/a/abc', Self: {}, User: null, Peer: null }),
382
+ }
383
+ await anywhere.start()
384
+ assert.equal(anywhere.pid, idle.child.pid)
385
+ } finally {
386
+ idle.child.kill('SIGKILL')
387
+ fs.rmSync(tmp, { recursive: true, force: true })
388
+ }
389
+ })
390
+
391
+
392
+ test('a daemon that cannot be spawned reports the cause at once instead of waiting 20 s', async () => {
393
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
394
+ try {
395
+ const kernel = fakeKernel(tmp)
396
+ kernel.bin.mod.tailscaled.binPath = () => path.join(tmp, 'no-such-tailscaled')
397
+ const anywhere = new Anywhere(kernel)
398
+ await anywhere.saveConfig({ enabled: true })
399
+ const started = Date.now()
400
+ await assert.rejects(() => anywhere.start(), /failed to start/)
401
+ assert.ok(Date.now() - started < 5000, 'did not sit out the 20 s daemon timeout')
402
+ assert.equal((await anywhere.snapshot()).phase, 'error')
403
+ assert.equal(fs.existsSync(anywhere.pidPath()), false)
404
+ } finally {
405
+ fs.rmSync(tmp, { recursive: true, force: true })
406
+ }
407
+ })
408
+
409
+ test('sign-out resets the login state, and a link the server rejected is replaced instead of shown', async () => {
410
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-'))
411
+ try {
412
+ const anywhere = new Anywhere(fakeKernel(tmp))
413
+ await anywhere.saveConfig({ enabled: true })
414
+ anywhere.pid = 123
415
+ const calls = []
416
+ let status = { BackendState: 'NeedsLogin', AuthURL: 'https://login.tailscale.com/a/dead', Self: {}, User: null, Peer: null,
417
+ Health: ['You are logged out. The last login error was: register request: http 410: auth path not found'] }
418
+ anywhere.api = {
419
+ status: async () => status,
420
+ logout: async () => { calls.push('logout') },
421
+ patchPrefs: async (p) => { calls.push('prefs:' + (p.LoggedOut === false ? 'login' : 'other')); return {} },
422
+ start: async () => { calls.push('start') },
423
+ loginInteractive: async () => { calls.push('login-interactive') },
424
+ }
425
+ await anywhere.logout()
426
+ assert.deepEqual(calls, ['logout', 'prefs:login', 'start'], 'sign-out: logout, then back to logged-in prefs, then reset')
427
+
428
+ calls.length = 0
429
+ let snap = await anywhere.snapshot()
430
+ assert.equal(snap.loginUrl, null, 'the rejected link is not offered')
431
+ assert.equal(snap.phase, 'starting')
432
+ assert.deepEqual(calls, ['start', 'login-interactive'], 'a rejected link triggers a reset and a fresh request')
433
+
434
+ // after the reset the daemon keeps a stale "last login error: ... context canceled" line; that is not a rejection
435
+ anywhere.lastStatusAt = 0
436
+ calls.length = 0
437
+ status = { BackendState: 'NeedsLogin', AuthURL: 'https://login.tailscale.com/a/fresh', Self: {}, User: null, Peer: null,
438
+ Health: ['You are logged out. The last login error was: fetch control key: Get "https://controlplane.tailscale.com/key?v=148": context canceled'] }
439
+ snap = await anywhere.snapshot()
440
+ assert.equal(snap.loginUrl, 'https://login.tailscale.com/a/fresh', 'a fresh link is offered as is')
441
+ assert.equal(snap.phase, 'login')
442
+ assert.deepEqual(calls, [], 'no reset loop on a stale error line')
443
+ } finally {
444
+ fs.rmSync(tmp, { recursive: true, force: true })
445
+ }
446
+ })
447
+
448
+
449
+ test('old Tailscale daemons are stopped and cannot be adopted through direct start', async t => {
450
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-version-'))
451
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }))
452
+ const anywhere = new Anywhere(fakeKernel(root, { installed: false }))
453
+ let stopped = 0
454
+ anywhere.daemonAlive = async () => true
455
+ anywhere.enabled = async () => true
456
+ anywhere.stop = async () => { stopped++ }
457
+ assert.equal(await anywhere.autostart(), false)
458
+ assert.equal(stopped, 1)
459
+ await assert.rejects(anywhere.start(), /Update the tailscaled tool/)
460
+ assert.equal(stopped, 2)
461
+ })
@@ -6,16 +6,40 @@ const os = require('node:os')
6
6
  const path = require('node:path')
7
7
  const test = require('node:test')
8
8
 
9
- const Bin = require('../kernel/bin')
10
- const Conda = require('../kernel/bin/conda')
9
+ let Bin
10
+ let Conda
11
11
  const {
12
12
  CONDA_PIN_VERSION,
13
13
  PYTHON_INSTALL_SPEC,
14
14
  WINDOWS_PYTHON_SSL_FIX_SPEC,
15
15
  isExpectedCondaPinned,
16
16
  isExpectedPythonPinned,
17
+ TLS_PACKAGE_MINIMUMS,
18
+ TLS_PACKAGE_SPECS,
19
+ PIP_INSTALL_SPEC,
20
+ PIP_MIN_VERSION,
17
21
  } = require('../kernel/bin/conda-pins')
18
22
 
23
+ test.beforeEach(t => {
24
+ const realExecFile = childProcess.execFile.bind(childProcess)
25
+ t.mock.method(childProcess, 'execFile', (binary, args, options, done) => {
26
+ if (args[0] !== '-I' || args[1] !== '-c') return realExecFile(binary, args, options, done)
27
+ const prefix = path.basename(binary) === 'python.exe' ? path.dirname(binary) : path.dirname(path.dirname(binary))
28
+ if (args[2].includes('certifi.where()')) {
29
+ done(null, path.join(prefix, 'certificates.pem') + '\n2026.10.19\n')
30
+ return
31
+ }
32
+ fs.readFile(path.join(prefix, 'test-pip-version'), 'utf8').then(
33
+ version => done(null, version), error => done(error, '')
34
+ )
35
+ })
36
+ for (const file of ['../kernel/bin/conda-meta', '../kernel/bin/conda', '../kernel/bin']) {
37
+ delete require.cache[require.resolve(file)]
38
+ }
39
+ Bin = require('../kernel/bin')
40
+ Conda = require('../kernel/bin/conda')
41
+ })
42
+
19
43
  async function pathExists(target) {
20
44
  return fs.access(target).then(() => true).catch(() => false)
21
45
  }
@@ -49,6 +73,10 @@ async function writeHealthyCondaMeta(root, platform = 'win32', condaVersion = CO
49
73
  await writeCondaMeta(root, 'conda', condaVersion, 'py310')
50
74
  await writeCondaMeta(root, 'conda-libmamba-solver', '25.4.0', 'pyhd3eb1b0_0')
51
75
  await writeCondaMeta(root, 'python', '3.10.20', platform === 'win32' ? 'h4de0772_1_cpython' : 'cpython')
76
+ for (const [name, version] of Object.entries(TLS_PACKAGE_MINIMUMS)) {
77
+ await writeCondaMeta(root, name, version)
78
+ }
79
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), PIP_MIN_VERSION)
52
80
  }
53
81
 
54
82
  async function writeFakeManagedConda(root, platform = 'win32') {
@@ -87,6 +115,7 @@ function restoreEnv(name, value) {
87
115
  function createConda(kernel) {
88
116
  const conda = new Conda()
89
117
  conda.kernel = kernel
118
+ conda.verifyInstaller = async () => {} // Installer lifecycle tests mock the download.
90
119
  return conda
91
120
  }
92
121
 
@@ -161,7 +190,7 @@ test('Conda uses Miniforge assets and writes conda-forge-only config', async ()
161
190
 
162
191
  assert.equal(
163
192
  conda.urls.win32.x64,
164
- 'https://github.com/conda-forge/miniforge/releases/download/26.3.2-3/Miniforge3-Windows-x86_64.exe'
193
+ 'https://github.com/conda-forge/miniforge/releases/download/26.9.4-0/Miniforge3-Windows-x86_64.exe'
165
194
  )
166
195
 
167
196
  await conda.init()
@@ -201,7 +230,7 @@ test('Conda init writes the current Conda pin into an existing runtime', async (
201
230
 
202
231
  assert.equal(
203
232
  await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
204
- 'conda ==26.5.3'
233
+ ['conda ==26.5.3', ...TLS_PACKAGE_SPECS].join('\n')
205
234
  )
206
235
  })
207
236
 
@@ -253,13 +282,14 @@ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () =>
253
282
  assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
254
283
  const condaInstall = calls.find((call) => Array.isArray(call.message))
255
284
  assert.ok(condaInstall)
285
+ assert.equal(condaInstall.message[2], `python -I -m pip --isolated --disable-pip-version-check install --no-index --no-deps "${PIP_INSTALL_SPEC}"`)
256
286
  assert.equal(
257
287
  condaInstall.message[1],
258
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0"'
288
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
259
289
  )
260
290
  assert.equal(
261
291
  await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
262
- 'conda ==26.5.3'
292
+ ['conda ==26.5.3', ...TLS_PACKAGE_SPECS].join('\n')
263
293
  )
264
294
  })
265
295
 
@@ -287,7 +317,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
287
317
  assert.ok(condaInstall)
288
318
  assert.equal(
289
319
  condaInstall.message[1],
290
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0"'
320
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
291
321
  )
292
322
  })
293
323
 
@@ -319,7 +349,7 @@ test('Conda install replaces the stale runtime and rebuilds declared Conda modul
319
349
  assert.ok(condaInstall)
320
350
  assert.equal(
321
351
  condaInstall.message[1],
322
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" uv=0.11.23 nodejs=22.21.1 pnpm'
352
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19" uv=0.11.23 nodejs=22.21.1 pnpm'
323
353
  )
324
354
  })
325
355
 
@@ -583,3 +613,91 @@ test('Conda install reports recovery details when the old runtime cannot be remo
583
613
  fsModule.promises.rm = originalRm
584
614
  }
585
615
  })
616
+
617
+
618
+ test('stale certificate packages trigger the existing Conda setup checks', async t => {
619
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-certs-'))
620
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
621
+ await writeHealthyCondaMeta(root, 'darwin')
622
+ await writeFakeManagedConda(root, 'darwin')
623
+ for (const [name, version] of Object.entries(TLS_PACKAGE_MINIMUMS)) {
624
+ await fs.rm(path.join(root, 'bin/miniforge/conda-meta', `${name}-${version}.json`))
625
+ await writeCondaMeta(root, name, '2026.4.22')
626
+ }
627
+ assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
628
+ const bin = createBin(root, 'darwin')
629
+ await bin.tryList()
630
+ assert.equal(bin.correct_conda, false)
631
+ })
632
+
633
+ test('tampered Miniforge installers leave the existing runtime untouched', async t => {
634
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-hash-'))
635
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
636
+ await createMiniforge(root)
637
+ const kernel = createKernel(root, 'darwin')
638
+ kernel.bin.download = async () => fs.writeFile(path.join(root, 'bin/installer.sh'), 'tampered')
639
+ const conda = new Conda()
640
+ conda.kernel = kernel
641
+ await assert.rejects(conda._install({}, () => {}), /SHA-256 verification failed/)
642
+ assert.equal(await pathExists(path.join(root, 'bin/miniforge/Scripts/conda.exe')), true)
643
+ })
644
+
645
+
646
+ test('old pip cannot pass Conda readiness even with current CA packages', async t => {
647
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-ready-'))
648
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
649
+ await writeHealthyCondaMeta(root, 'darwin')
650
+ await writeFakeManagedConda(root, 'darwin')
651
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), '26.2.1')
652
+ assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
653
+ const bin = createBin(root, 'darwin')
654
+ await bin.tryList()
655
+ assert.equal(bin.correct_conda, false)
656
+ })
657
+
658
+ test('certificate refresh preserves named environments and uses the new CA bundle', async t => {
659
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-refresh-'))
660
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
661
+ await writeHealthyCondaMeta(root, 'darwin')
662
+ await writeFakeManagedConda(root, 'darwin')
663
+ const marker = path.join(root, 'bin/miniforge/envs/app/keep')
664
+ await fs.mkdir(path.dirname(marker), { recursive: true })
665
+ await fs.writeFile(marker, 'existing app')
666
+ const kernel = createKernel(root, 'darwin')
667
+ const calls = []
668
+ kernel.bin.download = async () => assert.fail('must not replace a working runtime')
669
+ kernel.bin.exec = async payload => { calls.push(payload); return {} }
670
+ await createConda(kernel).install({}, () => {})
671
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
672
+ assert.equal(calls.length, 2)
673
+ assert.equal(calls[1].env.CONDA_SSL_VERIFY, path.join(root, 'bin/miniforge/certificates.pem'))
674
+ })
675
+
676
+ test('failed pip bootstrap stops certificate refresh without replacing environments', async t => {
677
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-failed-'))
678
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
679
+ await writeHealthyCondaMeta(root, 'darwin')
680
+ await writeFakeManagedConda(root, 'darwin')
681
+ const kernel = createKernel(root, 'darwin')
682
+ kernel.bin.exec = async () => ({ error: ['ERROR: hash verification failed'] })
683
+ kernel.bin.download = async () => assert.fail('must preserve the runtime')
684
+ await assert.rejects(createConda(kernel).install({}, () => {}), /bootstrap failed/)
685
+ assert.equal(await pathExists(path.join(root, 'bin/miniforge/conda-meta')), true)
686
+ })
687
+
688
+ test('fresh installation does not report a failed pip bootstrap as finished', async t => {
689
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-install-failed-'))
690
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
691
+ await fs.mkdir(path.join(root, 'bin'), { recursive: true })
692
+ const kernel = createKernel(root, 'darwin')
693
+ kernel.bin.mods = []
694
+ kernel.bin.download = async () => {}
695
+ kernel.bin.exec = async payload => {
696
+ if (payload.conda?.skip) {
697
+ await fs.mkdir(path.join(root, 'bin/miniforge/conda-meta'), { recursive: true })
698
+ return {}
699
+ }
700
+ return { error: ['ERROR: pip download failed'] }
701
+ }
702
+ await assert.rejects(createConda(kernel)._install({}, () => {}), /pip installation failed/)
703
+ })