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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.20",
3
+ "version": "8.0.22",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- if (
3018
- (!frame || frame.tagName !== 'IFRAME') &&
3019
- new URLSearchParams(window.location.search).get('mode') === 'explore' &&
3020
- event.origin === 'https://pinokio.co'
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
- const lanExplore =
3051
- new URLSearchParams(window.location.search).get('mode') === 'explore' &&
3052
- !['localhost', '127.0.0.1', '::1', '[::1]'].includes(window.location.hostname) &&
3053
- event.origin === 'https://pinokio.co' && event.source === frame.contentWindow;
3054
- if (lanExplore && localInstall) {
3055
- target = new URL(`${target.pathname}${target.search}${target.hash}`, window.location.origin);
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(url = "http://192.168.1.50:42000/home?mode=explore") {
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("<!doctype html><body><main><iframe></iframe></main></body>", {
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 = "https://pinokio.co/?embed=1&theme=dark"
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 rebases registry download installs onto the Home Server origin", async (t) => {
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(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev")
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
- assert.equal(
66
- frame.src,
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
- assert.equal(
81
- frame.src,
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
- assert.equal(
94
- frame.src,
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" }