pinokiod 3.250.0 → 3.251.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.
@@ -149,6 +149,81 @@
149
149
 
150
150
  const isIPv4Host = (host) => /^(\d{1,3}\.){3}\d{1,3}$/.test((host || '').trim())
151
151
 
152
+ const normalizeHostValue = (value) => {
153
+ if (!value || typeof value !== 'string') {
154
+ return ''
155
+ }
156
+ return value.trim().toLowerCase()
157
+ }
158
+
159
+ const classifyHostScope = (host) => {
160
+ const value = normalizeHostValue(host)
161
+ if (!value) {
162
+ return 'unknown'
163
+ }
164
+ if (value === 'localhost' || value === '0.0.0.0' || value.startsWith('127.')) {
165
+ return 'loopback'
166
+ }
167
+ if (/^10\./.test(value)) {
168
+ return 'lan'
169
+ }
170
+ if (/^192\.168\./.test(value)) {
171
+ return 'lan'
172
+ }
173
+ if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(value)) {
174
+ return 'lan'
175
+ }
176
+ if (/^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\./.test(value)) {
177
+ return 'cgnat'
178
+ }
179
+ if (/^169\.254\./.test(value) || value.startsWith('fe80:')) {
180
+ return 'linklocal'
181
+ }
182
+ return 'public'
183
+ }
184
+
185
+ const scopeToBadge = (scope) => {
186
+ if (!scope || typeof scope !== 'string') {
187
+ return ''
188
+ }
189
+ const normalized = scope.trim().toLowerCase()
190
+ switch (normalized) {
191
+ case 'lan':
192
+ return 'LAN'
193
+ case 'cgnat':
194
+ return 'VPN'
195
+ case 'public':
196
+ return 'Public'
197
+ case 'loopback':
198
+ return 'Local'
199
+ case 'linklocal':
200
+ return 'Link-Local'
201
+ default:
202
+ return ''
203
+ }
204
+ }
205
+
206
+ const mergeMeta = (existing, incoming) => {
207
+ if (!incoming) {
208
+ return existing || null
209
+ }
210
+ if (!existing) {
211
+ return { ...incoming }
212
+ }
213
+ const merged = { ...existing }
214
+ const assignIfMissing = (key) => {
215
+ if ((merged[key] === undefined || merged[key] === null || merged[key] === '') && incoming[key]) {
216
+ merged[key] = incoming[key]
217
+ }
218
+ }
219
+ assignIfMissing('scope')
220
+ assignIfMissing('interface')
221
+ assignIfMissing('source')
222
+ assignIfMissing('host')
223
+ assignIfMissing('port')
224
+ return merged
225
+ }
226
+
152
227
  const extractProjectSlug = (node) => {
153
228
  if (!node) {
154
229
  return ""
@@ -424,6 +499,7 @@
424
499
  const hostPortMap = new Map()
425
500
  const externalHttpByExtPort = new Map() // ext port -> Set of host:port (external_ip)
426
501
  const externalHttpByIntPort = new Map() // internal port -> Set of host:port (external_ip)
502
+ const externalHostMeta = new Map() // host:port -> meta info
427
503
  const hostAliasPortMap = new Map()
428
504
  if (data?.router && typeof data.router === "object") {
429
505
  Object.entries(data.router).forEach(([dial, hosts]) => {
@@ -451,6 +527,50 @@
451
527
  }
452
528
  const localAliases = ["127.0.0.1", "localhost", "0.0.0.0", "::1", "[::1]"]
453
529
 
530
+ const registerExternalHttpHost = ({ host, port, internalPort, scope, iface, source }) => {
531
+ if (!host || !port) {
532
+ return
533
+ }
534
+ const hostTrimmed = `${host}`.trim()
535
+ const portTrimmed = `${port}`.trim()
536
+ if (!hostTrimmed || !portTrimmed) {
537
+ return
538
+ }
539
+ const hostPort = `${hostTrimmed}:${portTrimmed}`
540
+ if (!externalHttpByExtPort.has(portTrimmed)) {
541
+ externalHttpByExtPort.set(portTrimmed, new Set())
542
+ }
543
+ externalHttpByExtPort.get(portTrimmed).add(hostPort)
544
+ if (internalPort) {
545
+ const intKey = `${internalPort}`.trim()
546
+ if (intKey) {
547
+ if (!externalHttpByIntPort.has(intKey)) {
548
+ externalHttpByIntPort.set(intKey, new Set())
549
+ }
550
+ externalHttpByIntPort.get(intKey).add(hostPort)
551
+ }
552
+ }
553
+ if (!externalHostMeta.has(hostPort)) {
554
+ const inferredScope = scope || classifyHostScope(hostTrimmed)
555
+ externalHostMeta.set(hostPort, {
556
+ scope: inferredScope,
557
+ interface: iface || null,
558
+ source: source || null,
559
+ host: hostTrimmed,
560
+ port: portTrimmed
561
+ })
562
+ } else {
563
+ const current = externalHostMeta.get(hostPort)
564
+ externalHostMeta.set(hostPort, mergeMeta(current, {
565
+ scope: scope || classifyHostScope(hostTrimmed),
566
+ interface: iface || null,
567
+ source: source || null,
568
+ host: hostTrimmed,
569
+ port: portTrimmed
570
+ }))
571
+ }
572
+ }
573
+
454
574
  const addHttpMapping = (host, port, httpsSet) => {
455
575
  if (!host || !port || !httpsSet || httpsSet.size === 0) {
456
576
  return
@@ -576,20 +696,59 @@
576
696
  // Record external http host:port candidates by external and internal ports for later
577
697
  if (entry.external_ip && typeof entry.external_ip === 'string') {
578
698
  const parsed = parseHostPort(entry.external_ip)
579
- if (parsed && parsed.port) {
580
- const keyExt = parsed.port
581
- if (!externalHttpByExtPort.has(keyExt)) {
582
- externalHttpByExtPort.set(keyExt, new Set())
699
+ if (parsed && parsed.host && parsed.port) {
700
+ registerExternalHttpHost({
701
+ host: parsed.host,
702
+ port: parsed.port,
703
+ internalPort: entry.internal_port,
704
+ source: 'external_ip'
705
+ })
706
+ }
707
+ }
708
+ if (Array.isArray(entry.external_hosts)) {
709
+ entry.external_hosts.forEach((hostEntry) => {
710
+ if (!hostEntry) {
711
+ return
583
712
  }
584
- externalHttpByExtPort.get(keyExt).add(`${parsed.host}:${parsed.port}`)
585
- const keyInt = String(entry.internal_port || '')
586
- if (keyInt) {
587
- if (!externalHttpByIntPort.has(keyInt)) {
588
- externalHttpByIntPort.set(keyInt, new Set())
713
+ if (typeof hostEntry === 'string') {
714
+ const parsed = parseHostPort(hostEntry)
715
+ if (parsed && parsed.host && parsed.port) {
716
+ registerExternalHttpHost({
717
+ host: parsed.host,
718
+ port: parsed.port,
719
+ internalPort: entry.internal_port,
720
+ source: 'external_hosts'
721
+ })
589
722
  }
590
- externalHttpByIntPort.get(keyInt).add(`${parsed.host}:${parsed.port}`)
723
+ return
591
724
  }
592
- }
725
+ if (typeof hostEntry === 'object') {
726
+ let host = typeof hostEntry.host === 'string' ? hostEntry.host : null
727
+ if (!host && typeof hostEntry.address === 'string') {
728
+ host = hostEntry.address
729
+ }
730
+ let portValue = hostEntry.port || hostEntry.external_port
731
+ if ((!host || !portValue) && typeof hostEntry.url === 'string') {
732
+ const parsed = parseHostPort(hostEntry.url)
733
+ if (parsed) {
734
+ if (!host && parsed.host) {
735
+ host = parsed.host
736
+ }
737
+ if (!portValue && parsed.port) {
738
+ portValue = parsed.port
739
+ }
740
+ }
741
+ }
742
+ registerExternalHttpHost({
743
+ host,
744
+ port: portValue,
745
+ internalPort: entry.internal_port,
746
+ scope: typeof hostEntry.scope === 'string' ? hostEntry.scope : null,
747
+ iface: typeof hostEntry.interface === 'string' ? hostEntry.interface : null,
748
+ source: 'external_hosts'
749
+ })
750
+ }
751
+ })
593
752
  }
594
753
 
595
754
  if (httpsTargets.size === 0) {
@@ -668,7 +827,8 @@
668
827
  portMap,
669
828
  hostPortMap,
670
829
  externalHttpByExtPort,
671
- externalHttpByIntPort
830
+ externalHttpByIntPort,
831
+ externalHostMeta
672
832
  }
673
833
  })
674
834
  .catch(() => {
@@ -677,7 +837,8 @@
677
837
  portMap: new Map(),
678
838
  hostPortMap: new Map(),
679
839
  externalHttpByExtPort: new Map(),
680
- externalHttpByIntPort: new Map()
840
+ externalHttpByIntPort: new Map(),
841
+ externalHostMeta: new Map()
681
842
  }
682
843
  })
683
844
  tabLinkRouterInfoExpiry = now + 3000
@@ -781,14 +942,21 @@
781
942
  if (entryByUrl.has(canonical)) {
782
943
  const existing = entryByUrl.get(canonical)
783
944
  if (opts && opts.qr === true) existing.qr = true
945
+ if (opts && opts.meta) {
946
+ existing.meta = mergeMeta(existing.meta, opts.meta)
947
+ existing.badge = scopeToBadge(existing.meta && existing.meta.scope ? existing.meta.scope : '')
948
+ }
784
949
  return
785
950
  }
951
+ const entryMeta = opts && opts.meta ? opts.meta : null
786
952
  const entry = {
787
953
  type,
788
954
  label,
789
955
  url: canonical,
790
956
  display: formatDisplayUrl(canonical),
791
- qr: opts && opts.qr === true
957
+ qr: opts && opts.qr === true,
958
+ meta: entryMeta,
959
+ badge: scopeToBadge(entryMeta && entryMeta.scope ? entryMeta.scope : '')
792
960
  }
793
961
  entryByUrl.set(canonical, entry)
794
962
  entries.push(entry)
@@ -802,11 +970,25 @@
802
970
  addEntry("url", "URL", baseHref, { allowSameOrigin: true })
803
971
  }
804
972
 
805
- const httpCandidates = new Map() // url -> { qr: boolean }
973
+ const httpCandidates = new Map() // url -> { qr: boolean, meta: object|null }
806
974
  const httpsCandidates = new Set()
807
975
 
976
+ const upsertHttpCandidate = (url, { qr = false, meta = null } = {}) => {
977
+ if (!url) {
978
+ return
979
+ }
980
+ const existing = httpCandidates.get(url) || { qr: false, meta: null }
981
+ if (qr === true) {
982
+ existing.qr = true
983
+ }
984
+ if (meta) {
985
+ existing.meta = mergeMeta(existing.meta, meta)
986
+ }
987
+ httpCandidates.set(url, existing)
988
+ }
989
+
808
990
  if (isHttpUrl(baseHref)) {
809
- httpCandidates.set(canonicalBase || canonicalizeUrl(baseHref), { qr: false })
991
+ upsertHttpCandidate(canonicalBase || canonicalizeUrl(baseHref), { qr: false })
810
992
  } else if (isHttpsUrl(baseHref)) {
811
993
  if (canonicalBase) {
812
994
  httpsCandidates.add(canonicalBase)
@@ -828,10 +1010,10 @@
828
1010
  const normalizedPath = pathname.toLowerCase()
829
1011
  if (normalizedPath.includes(`/asset/api/${projectSlug}`)) {
830
1012
  const fallbackHttp = `http://127.0.0.1:42000${pathname}`
831
- httpCandidates.set(canonicalizeUrl(fallbackHttp), { qr: false })
1013
+ upsertHttpCandidate(canonicalizeUrl(fallbackHttp), { qr: false })
832
1014
  } else if (normalizedPath.includes(`/api/${projectSlug}`)) {
833
1015
  const fallbackHttp = `http://127.0.0.1:42000/asset/api/${projectSlug}/`
834
- httpCandidates.set(canonicalizeUrl(fallbackHttp), { qr: false })
1016
+ upsertHttpCandidate(canonicalizeUrl(fallbackHttp), { qr: false })
835
1017
  }
836
1018
  } catch (_) {
837
1019
  // ignore fallback errors
@@ -855,8 +1037,7 @@
855
1037
  if (isHttpsUrl(canonical)) {
856
1038
  httpsCandidates.add(canonical)
857
1039
  } else if (isHttpUrl(canonical)) {
858
- const prev = httpCandidates.get(canonical)
859
- httpCandidates.set(canonical, { qr: prev ? prev.qr === true : false })
1040
+ upsertHttpCandidate(canonical, { qr: false })
860
1041
  }
861
1042
  })
862
1043
  })
@@ -886,8 +1067,8 @@
886
1067
  const hpUrl = `http://${hostport}${base.pathname || '/'}${base.search || ''}`
887
1068
  const canonical = canonicalizeUrl(hpUrl)
888
1069
  if (isHttpUrl(canonical)) {
889
- const prev = httpCandidates.get(canonical)
890
- httpCandidates.set(canonical, { qr: true || (prev ? prev.qr === true : false) })
1070
+ const meta = routerData && routerData.externalHostMeta ? routerData.externalHostMeta.get(hostport) : null
1071
+ upsertHttpCandidate(canonical, { qr: true, meta })
891
1072
  }
892
1073
  } catch (_) {}
893
1074
  })
