pinokiod 8.0.20 → 8.0.22
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/api/index.js
CHANGED
|
@@ -1887,6 +1887,9 @@ class Api {
|
|
|
1887
1887
|
}
|
|
1888
1888
|
} else {
|
|
1889
1889
|
let { cwd, script } = await this.resolveScript(request.path)
|
|
1890
|
+
if (!request.cwd && this.isPathInsideRoot(cwd, PluginSources.systemPluginRoot(this.kernel))) {
|
|
1891
|
+
request.cwd = this.kernel.path("bin")
|
|
1892
|
+
}
|
|
1890
1893
|
|
|
1891
1894
|
if (!script) {
|
|
1892
1895
|
this.ondata({
|
package/package.json
CHANGED
package/server/public/common.js
CHANGED
|
@@ -3013,15 +3013,19 @@ if (typeof window !== 'undefined' && !window.__pinokioNavigateListenerInstalled)
|
|
|
3013
3013
|
return;
|
|
3014
3014
|
}
|
|
3015
3015
|
}
|
|
3016
|
+
// Only the registry iframe rendered by the Explore page uses this stable name.
|
|
3017
|
+
const exploreFrame = document.querySelector('iframe[name="pinokio-explore"]');
|
|
3018
|
+
// Trust the message only when it comes from that exact iframe and its configured origin.
|
|
3019
|
+
const fromExplore = Boolean(
|
|
3020
|
+
exploreFrame &&
|
|
3021
|
+
event.source === exploreFrame.contentWindow &&
|
|
3022
|
+
event.origin === new URL(exploreFrame.src).origin
|
|
3023
|
+
);
|
|
3016
3024
|
let frame = document.activeElement;
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
) {
|
|
3022
|
-
const exploreFrame = document.querySelector('main > iframe');
|
|
3023
|
-
if (exploreFrame && event.source === exploreFrame.contentWindow) frame = exploreFrame;
|
|
3024
|
-
}
|
|
3025
|
+
// Mobile Safari can leave the parent body focused after a tap inside the registry iframe.
|
|
3026
|
+
const exploreFallback = (!frame || frame.tagName !== 'IFRAME') && fromExplore;
|
|
3027
|
+
if (exploreFallback) frame = exploreFrame;
|
|
3028
|
+
// Preserve the existing generic behavior when there is no identifiable sending iframe.
|
|
3025
3029
|
if (!frame || frame.tagName !== 'IFRAME') {
|
|
3026
3030
|
try {
|
|
3027
3031
|
console.warn('[pinokio:navigate] no active iframe');
|
|
@@ -3037,23 +3041,31 @@ if (typeof window !== 'undefined' && !window.__pinokioNavigateListenerInstalled)
|
|
|
3037
3041
|
} catch (_) {}
|
|
3038
3042
|
return;
|
|
3039
3043
|
}
|
|
3044
|
+
// Only translate the two local Pinokio actions emitted by the registry.
|
|
3040
3045
|
const localInstall =
|
|
3041
3046
|
target.origin === 'http://localhost:42000' &&
|
|
3042
3047
|
(
|
|
3048
|
+
// A normal launcher install must include the repository URI.
|
|
3043
3049
|
(target.pathname === '/pinokio/download' && Boolean(target.searchParams.get('uri'))) ||
|
|
3044
3050
|
(
|
|
3051
|
+
// A snapshot restore must include its registry and a canonical SHA-256 hash.
|
|
3045
3052
|
target.pathname === '/checkpoints' &&
|
|
3046
3053
|
Boolean(target.searchParams.get('registry')) &&
|
|
3047
3054
|
/^(?:sha256:)?[0-9a-f]{64}$/i.test(target.searchParams.get('hash') || '')
|
|
3048
3055
|
)
|
|
3049
3056
|
);
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3057
|
+
// Handle trusted Explore installs here so Pinokio replaces the parent page, not the iframe.
|
|
3058
|
+
if (fromExplore && localInstall) {
|
|
3059
|
+
// LAN/Home Server clients must use the Pinokio origin they can actually reach.
|
|
3060
|
+
if (target.origin !== window.location.origin) {
|
|
3061
|
+
target = new URL(`${target.pathname}${target.search}${target.hash}`, window.location.origin);
|
|
3062
|
+
}
|
|
3063
|
+
window.location.href = target.toString();
|
|
3064
|
+
return;
|
|
3056
3065
|
}
|
|
3066
|
+
// If focus was recovered through the Explore fallback, reject every non-allowlisted action.
|
|
3067
|
+
if (exploreFallback) return;
|
|
3068
|
+
// Keep the existing generic iframe navigation restricted to the current Pinokio origin.
|
|
3057
3069
|
if (target.origin !== window.location.origin) {
|
|
3058
3070
|
try {
|
|
3059
3071
|
console.warn('[pinokio:navigate] blocked origin', target.origin);
|
|
@@ -7,18 +7,34 @@ const { JSDOM, VirtualConsole } = require("jsdom")
|
|
|
7
7
|
const commonScriptPath = path.resolve(__dirname, "../server/public/common.js")
|
|
8
8
|
const exploreTemplatePath = path.resolve(__dirname, "../server/views/explore.ejs")
|
|
9
9
|
|
|
10
|
-
async function createHarness(
|
|
10
|
+
async function createHarness(
|
|
11
|
+
url = "http://192.168.1.50:42000/home?mode=explore",
|
|
12
|
+
frameUrl = "https://pinokio.co/?embed=1&theme=dark",
|
|
13
|
+
) {
|
|
11
14
|
const source = await fs.readFile(commonScriptPath, "utf8")
|
|
12
15
|
const virtualConsole = new VirtualConsole()
|
|
16
|
+
const navigationErrors = []
|
|
13
17
|
virtualConsole.on("jsdomError", (error) => {
|
|
18
|
+
if (error && /Not implemented: navigation/.test(error.message || "")) {
|
|
19
|
+
navigationErrors.push(error)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
14
22
|
throw error
|
|
15
23
|
})
|
|
16
|
-
const dom = new JSDOM(
|
|
24
|
+
const dom = new JSDOM('<!doctype html><body><main><iframe name="pinokio-explore"></iframe></main></body>', {
|
|
17
25
|
runScripts: "dangerously",
|
|
18
26
|
pretendToBeVisual: true,
|
|
19
27
|
url,
|
|
20
28
|
virtualConsole,
|
|
21
29
|
beforeParse(window) {
|
|
30
|
+
const NativeURL = window.URL
|
|
31
|
+
window.__pinokioConstructedUrls = []
|
|
32
|
+
window.URL = class extends NativeURL {
|
|
33
|
+
constructor(value, base) {
|
|
34
|
+
super(value, base)
|
|
35
|
+
window.__pinokioConstructedUrls.push(this.toString())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
22
38
|
window.fetch = async () => ({ ok: true, json: async () => ({}) })
|
|
23
39
|
window.matchMedia = () => ({
|
|
24
40
|
matches: false,
|
|
@@ -35,13 +51,14 @@ async function createHarness(url = "http://192.168.1.50:42000/home?mode=explore"
|
|
|
35
51
|
script.textContent = source
|
|
36
52
|
dom.window.document.body.appendChild(script)
|
|
37
53
|
const frame = dom.window.document.querySelector("iframe")
|
|
38
|
-
frame.src =
|
|
54
|
+
frame.src = frameUrl
|
|
39
55
|
frame.focus()
|
|
40
56
|
assert.equal(dom.window.document.activeElement, frame)
|
|
41
|
-
return { dom, frame }
|
|
57
|
+
return { dom, frame, navigationErrors }
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
function navigate(dom, frame, target, options = {}) {
|
|
61
|
+
dom.window.__pinokioConstructedUrls.length = 0
|
|
45
62
|
dom.window.dispatchEvent(new dom.window.MessageEvent("message", {
|
|
46
63
|
data: { e: "pinokio:navigate", url: target },
|
|
47
64
|
origin: options.origin || "https://pinokio.co",
|
|
@@ -49,6 +66,12 @@ function navigate(dom, frame, target, options = {}) {
|
|
|
49
66
|
}))
|
|
50
67
|
}
|
|
51
68
|
|
|
69
|
+
function assertParentNavigation(dom, frame, navigationErrors, originalFrameUrl, expectedUrl) {
|
|
70
|
+
assert.ok(dom.window.__pinokioConstructedUrls.includes(expectedUrl))
|
|
71
|
+
assert.equal(navigationErrors.length, 1)
|
|
72
|
+
assert.equal(frame.src, originalFrameUrl)
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
test("Explore uses a stable frame identity instead of the launcher schema", async () => {
|
|
53
76
|
const source = await fs.readFile(exploreTemplatePath, "utf8")
|
|
54
77
|
|
|
@@ -56,46 +79,77 @@ test("Explore uses a stable frame identity instead of the launcher schema", asyn
|
|
|
56
79
|
assert.doesNotMatch(source, /name="<%=schema%>"/)
|
|
57
80
|
})
|
|
58
81
|
|
|
59
|
-
test("LAN Explore
|
|
60
|
-
const { dom, frame } = await createHarness(
|
|
82
|
+
test("LAN Explore trusts the configured registry origin", async (t) => {
|
|
83
|
+
const { dom, frame, navigationErrors } = await createHarness(
|
|
84
|
+
undefined,
|
|
85
|
+
"https://beta.pinokio.co/?embed=1&theme=dark",
|
|
86
|
+
)
|
|
61
87
|
t.after(() => dom.window.close())
|
|
88
|
+
const originalFrameUrl = frame.src
|
|
62
89
|
|
|
63
|
-
navigate(
|
|
90
|
+
navigate(
|
|
91
|
+
dom,
|
|
92
|
+
frame,
|
|
93
|
+
"http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev",
|
|
94
|
+
{ origin: "https://beta.pinokio.co" },
|
|
95
|
+
)
|
|
64
96
|
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
assertParentNavigation(
|
|
98
|
+
dom,
|
|
99
|
+
frame,
|
|
100
|
+
navigationErrors,
|
|
101
|
+
originalFrameUrl,
|
|
67
102
|
"http://192.168.1.50:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev",
|
|
68
103
|
)
|
|
69
104
|
})
|
|
70
105
|
|
|
71
106
|
test("LAN Explore handles registry installs when mobile Safari leaves the parent body focused", async (t) => {
|
|
72
|
-
const { dom, frame } = await createHarness()
|
|
107
|
+
const { dom, frame, navigationErrors } = await createHarness()
|
|
73
108
|
t.after(() => dom.window.close())
|
|
109
|
+
const originalFrameUrl = frame.src
|
|
74
110
|
dom.window.document.body.tabIndex = -1
|
|
75
111
|
dom.window.document.body.focus()
|
|
76
112
|
assert.equal(dom.window.document.activeElement, dom.window.document.body)
|
|
77
113
|
|
|
78
114
|
navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api")
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
|
|
116
|
+
assertParentNavigation(
|
|
117
|
+
dom,
|
|
118
|
+
frame,
|
|
119
|
+
navigationErrors,
|
|
120
|
+
originalFrameUrl,
|
|
82
121
|
"http://192.168.1.50:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api",
|
|
83
122
|
)
|
|
84
123
|
})
|
|
85
124
|
|
|
86
125
|
test("LAN Explore rebases valid checkpoint installs onto the Home Server origin", async (t) => {
|
|
87
|
-
const { dom, frame } = await createHarness()
|
|
126
|
+
const { dom, frame, navigationErrors } = await createHarness()
|
|
88
127
|
t.after(() => dom.window.close())
|
|
128
|
+
const originalFrameUrl = frame.src
|
|
89
129
|
const hash = `sha256%3A${"a".repeat(64)}`
|
|
90
130
|
|
|
91
131
|
navigate(dom, frame, `http://localhost:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=${hash}&path=api`)
|
|
92
132
|
|
|
93
|
-
|
|
94
|
-
|
|
133
|
+
assertParentNavigation(
|
|
134
|
+
dom,
|
|
135
|
+
frame,
|
|
136
|
+
navigationErrors,
|
|
137
|
+
originalFrameUrl,
|
|
95
138
|
`http://192.168.1.50:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=${hash}&path=api`,
|
|
96
139
|
)
|
|
97
140
|
})
|
|
98
141
|
|
|
142
|
+
test("localhost Explore replaces itself for registry installs instead of nesting Pinokio", async (t) => {
|
|
143
|
+
const { dom, frame, navigationErrors } = await createHarness("http://localhost:42000/home?mode=explore")
|
|
144
|
+
t.after(() => dom.window.close())
|
|
145
|
+
const originalFrameUrl = frame.src
|
|
146
|
+
const target = "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api"
|
|
147
|
+
|
|
148
|
+
navigate(dom, frame, target)
|
|
149
|
+
|
|
150
|
+
assertParentNavigation(dom, frame, navigationErrors, originalFrameUrl, target)
|
|
151
|
+
})
|
|
152
|
+
|
|
99
153
|
const blockedCases = [
|
|
100
154
|
["unrelated local route", "http://localhost:42000/tasker?vars=url", {}],
|
|
101
155
|
["download without a URI", "http://localhost:42000/pinokio/download", {}],
|
|
@@ -237,6 +237,34 @@ test("resolveActionSteps links system plugin setup prompts to plugin detail", as
|
|
|
237
237
|
assert.equal(steps[0].params.target, "_parent")
|
|
238
238
|
})
|
|
239
239
|
|
|
240
|
+
test("process uses kernel bin as a bundled plugin's implicit cwd", async () => {
|
|
241
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-plugin-cwd-"))
|
|
242
|
+
try {
|
|
243
|
+
const virtualSystemRoot = path.join(root, "app.asar", "node_modules", "pinokiod", "system")
|
|
244
|
+
const virtualPluginDir = path.join(virtualSystemRoot, "plugin", "demo")
|
|
245
|
+
|
|
246
|
+
const kernel = createKernel()
|
|
247
|
+
kernel.homedir = path.join(root, "pinokio")
|
|
248
|
+
kernel.path = (...parts) => path.join(kernel.homedir, ...parts)
|
|
249
|
+
kernel.shell = { init: async () => {} }
|
|
250
|
+
kernel.systemPath = (...parts) => path.join(virtualSystemRoot, ...parts)
|
|
251
|
+
const api = new Api(kernel)
|
|
252
|
+
const request = { uri: path.join(virtualPluginDir, "pinokio.js") }
|
|
253
|
+
api.logSessions = null
|
|
254
|
+
api.resolveScript = async () => ({
|
|
255
|
+
cwd: virtualPluginDir,
|
|
256
|
+
script: { run: [{ method: "shell.run", params: { message: "install" } }] },
|
|
257
|
+
})
|
|
258
|
+
api.queue = () => {}
|
|
259
|
+
|
|
260
|
+
await api.process(request, () => {})
|
|
261
|
+
|
|
262
|
+
assert.equal(request.cwd, kernel.path("bin"))
|
|
263
|
+
} finally {
|
|
264
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
240
268
|
test("resolveActionSteps runs plugin action directly when installed check passes", async () => {
|
|
241
269
|
const api = new Api(createKernel())
|
|
242
270
|
const request = { id: "plugin-run-installed", path: "/pinokio/plugin/demo/pinokio.js" }
|