pinokiod 1.3.91 → 1.3.92

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.
@@ -1,18 +1,37 @@
1
1
  const { yellow, green, blue } = require('kleur');
2
2
  const QRCode = require('qrcode');
3
+ const path = require('path')
4
+ const Util = require('../../util')
3
5
  class C {
4
6
  async tunnel (req, ondata, kernel) {
5
7
  /*
6
8
  {
7
9
  "method": "cloundflare.tunnel",
8
10
  "params": {
9
- "uri": "{{local.url}}"
11
+ "uri": "{{local.url}}",
12
+ "passcode": (optional)
10
13
  }
11
14
  }
12
15
 
13
16
  => cloudflared tunnel --url http://localhost:8080
14
17
  */
15
- const message = `cloudflared tunnel --url ${req.params.uri}`
18
+
19
+
20
+ let pipe_uri
21
+ if (req.params.passcode) {
22
+ // 1. start a pipe server => needed for authentication
23
+ const api_path = Util.api_path(req.parent.path, kernel)
24
+ const pinokio_path = path.resolve(api_path, "pinokio.js")
25
+ const config = (await kernel.loader.load(pinokio_path)).resolved
26
+ pipe_uri = await kernel.pipe.start(req.params.uri, req.parent.path, req.params.passcode, config)
27
+ } else {
28
+ // 2. if no passcode required, no need for a pipe server just use the original
29
+ pipe_uri = req.params.uri
30
+ }
31
+
32
+
33
+ // 2. run cloudflare tunnel on the proxy
34
+ const message = `cloudflared tunnel --url ${pipe_uri}`
16
35
  let params = {
17
36
  id: message,
18
37
  message
@@ -65,15 +84,24 @@ class C {
65
84
  if (!kernel.memory.local[req.parent.path].$share.cloudflare) {
66
85
  kernel.memory.local[req.parent.path].$share.cloudflare = {}
67
86
  }
68
- kernel.memory.local[req.parent.path].$share.cloudflare[req.params.uri] = cloudflare_url
87
+ kernel.memory.local[req.parent.path].$share.cloudflare[pipe_uri] = cloudflare_url
69
88
 
70
89
  return { uri: cloudflare_url }
71
90
  }
72
91
  async stop (req, ondata, kernel) {
92
+ // stop the cloudflare shell
73
93
  const message = `cloudflared tunnel --url ${req.params.uri}`
74
94
  await kernel.shell.kill({
75
95
  id: message
76
96
  })
97
+
98
+ // stop pipe server
99
+ await kernel.pipe.stop(req.params.uri, req.parent.path)
100
+
101
+ // delete the local variable
102
+ delete kernel.memory.local[req.parent.path].$share.cloudflare[req.params.uri]
103
+
77
104
  }
105
+
78
106
  }
79
107
  module.exports = C
@@ -128,6 +128,10 @@ class Api {
128
128
  // stop all proxies
129
129
  this.stopProxy({ script: requestPath })
130
130
 
131
+ // stop all cloudflare tunnels
132
+
133
+ await this.kernel.stopCloudflare({ path: requestPath })
134
+
131
135
  // if there are any pending waiters, delete them
132
136
 
133
137
  if (this.waiter[requestPath]) {
@@ -538,7 +542,7 @@ class Api {
538
542
  // get fully resolved env
539
543
  let env = await Environment.get2(request.path, this.kernel)
540
544
  // set template
541
- this.kernel.template.update(env)
545
+ this.kernel.template.update({ env })
542
546
 
543
547
  // render until `{{ }}` pattern does not exist
544
548
  // 1. render once
@@ -74,6 +74,9 @@ class Local {
74
74
  if (val === "true" || val === "1") {
75
75
  const c = new Cloudflare()
76
76
  req.params.uri = req.params[KEY]
77
+ if ("PINOKIO_SHARE_PASSCODE" in current_env) {
78
+ req.params.passcode = current_env.PINOKIO_SHARE_PASSCODE
79
+ }
77
80
  await c.tunnel(req, ondata, kernel)
78
81
  }
79
82
  }
@@ -102,6 +102,48 @@ const ENVS = [{
102
102
  "#",
103
103
  "##########################################################################",
104
104
  ],
105
+ }, {
106
+ type: ["system", "app"],
107
+ key: "PINOKIO_SHARE_CLOUDFLARE",
108
+ val: "false",
109
+ comment: [
110
+ "##########################################################################",
111
+ "#",
112
+ "# PINOKIO_SHARE_CLOUDFLARE",
113
+ "# Set this variable to share the app publicly via cloudflare tunnel.",
114
+ "#",
115
+ "##########################################################################",
116
+ ]
117
+ }, {
118
+ type: ["app"],
119
+ key: "PINOKIO_SHARE_PASSCODE",
120
+ val: "",
121
+ comment: [
122
+ "##########################################################################",
123
+ "#",
124
+ "# PINOKIO_SHARE_PASSCODE",
125
+ "#",
126
+ "# By default, your publicly shared app will be 100% open to anyone",
127
+ "# with the link via Cloudflare.",
128
+ "#",
129
+ "# You can add authorization by protecting it with a passcode.",
130
+ "# Set this value, and any access to the app will require a pass code input",
131
+ "#",
132
+ "##########################################################################",
133
+ ],
134
+ }, {
135
+ type: ["system", "app"],
136
+ key: "PINOKIO_SCRIPT_DEFAULT",
137
+ val: "true",
138
+ comment: [
139
+ "##########################################################################",
140
+ "#",
141
+ "# PINOKIO_SCRIPT_DEFAULT",
142
+ "# If this variable is false, 'default': true menu items in pinokio.js",
143
+ "# will NOT automatically run",
144
+ "#",
145
+ "##########################################################################",
146
+ ]
105
147
  }, {
106
148
  type: ["system"],
107
149
  key: "PINOKIO_DRIVE",
@@ -194,31 +236,6 @@ const ENVS = [{
194
236
  "#",
195
237
  "##########################################################################",
196
238
  ],
197
- }, {
198
- type: ["system", "app"],
199
- key: "PINOKIO_SHARE_CLOUDFLARE",
200
- val: "false",
201
- comment: [
202
- "##########################################################################",
203
- "#",
204
- "# PINOKIO_SHARE_CLOUDFLARE",
205
- "# Set this variable to share the app publicly via cloudflare tunnel.",
206
- "#",
207
- "##########################################################################",
208
- ]
209
- }, {
210
- type: ["system", "app"],
211
- key: "PINOKIO_SCRIPT_DEFAULT",
212
- val: "true",
213
- comment: [
214
- "##########################################################################",
215
- "#",
216
- "# PINOKIO_SCRIPT_DEFAULT",
217
- "# If this variable is false, 'default': true menu items in pinokio.js",
218
- "# will NOT automatically run",
219
- "#",
220
- "##########################################################################",
221
- ]
222
239
  }];
223
240
  //const ENV = (homedir) => {
224
241
  // const lines = ENVS.map((e) => {
package/kernel/index.js CHANGED
@@ -22,6 +22,8 @@ const Environment = require("./environment")
22
22
  const Util = require('./util')
23
23
  const Config = require("./pinokio.json")
24
24
  const Info = require('./info')
25
+ const Pipe = require("../pipe")
26
+ const Cloudflare = require('./api/cloudflare')
25
27
  const VARS = {
26
28
  pip: {
27
29
  install: {
@@ -45,11 +47,33 @@ class Kernel {
45
47
  this.exposed = {}
46
48
  this.envs = {}
47
49
  this.info = new Info(this)
50
+ this.pipe = new Pipe(this)
51
+ this.cloudflare = new Cloudflare()
48
52
  }
49
53
  /*
50
54
  kernel.env() => return the system environment
51
55
  kernel.env("api/stablediffusion") => returns the full environment object
52
56
  */
57
+ async stopCloudflare(option) {
58
+ if (option.uri) {
59
+ } else if (option.path) {
60
+ let scriptPath = option.path
61
+ if (this.memory.local[scriptPath] && this.memory.local[scriptPath].$share) {
62
+ let cf = this.memory.local[scriptPath].$share.cloudflare
63
+ let uris = Object.keys(cf)
64
+ for(let uri of uris) {
65
+ await this.cloudflare.stop({
66
+ parent: {
67
+ path: scriptPath
68
+ },
69
+ params: { uri }
70
+ }, (e) => {
71
+ process.stdout.write(e.raw)
72
+ }, this)
73
+ }
74
+ }
75
+ }
76
+ }
53
77
  async env(...args) {
54
78
  let folderpath
55
79
  if (args) {
@@ -114,7 +138,10 @@ class Kernel {
114
138
  * let available_port = await kernel.port()
115
139
  *
116
140
  **********************************************/
117
- return portfinder.getPortPromise()
141
+ return portfinder.getPortPromise({
142
+ port: 42000
143
+ })
144
+ //return portfinder.getPortPromise()
118
145
  }
119
146
  path(...args) {
120
147
  return path.resolve(this.homedir, ...args)
package/kernel/sysinfo.js CHANGED
@@ -122,8 +122,7 @@ class Sysinfo {
122
122
  }
123
123
 
124
124
 
125
- this.info.env = process.env
126
- //this.info.shell_env = vars
125
+ // this.info.env = process.env
127
126
  this.info.shell_env = lines
128
127
  }
129
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "1.3.91",
3
+ "version": "1.3.92",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,6 +12,7 @@
12
12
  "7zip-min-win-asar-support": "^1.4.4",
13
13
  "async-retry": "^1.3.3",
14
14
  "axios": "^1.4.0",
15
+ "bcrypt": "^5.1.1",
15
16
  "clear-module": "^4.1.2",
16
17
  "compressing": "^1.10.0",
17
18
  "cookie-parser": "^1.4.6",
@@ -33,9 +34,10 @@
33
34
  "glob-gitignore": "^1.0.14",
34
35
  "handlebars": "^4.7.7",
35
36
  "http-proxy": "^1.18.1",
37
+ "http-proxy-middleware": "^3.0.0",
36
38
  "http-terminator": "^3.2.0",
37
39
  "isomorphic-git": "^1.23.0",
38
- "jimini": "^0.0.5",
40
+ "jimini": "^0.0.6",
39
41
  "jsdom": "^24.0.0",
40
42
  "jsonpath-plus": "^7.2.0",
41
43
  "key-store": "^1.2.0",
package/pipe/index.js ADDED
@@ -0,0 +1,102 @@
1
+ const { createProxyMiddleware } = require('http-proxy-middleware');
2
+
3
+ const path = require('path')
4
+ const { createHttpTerminator } = require('http-terminator')
5
+ const express = require('express')
6
+ const session = require('express-session');
7
+ const bcrypt = require('bcrypt');
8
+ const cors = require('cors');
9
+ class Pipe {
10
+ constructor(kernel) {
11
+ this.kernel = kernel
12
+ this.terminators = {}
13
+ this.procs = {}
14
+ }
15
+ authenticate (req, res, next) {
16
+ if (req.session.authenticated) {
17
+ next();
18
+ } else {
19
+ console.log("not authenticated. redirect /pionkio/login")
20
+ res.redirect('/pinokio/login');
21
+ }
22
+ }
23
+ // a pipe server for creating publicly shareable endpoints
24
+ async start(url, scriptPath, passcode, config) {
25
+
26
+ if (!config) config = {}
27
+ if (config.icon) {
28
+ }
29
+
30
+
31
+ let port = await this.kernel.port()
32
+ let app = express();
33
+ app.use(cors({ origin: '*' }));
34
+ app.use(express.urlencoded({ extended: true }));
35
+ app.set('view engine', 'ejs');
36
+ app.set("views", path.resolve(__dirname, "views"))
37
+ // app.use(express.static(path.resolve(__dirname, 'public')));
38
+ app.use(session({
39
+ secret: 'oikonip',
40
+ resave: false,
41
+ saveUninitialized: true
42
+ }));
43
+ app.get('/pinokio/login', (req, res) => {
44
+ res.render("login", { error: null, ...config })
45
+ });
46
+
47
+ app.post('/pinokio/login', async (req, res) => {
48
+
49
+ const { password } = req.body;
50
+
51
+ //if (await bcrypt.compare(password, hashedPassword)) {
52
+ if (password === passcode) {
53
+ req.session.authenticated = true;
54
+ res.redirect('/');
55
+ } else {
56
+ res.status(401).render("login", { error: "Invalid passcode", ...config })
57
+ }
58
+ });
59
+
60
+ app.get('/pinokio/logout', (req, res) => {
61
+ req.session.authenticated = false;
62
+ res.redirect('/login');
63
+ });
64
+
65
+
66
+ const proxy = createProxyMiddleware({
67
+ pathFilter: '/',
68
+ target: url,
69
+ ws: true
70
+ })
71
+
72
+ app.use('/', this.authenticate, proxy)
73
+
74
+
75
+ await new Promise((resolve, reject) => {
76
+ console.log("starting pipe server at port", port)
77
+ let server = app.listen(port, () => {
78
+ console.log(`Pipe server listening on port ${port}`)
79
+ resolve()
80
+ });
81
+ let terminator = createHttpTerminator({ server });
82
+ this.terminators[url] = terminator
83
+ })
84
+ let pipe_uri = `http://localhost:${port}`
85
+
86
+ // original url => pipe url
87
+ if (!this.procs[scriptPath]) {
88
+ this.procs[scriptPath] = {}
89
+ }
90
+ this.procs[scriptPath][url] = pipe_uri
91
+ return pipe_uri
92
+ }
93
+ async stop(url, scriptPath) {
94
+ console.log("stop pipe", { scriptPath, url, terminator: this.terminators[url] })
95
+ if (this.terminators[url]) {
96
+ await this.terminators[url].terminate()
97
+ delete this.terminators[url]
98
+ delete this.procs[scriptPath][url]
99
+ }
100
+ }
101
+ }
102
+ module.exports = Pipe
@@ -0,0 +1,78 @@
1
+ <html>
2
+ <head>
3
+ <title>Pinokio</title>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
6
+ <style>
7
+ body {
8
+ display: flex;
9
+ justify-content: center;
10
+ align-items: center;
11
+ height: 100%;
12
+ font-family: sans-serif;
13
+ font-size: 14px;
14
+ background: rgb(27, 28, 29);
15
+ color: white;
16
+ }
17
+ main {
18
+ max-width: 500px;
19
+ width: 100%;
20
+ text-align: center;
21
+ text-transform: lowercase;
22
+ }
23
+ main img {
24
+ width: 80px;
25
+ margin-bottom: 20px;
26
+ }
27
+ main form {
28
+ display: flex;
29
+ }
30
+ main input {
31
+ flex-grow: 1;
32
+ padding: 10px;
33
+ box-sizing: border-box;
34
+ border: none;
35
+ /*
36
+ background: rgba(0,0,0,0.06);
37
+ */
38
+ font-size: 14px;
39
+ border-radius: 0;
40
+
41
+ }
42
+ main button {
43
+ color: white;
44
+ background: royalblue;
45
+ padding: 10px;
46
+ box-sizing: border-box;
47
+ border: none;
48
+ }
49
+ .error {
50
+ border-left: 3px solid red;
51
+ color: red;
52
+ text-align: left;
53
+ padding: 5px 10px;
54
+ box-sizing: border-box;
55
+ font-size: 14px;
56
+ }
57
+ h1 {
58
+ letter-spacing: -1px;
59
+ margin-bottom: 20px;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <main>
65
+ <!--
66
+ <img src="/pinokio-black.png"/>
67
+ -->
68
+ <h1>PINOKIO</h1>
69
+ <form method="POST" action="/pinokio/login">
70
+ <input placeholder="enter passcode" type="password" name="password" required>
71
+ <button type="submit">Login</button>
72
+ </form>
73
+ <% if (error) { %>
74
+ <div class='error'><%=error%></div>
75
+ <% } %>
76
+ <main>
77
+ </body>
78
+ </html>
package/server/index.js CHANGED
@@ -173,11 +173,9 @@ class Server {
173
173
  })
174
174
  }
175
175
  async init_env(env_dir_path) {
176
- console.log("env_path",env_dir_path)
177
176
  let current = this.kernel.path(env_dir_path, "ENVIRONMENT")
178
177
  // if environment.json doesn't exist,
179
178
  let exists = await this.exists(current)
180
- console.log({ current, exists })
181
179
  if (exists) {
182
180
  // if ENVIRONMENT already exists, don't do anything
183
181
  } else {
@@ -187,11 +185,10 @@ class Server {
187
185
  // if _ENVIRONMENT exists,
188
186
  let _environment = this.kernel.path(env_dir_path, "_ENVIRONMENT")
189
187
  let _exists = await this.exists(_environment)
190
- console.log({ _environment, _exists })
191
188
  let content = await Environment.ENV("app")
192
189
  if (_exists) {
193
190
  let _environmentStr = await fs.promises.readFile(_environment, "utf8")
194
- await fs.promises.writeFile(current, _environmentStr + "\n\\n\n" + content)
191
+ await fs.promises.writeFile(current, _environmentStr + "\n\n\n" + content)
195
192
  } else {
196
193
  await fs.promises.writeFile(current, content)
197
194
  }
@@ -1878,9 +1875,15 @@ class Server {
1878
1875
  // stop proxies
1879
1876
  for(let scriptPath in this.kernel.api.proxies) {
1880
1877
  try {
1878
+ // Turn off local sharing
1881
1879
  await this.kernel.api.stopProxy({
1882
1880
  script: scriptPath
1883
1881
  })
1882
+
1883
+ // Turn off cloudflare sharing
1884
+ await this.kernel.stopCloudflare({
1885
+ path: scriptPath
1886
+ })
1884
1887
  } catch (e) {
1885
1888
  }
1886
1889
  }
@@ -2171,7 +2174,6 @@ class Server {
2171
2174
  }
2172
2175
  })
2173
2176
  this.app.post("/env", async (req, res) => {
2174
- console.log("req.body", req.body)
2175
2177
  let fullpath = path.resolve(this.kernel.homedir, req.body.filepath, "ENVIRONMENT")
2176
2178
  let updated = req.body.vals
2177
2179
  await Util.update_env(fullpath, updated)
@@ -2206,12 +2208,7 @@ class Server {
2206
2208
  let pathComponents = req.params[0].split("/")
2207
2209
  let filepath = path.resolve(this.kernel.homedir, req.params[0], "ENVIRONMENT")
2208
2210
  let editorpath = "/edit/" + req.params[0] + "/ENVIRONMENT"
2209
-
2210
- console.log({ pathComponents, filepath, editorpath, })
2211
2211
  const items = await Util.parse_env_detail(filepath)
2212
-
2213
- console.log(items)
2214
-
2215
2212
  let p = path.resolve(this.kernel.homedir, env_path, "pinokio.js")
2216
2213
  let config = (await this.kernel.loader.load(p)).resolved
2217
2214
  if (config.icon) {
@@ -2234,16 +2231,13 @@ class Server {
2234
2231
  agent: this.agent,
2235
2232
  })
2236
2233
  })
2237
- this.app.get("/pre/:name", async (req, res) => {
2238
- let p = path.resolve(this.kernel.homedir, "api", req.params.name, "pinokio.js")
2234
+ this.app.get("/pre/api/:name", async (req, res) => {
2235
+ let p = path.resolve(this.kernel.homedir, req.params.name, "pinokio.js")
2239
2236
  let config = (await this.kernel.loader.load(p)).resolved
2240
- console.log("config", config)
2241
2237
  if (config && config.pre) {
2242
-
2243
2238
  config.pre.forEach((item) => {
2244
2239
  item.icon = `/api/${req.params.name}/${item.icon}?raw=true`
2245
2240
  })
2246
-
2247
2241
  res.render("pre", {
2248
2242
  name: req.params.name,
2249
2243
  theme: this.theme,
@@ -2252,7 +2246,7 @@ class Server {
2252
2246
  items: config.pre
2253
2247
  })
2254
2248
  } else {
2255
- res.redirect("/env/" + req.params.name)
2249
+ res.redirect("/env/" + req.params.name + "?init=true")
2256
2250
  }
2257
2251
  })
2258
2252
  this.app.get("/initialize/:name", async (req, res) => {
@@ -2262,10 +2256,10 @@ class Server {
2262
2256
  // if pinokio.js exists
2263
2257
  if (config.pre && Array.isArray(config.pre)) {
2264
2258
  // if pre exists, redirect to /pre/:name
2265
- res.redirect(`/pre/${req.params.name}`)
2259
+ res.redirect(`/pre/api/${req.params.name}`)
2266
2260
  } else {
2267
2261
  // if pre doesn't exist, redirect to /env/:name
2268
- res.redirect(`/env/${req.params.name}`)
2262
+ res.redirect(`/env/api/${req.params.name}?init=true`)
2269
2263
  }
2270
2264
  } else {
2271
2265
  // if pinokio.js doesn't exist, send to /browser/:name
@@ -2275,9 +2269,7 @@ class Server {
2275
2269
  this.app.get("/share/:name", async (req, res) => {
2276
2270
  let filepath = path.resolve(this.kernel.homedir, "api", req.params.name, "ENVIRONMENT")
2277
2271
  //let filepath = path.resolve(this.kernel.homedir, req.params[0])
2278
- console.log("filepath", filepath)
2279
2272
  const config = await Util.parse_env(filepath)
2280
- console.log("config", config)
2281
2273
  const keys = [
2282
2274
  "PINOKIO_SHARE_CLOUDFLARE",
2283
2275
  "PINOKIO_SHARE_LOCAL",
@@ -2294,10 +2286,8 @@ class Server {
2294
2286
  })
2295
2287
  let cloudflare_links = []
2296
2288
  let local_links = []
2297
- console.log({ app_path, scripts })
2298
2289
  for(let script in this.kernel.memory.local) {
2299
2290
  let mem = this.kernel.memory.local[script]
2300
- console.log({ script, mem })
2301
2291
  if (mem.$share) {
2302
2292
  if (mem.$share.cloudflare) {
2303
2293
  for(let key in mem.$share.cloudflare) {
@@ -2890,47 +2880,6 @@ class Server {
2890
2880
  });
2891
2881
  })
2892
2882
 
2893
-
2894
-
2895
- // this.expose_app = express();
2896
- // this.expose_app.use(cors({
2897
- // origin: '*'
2898
- // }));
2899
- // this.expose_app.use('/', proxy(async (req) => {
2900
- // console.log("req.url", req.url)
2901
- // let tokens = req.url.split("/")
2902
- // console.log({ tokens })
2903
- // if (tokens.length >=2 ) {
2904
- // let app = tokens[1]
2905
- // console.log({ app })
2906
- // console.log("resolved = ", this.kernel.exposed[app])
2907
- // return this.kernel.exposed[app]
2908
- // }
2909
- // }, {
2910
- //// proxyReqPathResolver: (req) => {
2911
- //// console.log("proxyReqPathResolver", req.url)
2912
- //// let chunks = req.url.split("/")
2913
- //// let transformed
2914
- //// if (chunks.length > 3) {
2915
- //// transformed = "/" + chunks.slice(3).join("/")
2916
- //// } else {
2917
- //// transformed = "/"
2918
- //// }
2919
- //// console.log({ chunks, transformed })
2920
- //// return transformed
2921
- //// }
2922
- // }))
2923
- // this.expose_port = await portfinder.getPortPromise({ port: this.port })
2924
- // console.log("EXPOSE_PORT", this.expose_port)
2925
- // await new Promise((resolve, reject) => {
2926
- // this.expose_server = this.expose_app.listen(this.expose_port, () => {
2927
- // console.log(`Server listening on port ${this.expose_port}`)
2928
- // resolve()
2929
- // });
2930
- // this.exposeTerminator = createHttpTerminator({
2931
- // server: this.expose_server
2932
- // });
2933
- // })
2934
2883
  }
2935
2884
  }
2936
2885
  module.exports = Server
@@ -164,7 +164,7 @@ document.addEventListener("DOMContentLoaded", async () => {
164
164
  "Content-Type": "application/json"
165
165
  },
166
166
  body: JSON.stringify({
167
- filepath: "api/" + location.pathname.slice(7) + "/ENVIRONMENT",
167
+ filepath: "api/" + location.pathname.slice(7),
168
168
  vals
169
169
  })
170
170
  }).then((res) => {
@@ -194,6 +194,7 @@ document.addEventListener("DOMContentLoaded", async () => {
194
194
  <li>This will let you share your local Pinokio app to the public internet, which anyone with a URL can access.</li>
195
195
  <li>Powered by <a href="https://www.cloudflare.com/products/tunnel/" target="_blank" features="browser">Cloudflare Tunnel</a></li>
196
196
  <li>You can manually configure this in through the Configure menu (set PINOKIO_SHARE_CLOUDFLARE to true)</li>
197
+ <li>You can add <strong>authentication</strong> by adding a passcode (set PINOKIO_SHARE_PASSCODE in the <strong>Configure</strong> tab)</li>
197
198
  </ul>
198
199
  <% cloudflare_links.forEach((link) => { %>
199
200
  <div class='shared'>