@@ -909,8 +1090,7 @@
909
1090
  const hostPort = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
910
1091
  const httpUrl = `http://${hostPort}${parsed.pathname || "/"}${parsed.search || ""}`
911
1092
  const key = canonicalizeUrl(httpUrl)
912
- const prev = httpCandidates.get(key)
913
- httpCandidates.set(key, { qr: prev ? prev.qr === true : false })
1093
+ upsertHttpCandidate(key, { qr: false })
914
1094
  } catch (_) {
915
1095
  // ignore failures
916
1096
  }
@@ -920,8 +1100,8 @@
920
1100
  const httpList = Array.from(httpCandidates.keys()).sort()
921
1101
 
922
1102
  httpList.forEach((url) => {
923
- const meta = httpCandidates.get(url) || { qr: false }
924
- addEntry("http", "HTTP", url, { qr: meta.qr === true })
1103
+ const candidate = httpCandidates.get(url) || { qr: false, meta: null }
1104
+ addEntry("http", "HTTP", url, { qr: candidate.qr === true, meta: candidate.meta || null })
925
1105
  })
926
1106
  httpsList.forEach((url) => {
927
1107
  addEntry("https", "HTTPS", url)
@@ -954,6 +1134,28 @@
954
1134
  if (peerHostLower !== "localhost" && !peerHostLower.startsWith("127.")) {
955
1135
  const baseUrl = parsedBaseUrl || new URL(baseHref, location.origin)
956
1136
  const baseHostLower = (baseUrl.hostname || "").toLowerCase()
1137
+ const candidateList = Array.isArray(peerInfo?.host_candidates) ? peerInfo.host_candidates : []
1138
+ const metaForHost = (hostValue, source = 'peer-fallback') => {
1139
+ if (!hostValue) {
1140
+ return null
1141
+ }
1142
+ const normalized = normalizeHostValue(hostValue)
1143
+ const match = candidateList.find((candidate) => normalizeHostValue(candidate && candidate.address) === normalized)
1144
+ if (match) {
1145
+ return {
1146
+ scope: typeof match.scope === 'string' ? match.scope : classifyHostScope(hostValue),
1147
+ interface: typeof match.interface === 'string' ? match.interface : null,
1148
+ source: 'peer-candidate',
1149
+ host: match.address || hostValue
1150
+ }
1151
+ }
1152
+ return {
1153
+ scope: classifyHostScope(hostValue),
1154
+ interface: null,
1155
+ source,
1156
+ host: hostValue
1157
+ }
1158
+ }
957
1159
  if (peerHostLower !== baseHostLower) {
958
1160
  const baseProtocol = baseUrl.protocol ? baseUrl.protocol.toLowerCase() : "http:"
959
1161
  const scheme = baseProtocol === "https:" ? "https://" : "http://"
@@ -963,7 +1165,40 @@
963
1165
  const searchSegment = baseUrl.search || ""
964
1166
  const fallbackUrl = `${scheme}${hostPort}${pathSegment}${searchSegment}`
965
1167
  const label = baseProtocol === "https:" ? "HTTPS" : "HTTP"
966
- addEntry(baseProtocol === "https:" ? "https" : "http", label, fallbackUrl, { qr: true })
1168
+ addEntry(baseProtocol === "https:" ? "https" : "http", label, fallbackUrl, { qr: true, meta: metaForHost(peerHost) })
1169
+ }
1170
+ if (candidateList.length > 0) {
1171
+ candidateList.forEach((candidate) => {
1172
+ if (!candidate || typeof candidate.address !== "string") {
1173
+ return
1174
+ }
1175
+ const candidateHost = candidate.address.trim()
1176
+ if (!candidateHost) {
1177
+ return
1178
+ }
1179
+ const candidateHostLower = candidateHost.toLowerCase()
1180
+ if (candidateHostLower === peerHostLower) {
1181
+ return
1182
+ }
1183
+ if (candidateHostLower === "localhost" || candidateHostLower.startsWith("127.")) {
1184
+ return
1185
+ }
1186
+ const baseProtocol = baseUrl.protocol ? baseUrl.protocol.toLowerCase() : "http:"
1187
+ const scheme = baseProtocol === "https:" ? "https://" : "http://"
1188
+ const port = baseUrl.port || (baseProtocol === "https:" ? "443" : "80")
1189
+ const hostPort = port ? `${candidateHost}:${port}` : candidateHost
1190
+ const pathSegment = baseUrl.pathname || "/"
1191
+ const searchSegment = baseUrl.search || ""
1192
+ const fallbackUrl = `${scheme}${hostPort}${pathSegment}${searchSegment}`
1193
+ const label = baseProtocol === "https:" ? "HTTPS" : "HTTP"
1194
+ const entryMeta = {
1195
+ scope: typeof candidate.scope === 'string' ? candidate.scope : classifyHostScope(candidateHost),
1196
+ interface: typeof candidate.interface === 'string' ? candidate.interface : null,
1197
+ source: 'peer-candidate',
1198
+ host: candidateHost
1199
+ }
1200
+ addEntry(baseProtocol === "https:" ? "https" : "http", label, fallbackUrl, { qr: true, meta: entryMeta })
1201
+ })
967
1202
  }
968
1203
  }
969
1204
  }
@@ -1281,7 +1516,8 @@
1281
1516
  item.setAttribute("data-url", entry.url)
1282
1517
  const labelSpan = document.createElement("span")
1283
1518
  labelSpan.className = "label"
1284
- labelSpan.textContent = entry.label
1519
+ const labelText = entry && entry.badge ? `${entry.label} (${entry.badge})` : entry.label
1520
+ labelSpan.textContent = labelText
1285
1521
  const valueSpan = document.createElement("span")
1286
1522
  valueSpan.className = "value"
1287
1523
  valueSpan.textContent = entry.display
@@ -518,16 +518,8 @@ body.dark .plugin-option:hover {
518
518
  <a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
519
519
  <a class='tab selected' href="/agents"><i class="fa-solid fa-robot"></i><div class='caption'>Agents</div></a>
520
520
  <a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
521
- <% if (typeof peer_qr !== 'undefined' && peer_qr) { %>
522
- <div class='qr' style='padding:12px 10px; text-align:center;'>
523
- <a href="<%=peer_url%>" target="_blank" style="text-decoration:none; color:inherit; display:block;">
524
- <img src="<%=peer_qr%>" alt="Open <%=peer_url%>" style="width:128px; height:128px; image-rendering: pixelated;"/>
525
- <div class='caption'>Scan to open</div>
526
- <div class='caption' style='font-size:10px; opacity:0.7;'><%=peer_url%></div>
527
- </a>
528
- </div>
529
- <% } %>
530
- </aside>
521
+ <%- include('partials/peer_access_points', { peer_access_points, peer_url, peer_qr }) %>
522
+ </aside>
531
523
  </main>
532
524
  <script type="application/json" id="plugin-data"><%- JSON.stringify(serializedPlugins) %></script>
533
525
  <script type="application/json" id="plugin-app-data"><%- JSON.stringify(apps) %></script>
@@ -2982,10 +2982,10 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2982
2982
  <button class='btn2' id='inspector' data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
2983
2983
  <div class='sep'></div>
2984
2984
  <div class='mode-selector'>
2985
- <a class="btn2 <%=type === 'review' ? 'selected' : ''%>" href="<%=review_tab%>"><div><i class="fa-regular fa-message"></i></div><div class='caption'>Forum</div></a>
2986
- <a class="btn2 <%=type === 'files' ? 'selected' : ''%>" href="<%=files_tab%>"><div><i class="fa-solid fa-file-lines"></i></div><div class='caption'>Files</div></a>
2987
- <a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div class='caption'>Dev</div></a>
2988
2985
  <a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div class='caption'>Run</div></a>
2986
+ <a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div class='caption'>Dev</div></a>
2987
+ <a class="btn2 <%=type === 'files' ? 'selected' : ''%>" href="<%=files_tab%>"><div><i class="fa-solid fa-file-lines"></i></div><div class='caption'>Files</div></a>
2988
+ <a class="btn2 <%=type === 'review' ? 'selected' : ''%>" href="<%=review_tab%>"><div><i class="fa-regular fa-message"></i></div><div class='caption'>Forum</div></a>
2989
2989
  </div>
2990
2990
  <a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
2991
2991
  <div><i class="fa-solid fa-table-columns"></i></div>
@@ -925,15 +925,7 @@ document.addEventListener('DOMContentLoaded', function() {
925
925
  <a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
926
926
  <a class='tab' href="/agents"><i class="fa-solid fa-robot"></i><div class='caption'>Agents</div></a>
927
927
  <a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
928
- <% if (typeof peer_qr !== 'undefined' && peer_qr) { %>
929
- <div class='qr' style='padding:12px 10px; text-align:center;'>
930
- <a href="<%=peer_url%>" target="_blank" style="text-decoration:none; color:inherit; display:block;">
931
- <img src="<%=peer_qr%>" alt="Open <%=peer_url%>" style="width:128px; height:128px; image-rendering: pixelated;"/>
932
- <div class='caption'>Scan to open</div>
933
- <div class='caption' style='font-size:10px; opacity:0.7;'><%=peer_url%></div>
934
- </a>
935
- </div>
936
- <% } %>
928
+ <%- include('partials/peer_access_points', { peer_access_points, peer_url, peer_qr }) %>
937
929
  </aside>
938
930
  </main>
939
931
  </body>