pinokiod 7.0.1 → 7.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.0.1",
3
+ "version": "7.0.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -113,7 +113,7 @@ Follow these sections in order:
113
113
  - `running`: script is running
114
114
  - `ready`: app is reachable/ready
115
115
  - `ready_url`: default base URL for API calls when available
116
- - `external_ready_url`: optional non-loopback app URL, usually a LAN-accessible or otherwise externally reachable address exposed by Pinokio; use it only when `ready_url` fails due to loopback restrictions
116
+ - `external_ready_urls`: optional ordered non-loopback app URLs for caller-side access; use them only when `ready_url` fails due to loopback restrictions
117
117
  - `state`: `offline | starting | online`
118
118
  - `source`: machine identity for federated results
119
119
  - Use `--probe` only for readiness confirmation before first API call (or when status is uncertain).
@@ -121,7 +121,7 @@ Follow these sections in order:
121
121
  - Treat `offline` as expected before first run.
122
122
  - If `app_id` contains `@<host>` or `source.local=false`, the app is remote:
123
123
  - treat `path` and `ready_url` as source-local fields, not caller-usable local paths/URLs
124
- - use `external_ready_url` for caller-side API access when available
124
+ - use `external_ready_urls` in order for caller-side API access when available
125
125
  - do not assume `pterm run <path>` can launch a remote app from the current machine
126
126
  - If app is offline or not ready, run it:
127
127
  - Run `pterm run <app_path>`.
@@ -132,8 +132,8 @@ Follow these sections in order:
132
132
  - Success criteria:
133
133
  - `state=online` and `ready=true`
134
134
  - use `ready_url` by default
135
- - if `ready_url` fails because the client cannot access loopback and `external_ready_url` exists, use `external_ready_url`
136
- - missing `external_ready_url` is normal; it usually means network sharing is off
135
+ - if `ready_url` fails because the client cannot access loopback and `external_ready_urls` exists, try those URLs in order
136
+ - missing `external_ready_urls` is normal; it usually means network sharing is off
137
137
  - Failure criteria:
138
138
  - timeout before success
139
139
  - app drops back to `offline` during startup after a run attempt
@@ -174,7 +174,7 @@ Follow these sections in order:
174
174
  - Prefer returning full logs over brittle deterministic error parsing.
175
175
  - REST endpoints may be used for diagnostics only when pterm is unavailable; do not claim full install/launch lifecycle completion without compatible pterm commands.
176
176
  - Do not keep searching after app selection; move to status/run.
177
- - Do not assume `external_ready_url` exists; localhost-only apps are normal.
177
+ - Do not assume `external_ready_urls` exists; localhost-only apps are normal.
178
178
  - Do not conflate loopback access failure, sandbox denial, or missing permission with "Pinokio is not running" or "`pterm` is not installed."
179
179
  - On `pterm` permission failure, prefer asking for permission over asking the user to manually run commands.
180
180
  - If `pterm` exists locally but cannot reach the control plane, explicitly tell the user this looks like a client permission/sandbox issue.
@@ -70,7 +70,9 @@ class AppRegistryService {
70
70
  seen.add(signature)
71
71
  entries.push({
72
72
  host: String(externalHost.host).trim(),
73
- port: Number.parseInt(String(externalHost.port), 10)
73
+ port: Number.parseInt(String(externalHost.port), 10),
74
+ scope: externalHost.scope || null,
75
+ interface: externalHost.interface || null
74
76
  })
75
77
  }
76
78
  if (item.external_ip && typeof item.external_ip === 'string') {
@@ -81,7 +83,9 @@ class AppRegistryService {
81
83
  seen.add(signature)
82
84
  entries.push({
83
85
  host: parsed.hostname,
84
- port: Number.parseInt(parsed.port, 10)
86
+ port: Number.parseInt(parsed.port, 10),
87
+ scope: null,
88
+ interface: null
85
89
  })
86
90
  }
87
91
  } catch (_) {}
@@ -90,68 +94,88 @@ class AppRegistryService {
90
94
  return entries
91
95
  }
92
96
 
