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
|
@@ -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
|
-
- `
|
|
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 `
|
|
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 `
|
|
136
|
-
- missing `
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
package/server/lib/app_search.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
525
|
+
external_ready_urls: externalReadyUrls,
|
|
526
526
|
state: runtime.state,
|
|
527
527
|
...extras
|
|
528
528
|
}
|