pinokiod 8.0.21 → 8.0.23

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": "8.0.21",
3
+ "version": "8.0.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -3606,7 +3606,6 @@ class Server {
3606
3606
 
3607
3607
  let error
3608
3608
  let items
3609
- let readme
3610
3609
  // if (pathComponents.length === 0) {
3611
3610
  // let files = await fs.promises.readdir(filepath, { withFileTypes: true })
3612
3611
  // items = files
@@ -3629,18 +3628,8 @@ class Server {
3629
3628
  }
3630
3629
  }
3631
3630
 
3632
- // look for README.md
3633
-
3634
3631
  let config
3635
3632
  for(let file of f.files) {
3636
- if (file.name.toLowerCase() === "readme.md") {
3637
- let p = path.resolve(filepath, file.name)
3638
- let md = await fs.promises.readFile(p, "utf8")
3639
- readme = marked.parse(md, {
3640
- baseUrl: req._parsedUrl.pathname.replace(/^\/_api/, "/raw/") + "/"
3641
- //baseUrl: req.originalUrl + "/"
3642
- })
3643
- }
3644
3633
  if (file.name === "pinokio.js") {
3645
3634
  let p = path.resolve(filepath, file.name)
3646
3635
  config = (await this.kernel.loader.load(p)).resolved
@@ -4297,7 +4286,6 @@ class Server {
4297
4286
  home_sort: homeSortMode,
4298
4287
  running,
4299
4288
  notRunning,
4300
- readme,
4301
4289
  filepath,
4302
4290
  mode: null,
4303
4291
  kernel: this.kernel,
@@ -4329,7 +4317,6 @@ class Server {
4329
4317
  ishome: meta,
4330
4318
  running,
4331
4319
  notRunning,
4332
- readme,
4333
4320
  filepath,
4334
4321
  mode: null,
4335
4322
  kernel: this.kernel,
@@ -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);
@@ -2637,13 +2637,6 @@ pre {
2637
2637
  pre:empty {
2638
2638
  display: none;
2639
2639
  }
2640
- body.dark .readme {
2641
- background: none;
2642
- color: var(--dark-color);
2643
- }
2644
- .readme .content {
2645
- padding: 0 20px 20px;
2646
- }
2647
2640
  body.dark .empty {
2648
2641
  background: var(--dark-bg);
2649
2642
  }
@@ -2653,21 +2646,6 @@ body.dark .empty {
2653
2646
  opacity: 0.5;
2654
2647
  background: var(--light-bg);
2655
2648
  }
2656
- .readme {
2657
- max-width: var(--content-width);
2658
- box-sizing: border-box;
2659
- margin: 0 auto;
2660
- }
2661
- body.dark .readme a {
2662
- color: var(--dark-link-color);
2663
- }
2664
- .readme a {
2665
- color: var(--light-link-color);
2666
- text-decoration: none;
2667
- }
2668
- .readme ol, .readme ul {
2669
- padding-inline-start: 20px;
2670
- }
2671
2649
  .runnables .row {
2672
2650
  display: flex;
2673
2651
  padding: 10px;
@@ -8,7 +8,6 @@
8
8
  <link href="/css/solid.min.css" rel="stylesheet">
9
9
  <link href="/css/regular.min.css" rel="stylesheet">
10
10
  <link href="/css/brands.min.css" rel="stylesheet">
11
- <link href="/markdown.css" rel="stylesheet"/>
12
11
  <link href="/noty.css" rel="stylesheet"/>
13
12
  <link href="/style.css" rel="stylesheet"/>
14
13
  <% if (agent === "electron") { %>
@@ -230,11 +229,6 @@ body.dark .browser-options-row {
230
229
  <div class='description'><%=item.description%></div>
231
230
  </a>
232
231
  <% }) %>
233
- <% if (readme) { %>
234
- <div class='readme markdown-body'>
235
- <div class='content'><%-readme%></div>
236
- </div>
237
- <% } %>
238
232
  </div>
239
233
  </main>
240
234
  <script>
@@ -8,7 +8,6 @@
8
8
  <link href="/css/solid.min.css" rel="stylesheet">
9
9
  <link href="/css/regular.min.css" rel="stylesheet">
10
10
  <link href="/css/brands.min.css" rel="stylesheet">
11
- <link href="/markdown.css" rel="stylesheet"/>
12
11
  <link href="/noty.css" rel="stylesheet"/>
13
12
  <link href="/filepond.min.css" rel="stylesheet" />
14
13
  <link href="/filepond-plugin-image-preview.min.css" rel="stylesheet" />
@@ -2000,11 +1999,6 @@ body.dark aside .current.selected {
2000
1999
  </a>
2001
2000
  <% }) %>
2002
2001
  <% } %>
2003
- <% if (readme) { %>
2004
- <div class='readme markdown-body'>
2005
- <div class='content'><%-readme%></div>
2006
- </div>
2007
- <% } %>
2008
2002
  </div>
2009
2003
  <%- include('partials/main_sidebar', { selected: 'home' }) %>
2010
2004
  </main>
@@ -8,7 +8,6 @@
8
8
  <link href="/css/solid.min.css" rel="stylesheet">
9
9
  <link href="/css/regular.min.css" rel="stylesheet">
10
10
  <link href="/css/brands.min.css" rel="stylesheet">
11
- <link href="/markdown.css" rel="stylesheet"/>
12
11
  <link href="/noty.css" rel="stylesheet"/>
13
12
  <link href="/filepond.min.css" rel="stylesheet" />
14
13
  <link href="/filepond-plugin-image-preview.min.css" rel="stylesheet" />
@@ -500,11 +499,6 @@ body.dark .open-menu, body.dark .browse {
500
499
  </a>
501
500
  <% }) %>
502
501
  <% } %>
503
- <% if (readme) { %>
504
- <div class='readme markdown-body'>
505
- <div class='content'><%-readme%></div>
506
- </div>
507
- <% } %>
508
502
  <% if (display.includes("onboarding")) { %>
509
503
  <!--
510
504
  <div class='container'>
@@ -0,0 +1,125 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs')
3
+ const http = require('node:http')
4
+ const os = require('node:os')
5
+ const path = require('node:path')
6
+ const test = require('node:test')
7
+ const express = require('express')
8
+ const Server = require('../server')
9
+
10
+ const viewsPath = path.resolve(__dirname, '..', 'server', 'views')
11
+
12
+ async function createDirectoryRendererFixture() {
13
+ const homedir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pinokio-directory-readme-'))
14
+ const apiRoot = path.resolve(homedir, 'api')
15
+ const appRoot = path.resolve(apiRoot, 'demo')
16
+ await fs.promises.mkdir(appRoot, { recursive: true })
17
+ await fs.promises.writeFile(path.resolve(apiRoot, 'README.md'), '# ROOT README SENTINEL\n')
18
+ await fs.promises.writeFile(path.resolve(appRoot, 'README.md'), '# APP README SENTINEL\n')
19
+ await fs.promises.writeFile(path.resolve(appRoot, 'keep.txt'), 'keep\n')
20
+
21
+ const renderer = Object.create(Server.prototype)
22
+ renderer.kernel = {
23
+ homedir,
24
+ path: (...segments) => path.resolve(homedir, ...segments),
25
+ api: {
26
+ userdir: apiRoot,
27
+ proxies: {},
28
+ running: {},
29
+ launcher: async (name) => ({
30
+ script: null,
31
+ launcher_root: ''
32
+ })
33
+ },
34
+ dns: async ({ name }) => {
35
+ renderer.kernel.pinokio_configs[name] = { dns: { '@': [] } }
36
+ },
37
+ peer: {
38
+ host: '127.0.0.1',
39
+ check_peers: async () => {}
40
+ },
41
+ launch_complete: true,
42
+ pinokio_configs: {}
43
+ }
44
+ renderer.autolaunch = {
45
+ applyHomeStartingState: async () => false
46
+ }
47
+ renderer.portal = ''
48
+ renderer.install = ''
49
+ renderer.port = 0
50
+ renderer.theme = 'light'
51
+ renderer.logo = ''
52
+ renderer.cloudflare_pub = null
53
+ renderer.composePeerAccessPayload = async () => ({
54
+ peer_access_points: [],
55
+ peer_url: 'http://127.0.0.1:42000',
56
+ peer_qr: null,
57
+ peer_access_router_installed: false
58
+ })
59
+ renderer.current_urls = async () => ({})
60
+ renderer.getPeers = () => []
61
+
62
+ const app = express()
63
+ app.set('views', viewsPath)
64
+ app.set('view engine', 'ejs')
65
+ app.get('/home', (req, res, next) => {
66
+ renderer.render(req, res, [], {}).catch(next)
67
+ })
68
+ app.get('/_api/demo', (req, res, next) => {
69
+ req.query.mode = 'source'
70
+ renderer.render(req, res, ['demo']).catch(next)
71
+ })
72
+ app.get('/api/demo', (req, res, next) => {
73
+ renderer.render(req, res, ['demo']).catch(next)
74
+ })
75
+ app.use((error, req, res, next) => {
76
+ res.status(500).send(error.stack || error.message)
77
+ })
78
+
79
+ const httpServer = http.createServer(app)
80
+ await new Promise((resolve, reject) => {
81
+ httpServer.once('error', reject)
82
+ httpServer.listen(0, '127.0.0.1', resolve)
83
+ })
84
+ const address = httpServer.address()
85
+
86
+ return {
87
+ baseUrl: `http://127.0.0.1:${address.port}`,
88
+ close: async () => {
89
+ await new Promise((resolve, reject) => {
90
+ httpServer.close((error) => error ? reject(error) : resolve())
91
+ })
92
+ await fs.promises.rm(homedir, { recursive: true, force: true })
93
+ }
94
+ }
95
+ }
96
+
97
+ test('directory pages list README files without auto-rendering their contents', async () => {
98
+ const fixture = await createDirectoryRendererFixture()
99
+ try {
100
+ const [homeResponse, sourceResponse, runResponse] = await Promise.all([
101
+ fetch(`${fixture.baseUrl}/home`),
102
+ fetch(`${fixture.baseUrl}/_api/demo`),
103
+ fetch(`${fixture.baseUrl}/api/demo`)
104
+ ])
105
+ const [homeHtml, sourceHtml, runHtml] = await Promise.all([
106
+ homeResponse.text(),
107
+ sourceResponse.text(),
108
+ runResponse.text()
109
+ ])
110
+
111
+ assert.equal(homeResponse.status, 200, homeHtml)
112
+ assert.equal(sourceResponse.status, 200, sourceHtml)
113
+ assert.equal(runResponse.status, 200, runHtml)
114
+
115
+ assert.doesNotMatch(homeHtml, /ROOT README SENTINEL/)
116
+ for (const html of [sourceHtml, runHtml]) {
117
+ assert.doesNotMatch(html, /APP README SENTINEL/)
118
+ assert.match(html, /data-name="README\.md"/)
119
+ assert.match(html, /data-name="keep\.txt"/)
120
+ assert.doesNotMatch(html, /class=['"]readme markdown-body['"]/)
121
+ }
122
+ } finally {
123
+ await fixture.close()
124
+ }
125
+ })
@@ -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", {}],