93
- buildExternalReadyUrl(url, source = null) {
97
+ sortExternalHostEntries(entries = [], source = null) {
98
+ const sourceInfo = this.normalizeSource(source)
99
+ const scopedEntries = Array.isArray(entries) ? entries.slice() : []
100
+ const scopeRank = (scope = '') => {
101
+ switch (String(scope || '').toLowerCase()) {
102
+ case 'lan':
103
+ return 0
104
+ case 'cgnat':
105
+ return 1
106
+ default:
107
+ return 2
108
+ }
109
+ }
110
+ return scopedEntries.sort((a, b) => {
111
+ const aHost = String(a && a.host ? a.host : '').toLowerCase()
112
+ const bHost = String(b && b.host ? b.host : '').toLowerCase()
113
+ const aMatchesCaller = sourceInfo.hostname && aHost === sourceInfo.hostname ? 1 : 0
114
+ const bMatchesCaller = sourceInfo.hostname && bHost === sourceInfo.hostname ? 1 : 0
115
+ if (aMatchesCaller !== bMatchesCaller) {
116
+ return bMatchesCaller - aMatchesCaller
117
+ }
118
+ const aScope = scopeRank(a && a.scope)
119
+ const bScope = scopeRank(b && b.scope)
120
+ if (aScope !== bScope) {
121
+ return aScope - bScope
122
+ }
123
+ return `${aHost}:${a && a.port ? a.port : ''}`.localeCompare(`${bHost}:${b && b.port ? b.port : ''}`)
124
+ })
125
+ }
126
+
127
+ buildExternalReadyUrls(url, source = null) {
94
128
  if (!url || typeof url !== 'string') {
95
- return null
129
+ return []
96
130
  }
97
131
  const originalUrl = String(url)
98
132
  let parsed
99
133
  try {
100
134
  parsed = new URL(originalUrl)
101
135
  } catch (_) {
102
- return null
136
+ return []
103
137
  }
104
138
  const originalProtocol = parsed.protocol === 'https:' ? 'https:' : 'http:'
105
139
  const hostname = (parsed.hostname || '').trim().toLowerCase()
106
- if (!hostname) {
107
- return null
108
- }
109
- if (!this.isLoopbackHostname(hostname)) {
110
- let result = parsed.toString()
111
- if (/^https?:\/\/[^/?#]+$/i.test(originalUrl) && parsed.pathname === '/' && !parsed.search && !parsed.hash) {
112
- result = result.replace(/\/$/, '')
113
- }
114
- return result
140
+ if (!hostname || !this.isLoopbackHostname(hostname)) {
141
+ return []
115
142
  }
116
143
 
117
- const sourceInfo = this.normalizeSource(source)
118
- const externalEntries = this.findExternalHostEntries(parsed.port)
119
- let nextHost = ''
120
- let nextPort = Number.parseInt(parsed.port, 10)
121
-
122
- if (externalEntries.length > 0) {
123
- const matchingEntry = sourceInfo.hostname
124
- ? externalEntries.find((entry) => entry.host === sourceInfo.hostname)
125
- : null
126
- const selectedEntry = matchingEntry || externalEntries[0]
127
- nextHost = selectedEntry.host
128
- nextPort = selectedEntry.port
129
- } else {
130
- const mappedPort = this.kernel && this.kernel.router && this.kernel.router.port_mapping
131
- ? this.kernel.router.port_mapping[String(parsed.port)]
132
- : null
133
- if (sourceInfo.hostname && !this.isLoopbackHostname(sourceInfo.hostname)) {
134
- nextHost = sourceInfo.hostname
135
- } else if (this.kernel && this.kernel.peer && this.kernel.peer.host) {
136
- nextHost = String(this.kernel.peer.host).trim()
144
+ const externalEntries = this.sortExternalHostEntries(this.findExternalHostEntries(parsed.port), source)
145
+ const results = []
146
+ const seen = new Set()
147
+ for (const entry of externalEntries) {
148
+ if (!entry || !entry.host || !entry.port) {
149
+ continue
137
150
  }
138
- if (mappedPort) {
139
- nextPort = Number.parseInt(String(mappedPort), 10)
151
+ const next = new URL(parsed.toString())
152
+ next.protocol = originalProtocol
153
+ next.hostname = entry.host
154
+ next.port = String(entry.port)
155
+ let nextUrl = next.toString()
156
+ if (/^https?:\/\/[^/?#]+$/i.test(originalUrl) && next.pathname === '/' && !next.search && !next.hash) {
157
+ nextUrl = nextUrl.replace(/\/$/, '')
140
158
  }
159
+ if (seen.has(nextUrl)) {
160
+ continue
161
+ }
162
+ seen.add(nextUrl)
163
+ const item = {
164
+ url: nextUrl,
165
+ transport: 'ip',
166
+ scope: entry.scope || 'unknown'
167
+ }
168
+ if (entry.interface) {
169
+ item.interface = entry.interface
170
+ }
171
+ results.push(item)
141
172
  }
173
+ return results
174
+ }
142
175
 
143
- if (!nextHost || !Number.isFinite(nextPort) || nextPort <= 0) {
144
- return null
145
- }
146
-
147
- parsed.protocol = originalProtocol
148
- parsed.hostname = nextHost
149
- parsed.port = String(nextPort)
150
- let result = parsed.toString()
151
- if (/^https?:\/\/[^/?#]+$/i.test(originalUrl) && parsed.pathname === '/' && !parsed.search && !parsed.hash) {
152
- result = result.replace(/\/$/, '')
153
- }
154
- return result
176
+ buildExternalReadyUrl(url, source = null) {
177
+ const externalReadyUrls = this.buildExternalReadyUrls(url, source)
178
+ return externalReadyUrls.length > 0 ? externalReadyUrls[0].url : null
155
179
  }
156
180
 
157
181
  isPathWithin(parentPath, childPath) {
@@ -241,7 +265,7 @@ class AppRegistryService {
241
265
  ready: false,
242
266
  state: 'offline',
243
267
  ready_url: null,
244
- external_ready_url: null,
268
+ external_ready_urls: [],
245
269
  ready_script: null,
246
270
  running_scripts: [],
247
271
  local_entries: []
@@ -419,7 +443,7 @@ class AppRegistryService {
419
443
  }
420
444
 
421
445
  const runtime = this.collectAppRuntime(appRoot)
422
- runtime.external_ready_url = this.buildExternalReadyUrl(runtime.ready_url, options.source || null)
446
+ runtime.external_ready_urls = this.buildExternalReadyUrls(runtime.ready_url, options.source || null)
423
447
  const installScript = await this.firstExistingScript(appRoot, ['install.js', 'install.json'])
424
448
  const startScript = await this.firstExistingScript(appRoot, ['start.js', 'start.json'])
425
449
  let defaultTarget = null
@@ -457,7 +481,7 @@ class AppRegistryService {
457
481
  running: runtime.running,
458
482
  ready,
459
483
  ready_url: runtime.ready_url,
460
- external_ready_url: runtime.external_ready_url,
484
+ external_ready_urls: runtime.external_ready_urls,
461
485
  state,
462
486
  running_scripts: runtime.running_scripts,
463
487
  ready_script: runtime.ready_script,
@@ -515,14 +515,14 @@ class AppSearchService {
515
515
  decorateAppWithRuntime(app, extras = {}, source = null) {
516
516
  const appRoot = this.kernel.path('api', app.name)
517
517
  const runtime = this.registry.collectAppRuntime(appRoot)
518
- const externalReadyUrl = this.registry.buildExternalReadyUrl(runtime.ready_url, source)
518
+ const externalReadyUrls = this.registry.buildExternalReadyUrls(runtime.ready_url, source)
519
519
  return {
520
520
  app_id: app.name,
521
521
  ...app,
522
522
  running: runtime.running,
523
523
  ready: runtime.ready,
524
524
  ready_url: runtime.ready_url,
525
- external_ready_url: externalReadyUrl,
525
+ external_ready_urls: externalReadyUrls,
526
526
  state: runtime.state,
527
527
  ...extras
528
528
  }