pinokiod 3.180.0 → 3.181.0

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/favicon.js CHANGED
@@ -1,55 +1,112 @@
1
1
  const axios = require('axios')
2
- const { URL } = require("url");
3
- const { JSDOM } = require("jsdom");
2
+ const { URL } = require('url')
3
+ const { JSDOM } = require('jsdom')
4
4
 
5
5
  class Favicon {
6
+ constructor(opts = {}) {
7
+ this.cache = new Map() // origin -> { url: string|null, ts: number }
8
+ this.ttlMs = opts.ttlMs || (10 * 60 * 1000) // 10 minutes
9
+ this.headTimeoutMs = opts.headTimeoutMs || 800
10
+ this.getTimeoutMs = opts.getTimeoutMs || 700
11
+ this.totalBudgetMs = opts.totalBudgetMs || 900
12
+ this.commonPaths = Array.isArray(opts.commonPaths) && opts.commonPaths.length > 0
13
+ ? opts.commonPaths
14
+ : ['/favicon.ico', '/favicon.png']
15
+ }
16
+
17
+ _now() {
18
+ return Date.now()
19
+ }
20
+
21
+ _getCache(origin) {
22
+ const entry = this.cache.get(origin)
23
+ if (!entry) return null
24
+ if (this._now() - entry.ts > this.ttlMs) {
25
+ this.cache.delete(origin)
26
+ return null
27
+ }
28
+ return entry.url
29
+ }
30
+
31
+ _setCache(origin, url) {
32
+ this.cache.set(origin, { url: url || null, ts: this._now() })
33
+ }
34
+
6
35
  async get(pageUrl) {
7
- let origin;
36
+ let origin
8
37
  try {
9
- origin = new URL(pageUrl).origin;
38
+ origin = new URL(pageUrl).origin
10
39
  } catch {
11
- throw new Error("Invalid URL: " + pageUrl);
40
+ throw new Error('Invalid URL: ' + pageUrl)
12
41
  }
13
42
 
14
- const candidates = [
15
- "/favicon.ico",
16
- "/favicon.png",
17
- "/assets/favicon.ico",
18
- "/static/favicon.ico",
19
- "/favicon.svg",
20
- ];
21
-
22
- // 1. Try common favicon paths first
23
- for (const path of candidates) {
24
- const fullUrl = origin + path;
25
- if (await this.checkImageUrl(fullUrl)) return fullUrl;
43
+ const cached = this._getCache(origin)
44
+ if (typeof cached !== 'undefined' && cached !== null) {
45
+ return cached
46
+ } else if (cached === null) {
47
+ // we cached a miss previously
48
+ return null
26
49
  }
27
50
 
28
- // 2. Fallback to parsing HTML
51
+ const start = this._now()
52
+ const withinBudget = () => (this._now() - start) < this.totalBudgetMs
53
+
54
+ // 1) Try common paths (limited set) in parallel, with short HEAD timeouts
29
55
  try {
30
- const res = await axios.get(pageUrl, { timeout: 1000 });
31
- const dom = new JSDOM(res.data);
32
- const icons = Array.from(dom.window.document.querySelectorAll("link[rel~='icon'], link[rel='apple-touch-icon']"));
33
-
34
- for (const icon of icons) {
35
- const href = icon.getAttribute("href");
36
- if (!href) continue;
37
- const resolvedUrl = new URL(href, origin).href;
38
- if (await this.checkImageUrl(resolvedUrl)) return resolvedUrl;
56
+ const attempts = this.commonPaths.map((p) => this.checkImageUrl(origin + p, this.headTimeoutMs))
57
+ const results = await Promise.all(attempts)
58
+ for (let i = 0; i < results.length; i++) {
59
+ if (results[i]) {
60
+ const url = origin + this.commonPaths[i]
61
+ this._setCache(origin, url)
62
+ return url
63
+ }
39
64
  }
40
- } catch {
41
- // Ignore HTML errors
65
+ } catch (_) {}
66
+
67
+ if (!withinBudget()) {
68
+ this._setCache(origin, null)
69
+ return null
70
+ }
71
+
72
+ // 2) Fallback: fetch the page quickly, parse <link rel="icon">, then HEAD the first 1-2 candidates
73
+ try {
74
+ const res = await axios.get(pageUrl, { timeout: this.getTimeoutMs })
75
+ const dom = new JSDOM(res.data)
76
+ const nodes = Array.from(dom.window.document.querySelectorAll("link[rel~='icon'], link[rel='apple-touch-icon']"))
77
+ const hrefs = []
78
+ for (const node of nodes) {
79
+ const href = node.getAttribute('href')
80
+ if (href && typeof href === 'string') {
81
+ try {
82
+ const resolved = new URL(href, origin).href
83
+ hrefs.push(resolved)
84
+ } catch (_) {}
85
+ }
86
+ }
87
+ // Try at most 2 parsed icons within budget
88
+ for (let i = 0; i < Math.min(2, hrefs.length); i++) {
89
+ if (!withinBudget()) break
90
+ const ok = await this.checkImageUrl(hrefs[i], this.headTimeoutMs)
91
+ if (ok) {
92
+ this._setCache(origin, hrefs[i])
93
+ return hrefs[i]
94
+ }
95
+ }
96
+ } catch (_) {
97
+ // ignore parse failures
42
98
  }
43
99
 
44
- return null;
100
+ this._setCache(origin, null)
101
+ return null
45
102
  }
46
103
 
47
- async checkImageUrl(url) {
104
+ async checkImageUrl(url, timeoutOverride) {
48
105
  try {
49
- const res = await axios.head(url, { timeout: 3000 });
50
- return res.status === 200 && res.headers["content-type"]?.startsWith("image/");
106
+ const res = await axios.head(url, { timeout: timeoutOverride || this.headTimeoutMs })
107
+ return res.status === 200 && (res.headers['content-type'] || '').startsWith('image/')
51
108
  } catch {
52
- return false;
109
+ return false
53
110
  }
54
111
  }
55
112
  }
package/kernel/peer.js CHANGED
@@ -337,6 +337,79 @@ class PeerDiscovery {
337
337
  return []
338
338
  }
339
339
  }
340
+ async router_info_lite() {
341
+ try {
342
+ let processes = []
343
+ if (this.info && this.info[this.host]) {
344
+ let procs = this.info[this.host].proc
345
+ let router = this.info[this.host].router
346
+ let port_mapping = this.info[this.host].port_mapping
347
+ for (let proc of procs) {
348
+ let chunks = (proc.ip || '').split(":")
349
+ let internal_port = chunks[chunks.length - 1]
350
+ let internal_host = chunks.slice(0, chunks.length - 1).join(":")
351
+ let external_port = port_mapping ? port_mapping[internal_port] : undefined
352
+ let external_ip = external_port ? `${this.host}:${external_port}` : undefined
353
+
354
+ let internal_router = []
355
+ // Check common local keys
356
+ const keys = [
357
+ `127.0.0.1:${proc.port}`,
358
+ `0.0.0.0:${proc.port}`,
359
+ `localhost:${proc.port}`,
360
+ ]
361
+ for (const key of keys) {
362
+ if (router && router[key]) {
363
+ internal_router = internal_router.concat(router[key])
364
+ }
365
+ }
366
+
367
+ const info = {
368
+ external_router: (router && external_ip && router[external_ip]) ? router[external_ip] : [],
369
+ internal_router,
370
+ external_ip,
371
+ external_port: external_port ? parseInt(external_port) : undefined,
372
+ internal_port: internal_port ? parseInt(internal_port) : undefined,
373
+ ...proc,
374
+ }
375
+
376
+ // In custom-domain mode, ensure external_router has something meaningful
377
+ const usingCustomDomain = this.kernel.router_kind === 'custom-domain'
378
+ if (usingCustomDomain) {
379
+ if (!info.external_router || info.external_router.length === 0) {
380
+ const fallbackKeys = new Set([
381
+ proc.ip,
382
+ `${internal_host}:${proc.port}`,
383
+ `127.0.0.1:${proc.port}`,
384
+ `0.0.0.0:${proc.port}`,
385
+ `localhost:${proc.port}`
386
+ ])
387
+ for (const key of fallbackKeys) {
388
+ if (key && router && router[key] && router[key].length > 0) {
389
+ info.external_router = router[key]
390
+ break
391
+ }
392
+ }
393
+ }
394
+ if (info.external_router && info.external_router.length > 0) {
395
+ info.external_router = Array.from(new Set(info.external_router))
396
+ } else if (internal_router.length > 0) {
397
+ info.external_router = Array.from(new Set(internal_router))
398
+ }
399
+ }
400
+
401
+ processes.push(info)
402
+ }
403
+ }
404
+ processes.sort((a, b) => {
405
+ return (b.external_port || 0) - (a.external_port || 0)
406
+ })
407
+ return processes
408
+ } catch (e) {
409
+ console.log('router_info_lite ERROR', e)
410
+ return []
411
+ }
412
+ }
340
413
  async installed() {
341
414
  let folders = await fs.promises.readdir(this.kernel.path("api"))
342
415
  let installed = []
package/kernel/util.js CHANGED
@@ -698,6 +698,8 @@ function push(params) {
698
698
  }
699
699
  }
700
700
  const clientImage = resolvePublicAssetUrl(notifyParams.contentImage) || resolvePublicAssetUrl(notifyParams.image)
701
+ const deviceId = (typeof notifyParams.device_id === 'string' && notifyParams.device_id.trim()) ? notifyParams.device_id.trim() : null
702
+ const audience = (typeof notifyParams.audience === 'string' && notifyParams.audience.trim()) ? notifyParams.audience.trim() : null
701
703
  const eventPayload = {
702
704
  id: randomUUID(),
703
705
  title: notifyParams.title,
@@ -707,11 +709,20 @@ function push(params) {
707
709
  sound: clientSoundUrl || null,
708
710
  timestamp: Date.now(),
709
711
  platform,
712
+ device_id: deviceId,
713
+ audience,
710
714
  }
711
715
 
712
716
  emitPushEvent(eventPayload)
713
-
714
- notifier.notify(notifyParams)
717
+ // Suppress host OS notification when explicitly disabled (e.g., device-scoped pushes)
718
+ const shouldNotifyHost = notifyParams.host !== false
719
+ if (shouldNotifyHost) {
720
+ const notifyCopy = { ...notifyParams }
721
+ delete notifyCopy.host
722
+ delete notifyCopy.device_id
723
+ delete notifyCopy.audience
724
+ notifier.notify(notifyCopy)
725
+ }
715
726
  }
716
727
  function p2u(localPath) {
717
728
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.180.0",
3
+ "version": "3.181.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1509,10 +1509,15 @@ class Server {
1509
1509
  drive: path.resolve(this.kernel.homedir, "drive"),
1510
1510
  }
1511
1511
  }
1512
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
1513
+ let peer_qr = null
1514
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
1512
1515
  let list = this.getPeers()
1513
1516
  res.render("settings", {
1514
1517
  list,
1515
1518
  current_host: this.kernel.peer.host,
1519
+ peer_url,
1520
+ peer_qr,
1516
1521
  platform,
1517
1522
  version: this.version,
1518
1523
  logo: this.logo,
@@ -2234,6 +2239,10 @@ class Server {
2234
2239
  qr_cloudflare = await QRCode.toDataURL(this.cloudflare_pub)
2235
2240
  }
2236
2241
 
2242
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
2243
+ let peer_qr = null
2244
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
2245
+
2237
2246
  // custom theme
2238
2247
  let exists = await fse.pathExists(this.kernel.path("web"))
2239
2248
  if (exists) {
@@ -2263,6 +2272,8 @@ class Server {
2263
2272
  res.render("index", {
2264
2273
  list,
2265
2274
  current_host: this.kernel.peer.host,
2275
+ peer_url,
2276
+ peer_qr,
2266
2277
  current_urls,
2267
2278
  portal: this.portal,
2268
2279
  install: this.install,
@@ -3924,6 +3935,9 @@ class Server {
3924
3935
  }))
3925
3936
  */
3926
3937
  this.app.get("/tools", ex(async (req, res) => {
3938
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
3939
+ let peer_qr = null
3940
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
3927
3941
  let list = this.getPeers()
3928
3942
  let installs = []
3929
3943
  for(let key in this.kernel.bin.installed) {
@@ -3955,6 +3969,8 @@ class Server {
3955
3969
  }
3956
3970
  res.render("tools", {
3957
3971
  current_host: this.kernel.peer.host,
3972
+ peer_url,
3973
+ peer_qr,
3958
3974
  pending,
3959
3975
  installs,
3960
3976
  bundles,
@@ -4039,9 +4055,14 @@ class Server {
4039
4055
  return (a.name || '').localeCompare(b.name || '')
4040
4056
  })
4041
4057
 
4058
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4059
+ let peer_qr = null
4060
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4042
4061
  const list = this.getPeers()
4043
4062
  res.render("terminals", {
4044
4063
  current_host: this.kernel.peer.host,
4064
+ peer_url,
4065
+ peer_qr,
4045
4066
  pluginMenu,
4046
4067
  apps,
4047
4068
  portal: this.portal,
@@ -4055,9 +4076,14 @@ class Server {
4055
4076
  res.redirect(301, "/terminals")
4056
4077
  })
4057
4078
  this.app.get("/screenshots", ex(async (req, res) => {
4079
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4080
+ let peer_qr = null
4081
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4058
4082
  let list = this.getPeers()
4059
4083
  res.render("screenshots", {
4060
4084
  current_host: this.kernel.peer.host,
4085
+ peer_url,
4086
+ peer_qr,
4061
4087
  version: this.version,
4062
4088
  portal: this.portal,
4063
4089
  logo: this.logo,
@@ -4144,6 +4170,7 @@ class Server {
4144
4170
  // if <app_name>.localhost
4145
4171
  // otherwise => redirect
4146
4172
 
4173
+ console.log("Chunks", chunks)
4147
4174
 
4148
4175
  if (chunks.length >= 2) {
4149
4176
 
@@ -4190,29 +4217,31 @@ class Server {
4190
4217
  } else {
4191
4218
  nameChunks = chunks
4192
4219
  }
4193
- let name = nameChunks.join(".")
4194
- let api_path = this.kernel.path("api", name)
4195
- let exists = await this.exists(api_path)
4196
- if (exists) {
4197
- let meta = await this.kernel.api.meta(name)
4198
- let launcher = await this.kernel.api.launcher(name)
4199
- let pinokio = launcher.script
4200
- let launchable = false
4201
- if (pinokio && pinokio.menu && pinokio.menu.length > 0) {
4202
- launchable = true
4203
- }
4204
- res.render("start", {
4205
- url,
4206
- launchable,
4207
- autolaunch,
4208
- logo: this.logo,
4209
- theme: this.theme,
4210
- agent: req.agent,
4211
- name: meta.title,
4212
- image: meta.icon,
4213
- link: `/p/${name}?autolaunch=${autolaunch ? "1" : "0"}`,
4214
- })
4215
- return
4220
+ if (nameChunks) {
4221
+ let name = nameChunks.join(".")
4222
+ let api_path = this.kernel.path("api", name)
4223
+ let exists = await this.exists(api_path)
4224
+ if (exists) {
4225
+ let meta = await this.kernel.api.meta(name)
4226
+ let launcher = await this.kernel.api.launcher(name)
4227
+ let pinokio = launcher.script
4228
+ let launchable = false
4229
+ if (pinokio && pinokio.menu && pinokio.menu.length > 0) {
4230
+ launchable = true
4231
+ }
4232
+ res.render("start", {
4233
+ url,
4234
+ launchable,
4235
+ autolaunch,
4236
+ logo: this.logo,
4237
+ theme: this.theme,
4238
+ agent: req.agent,
4239
+ name: meta.title,
4240
+ image: meta.icon,
4241
+ link: `/p/${name}?autolaunch=${autolaunch ? "1" : "0"}`,
4242
+ })
4243
+ return
4244
+ }
4216
4245
  }
4217
4246
  }
4218
4247
  res.render("start", {
@@ -4325,9 +4354,14 @@ class Server {
4325
4354
  drive: path.resolve(this.kernel.homedir, "drive"),
4326
4355
  }
4327
4356
  }
4357
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4358
+ let peer_qr = null
4359
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4328
4360
  let list = this.getPeers()
4329
4361
  res.render("settings", {
4330
4362
  current_host: this.kernel.peer.host,
4363
+ peer_url,
4364
+ peer_qr,
4331
4365
  list,
4332
4366
  platform,
4333
4367
  version: this.version,
@@ -4443,6 +4477,9 @@ class Server {
4443
4477
  return
4444
4478
  }
4445
4479
 
4480
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4481
+ let peer_qr = null
4482
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4446
4483
  let list = this.getPeers()
4447
4484
  let ai = await this.kernel.proto.ai()
4448
4485
  ai = [{
@@ -4456,6 +4493,8 @@ class Server {
4456
4493
  list,
4457
4494
  ai,
4458
4495
  current_host: this.kernel.peer.host,
4496
+ peer_url,
4497
+ peer_qr,
4459
4498
  cwd: this.kernel.path("api"),
4460
4499
  name: null,
4461
4500
  // name: req.params.name,
@@ -4556,9 +4595,14 @@ class Server {
4556
4595
  } catch (e) {
4557
4596
  }
4558
4597
  }
4598
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4599
+ let peer_qr = null
4600
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4559
4601
  res.render(`connect`, {
4560
4602
  current_urls,
4561
4603
  current_host: this.kernel.peer.host,
4604
+ peer_url,
4605
+ peer_qr,
4562
4606
  list,
4563
4607
  portal: this.portal,
4564
4608
  logo: this.logo,
@@ -4700,6 +4744,13 @@ class Server {
4700
4744
  this.app.post("/push", ex(async (req, res) => {
4701
4745
  try {
4702
4746
  const payload = { ...(req.body || {}) }
4747
+ // Normalise audience and device targeting
4748
+ if (typeof payload.audience === 'string') {
4749
+ payload.audience = payload.audience.trim() || undefined
4750
+ }
4751
+ if (typeof payload.device_id === 'string') {
4752
+ payload.device_id = payload.device_id.trim() || undefined
4753
+ }
4703
4754
  const resolveAssetPath = (raw) => {
4704
4755
  if (typeof raw !== 'string') {
4705
4756
  return null
@@ -4782,6 +4833,19 @@ class Server {
4782
4833
  }
4783
4834
  delete payload.soundUrl
4784
4835
  delete payload.soundPath
4836
+ // For device-scoped notifications, suppress host OS notifier for remote origins,
4837
+ // but allow it when the request originates from the local machine
4838
+ if (payload.audience === 'device' && typeof payload.device_id === 'string' && payload.device_id) {
4839
+ try {
4840
+ if (this.socket && typeof this.socket.isLocalDevice === 'function') {
4841
+ payload.host = !!this.socket.isLocalDevice(payload.device_id)
4842
+ } else {
4843
+ payload.host = false
4844
+ }
4845
+ } catch (_) {
4846
+ payload.host = false
4847
+ }
4848
+ }
4785
4849
  Util.push(payload)
4786
4850
  res.json({ success: true })
4787
4851
  } catch (e) {
@@ -5343,6 +5407,9 @@ class Server {
5343
5407
  let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
5344
5408
  return this.kernel.router.rewrite_mapping[key]
5345
5409
  })
5410
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5411
+ let peer_qr = null
5412
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5346
5413
  res.render("net", {
5347
5414
  static_routes,
5348
5415
  selected_name: req.params.name,
@@ -5361,6 +5428,8 @@ class Server {
5361
5428
  peer,
5362
5429
  protocol,
5363
5430
  current_host: this.kernel.peer.host,
5431
+ peer_url,
5432
+ peer_qr,
5364
5433
  })
5365
5434
  }))
5366
5435
  this.app.get("/network", ex(async (req, res) => {
@@ -5502,6 +5571,9 @@ class Server {
5502
5571
  let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
5503
5572
  return this.kernel.router.rewrite_mapping[key]
5504
5573
  })
5574
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5575
+ let peer_qr = null
5576
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5505
5577
  res.render("network", {
5506
5578
  static_routes,
5507
5579
  host,
@@ -5527,6 +5599,8 @@ class Server {
5527
5599
  peer_active: this.kernel.peer.active,
5528
5600
  port_mapping: this.kernel.router.port_mapping,
5529
5601
  // port_mapping: this.kernel.caddy.port_mapping,
5602
+ peer_url,
5603
+ peer_qr,
5530
5604
  // ip_mapping: this.kernel.caddy.ip_mapping,
5531
5605
  lan: this.kernel.router.local_network_mapping,
5532
5606
  agent: req.agent,
@@ -7059,6 +7133,35 @@ class Server {
7059
7133
  let current_peer_info = await this.kernel.peer.current_host()
7060
7134
  res.json(current_peer_info)
7061
7135
  }))
7136
+ this.app.get("/info/router", ex(async (req, res) => {
7137
+ try {
7138
+ // Lightweight router mapping without favicon or installed scans
7139
+ const https_active = this.kernel.peer.https_active
7140
+ const router_info = await this.kernel.peer.router_info_lite()
7141
+ const rewrite_mapping = this.kernel.router.rewrite_mapping
7142
+ const router = this.kernel.router.published()
7143
+ res.json({ https_active, router_info, rewrite_mapping, router })
7144
+ } catch (err) {
7145
+ res.json({ https_active: false, router_info: [], rewrite_mapping: {}, router: {} })
7146
+ }
7147
+ }))
7148
+ this.app.get("/qr", ex(async (req, res) => {
7149
+ try {
7150
+ const data = typeof req.query.data === 'string' ? req.query.data : ''
7151
+ if (!data) {
7152
+ res.status(400).json({ error: 'Missing data parameter' })
7153
+ return
7154
+ }
7155
+ const scale = Math.max(2, Math.min(10, parseInt(req.query.s || '4', 10) || 4))
7156
+ const margin = Math.max(0, Math.min(4, parseInt(req.query.m || '0', 10) || 0))
7157
+ const buf = await QRCode.toBuffer(data, { type: 'png', scale, margin })
7158
+ res.setHeader('Content-Type', 'image/png')
7159
+ res.setHeader('Cache-Control', 'no-store')
7160
+ res.send(buf)
7161
+ } catch (err) {
7162
+ res.status(500).json({ error: 'Failed to generate QR' })
7163
+ }
7164
+ }))
7062
7165
  this.app.get("/info/api", ex(async (req,res) => {
7063
7166
  // api related info
7064
7167
  let repo = this.kernel.git.find(req.query.git)