Skip to content

Commit 7cc0df5

Browse files
fix: no seeding or token secret in --no-auth mode; atomic no-clobber; jss 0.0.212
Review follow-ups on top of the skip-if-exists seeding fix: - Gate seedPodFiles() and the token secret on options.auth. In --no-auth mode jspod runs JSS with --public, where WAC is bypassed (seeded .acl files are never consulted) and a publicly served tree must not be written into — the same rationale as JSS's public-mode landing-page seeding skip (JSS#578). - Use COPYFILE_EXCL so the no-clobber guarantee holds even against a concurrent writer (Copilot review). - Update docs.html, which still described the old overwrite-every-start behaviour (Copilot review). - Bump javascript-solid-server to >=0.0.212 <0.0.213 to pick up the public-mode seeding skip (JSS#578) and the WAC sidecar fix (JSS#580).
1 parent 65f4650 commit 7cc0df5

3 files changed

Lines changed: 53 additions & 38 deletions

File tree

docs.html

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ <h2 id="layout">File layout (<code>pod-data/</code>)</h2>
5252
<p>jspod creates <code>pod-data/</code> in the current working directory on first start (or wherever you pass via <code>--root</code>). Everything the pod is, including identities and signing keys, lives under that single tree.</p>
5353

5454
<pre class="tree"><span class="label">pod-data/</span> <span class="note">your pod root</span>
55-
├── <span class="label">index.html</span> <span class="note">welcome page — jspod overwrites every start</span>
55+
├── <span class="label">index.html</span> <span class="note">welcome page — jspod-seeded, skip-if-exists</span>
5656
├── <span class="label">index.html.acl</span> <span class="note">public-read for the landing page (JSS-managed)</span>
57-
├── <span class="label">signin.html</span> <span class="note">jspod's sign-in app — overwritten every start</span>
57+
├── <span class="label">signin.html</span> <span class="note">jspod's sign-in app — seeded, skip-if-exists</span>
5858
├── <span class="label">signin.html.acl</span> <span class="note">public-read</span>
59-
├── <span class="label">account.html</span> <span class="note">post-sign-in dashboard — overwritten every start</span>
59+
├── <span class="label">account.html</span> <span class="note">post-sign-in dashboard — seeded, skip-if-exists</span>
6060
├── <span class="label">account.html.acl</span> <span class="note">public-read</span>
61-
├── <span class="label">docs.html</span> <span class="note">this page — overwritten every start</span>
61+
├── <span class="label">docs.html</span> <span class="note">this page — seeded, skip-if-exists</span>
6262
├── <span class="label">docs.html.acl</span> <span class="note">public-read</span>
6363
├── <span class="label">profile/</span>
6464
│ └── <span class="label">card.jsonld</span> <span class="note">your WebID profile (JSS-seeded)</span>
@@ -73,7 +73,7 @@ <h2 id="layout">File layout (<code>pod-data/</code>)</h2>
7373
└── <span class="label">.token-secret</span> <span class="note">JWT signing secret (auto-generated, mode 0600)</span></pre>
7474

7575
<div class="callout">
76-
<strong>Overwrite warning.</strong> <code>index.html</code>, <code>signin.html</code>, <code>account.html</code>, and <code>docs.html</code> are owned by jspod and overwritten on every <em>start</em>. Edits won't survive a restart. To customize, fork jspod or run JSS directly instead. The <code>public/links.jsonld</code> and <code>public/apps/pilot/</code> bundles are <em>skip-if-exists</em>, so user edits there <em>do</em> survive.
76+
<strong>Seeding.</strong> <code>index.html</code>, <code>signin.html</code>, <code>account.html</code>, and <code>docs.html</code> are seeded by jspod on first start and never overwritten — edits survive restarts, same as <code>public/links.jsonld</code> and <code>public/apps/pilot/</code>. The trade-off: upgrading jspod won't refresh a page you already have; delete the file and restart to re-seed the current stock copy. In <code>--no-auth</code> mode nothing is seeded at all — the served directory is left untouched.
7777
</div>
7878

7979
<h2 id="idp">Where the IDP data lives</h2>
@@ -91,12 +91,7 @@ <h2 id="idp">Where the IDP data lives</h2>
9191
<p>The account record contains a bcrypt hash of the password — not the plaintext. The signing keys are sensitive (anyone with these can mint valid tokens against your pod). JSS manages file modes on these; jspod doesn't touch them.</p>
9292

9393
<h2 id="customize">Customize the landing page</h2>
94-
<p>The default landing page (<code>pod-data/index.html</code>) is owned by jspod. Edits to it are overwritten on the next start. To genuinely customize, the options are, in increasing order of effort:</p>
95-
<ul>
96-
<li>Fork jspod (clone the repo, edit <code>welcome.html</code>, run <code>./index.js</code> from the fork). All overwrites use <em>your</em> templates.</li>
97-
<li>Run JSS directly with your own <code>pod-data/index.html</code> pre-seeded. JSS does skip-if-exists for its own seed, so your file wins. You lose jspod's account dashboard / signin app / data browser, but you can copy any of them out of the npm package and adapt.</li>
98-
<li>Track <a href="https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/459">JSS#459</a> for a future <code>--landing-page &lt;file&gt;</code> option that would make this cleaner.</li>
99-
</ul>
94+
<p>The default landing page (<code>pod-data/index.html</code>) is seeded on first start only — just edit it, and your version survives restarts. To return to the stock page, delete <code>pod-data/index.html</code> and restart. To change the template used for <em>new</em> pods, fork jspod and edit <code>welcome.html</code>.</p>
10095

10196
<h2 id="auth-ladder">Auth ladder</h2>
10297
<p>jspod ships you onto rung 1 (default <code>me</code>/<code>me</code>) and makes climbing visible. The dashboard at <code>/account.html</code> exposes a Change password form (rung 2). Passkey support (rung 3) is on the roadmap. See <a href="https://github.com/JavaScriptSolidServer/jspod/issues/6">jspod#6</a> for the full ladder framing.</p>

lib/start.js

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { dirname, join, delimiter } from 'path';
1616
import chalk from 'chalk';
1717
import {
1818
existsSync, mkdirSync, readFileSync, writeFileSync,
19-
chmodSync, statSync, copyFileSync, cpSync
19+
chmodSync, statSync, copyFileSync, cpSync, constants as fsConstants
2020
} from 'fs';
2121
import { randomBytes } from 'crypto';
2222
import { createServer } from 'net';
@@ -130,6 +130,17 @@ function shouldAutoOpen() {
130130
return true;
131131
}
132132

133+
// Copy src → dst only if dst does not already exist. COPYFILE_EXCL makes
134+
// the no-clobber guarantee atomic: a dst created between check and copy
135+
// surfaces as EEXIST (ignored) instead of being overwritten.
136+
function copyIfAbsent(src, dst) {
137+
try {
138+
copyFileSync(src, dst, fsConstants.COPYFILE_EXCL);
139+
} catch (e) {
140+
if (e.code !== 'EEXIST') throw e;
141+
}
142+
}
143+
133144
// Seed jspod-owned pages into the running pod's root once JSS has
134145
// finished its init. All seeding is skip-if-exists, matching JSS's own
135146
// root-landing-page seeding: a pod owner who edits index.html (or any
@@ -147,13 +158,13 @@ function seedPodFiles(root) {
147158
// welcome.html maps onto index.html; everything else is a passthrough
148159
const destName = name === 'welcome.html' ? 'index.html' : name;
149160
const dst = join(root, destName);
150-
if (existsSync(src) && !existsSync(dst)) copyFileSync(src, dst);
161+
if (existsSync(src)) copyIfAbsent(src, dst);
151162
}
152163

153164
const linksSrc = join(PACKAGE_ROOT, 'links.jsonld');
154165
const linksDst = join(root, 'public', 'links.jsonld');
155-
if (existsSync(linksSrc) && !existsSync(linksDst)) {
156-
copyFileSync(linksSrc, linksDst);
166+
if (existsSync(linksSrc)) {
167+
copyIfAbsent(linksSrc, linksDst);
157168
}
158169

159170
const appsSrc = join(PACKAGE_ROOT, 'apps');
@@ -227,7 +238,10 @@ export async function start(userOptions = {}) {
227238
}
228239
options.port = freePort;
229240

230-
const tokenSecret = resolveTokenSecret(options.root);
241+
// No token secret in --no-auth mode: JSS runs --public there (auth is
242+
// bypassed, so no tokens are ever minted) and persisting the secret
243+
// would write <root>/.token-secret into a publicly served tree.
244+
const tokenSecret = options.auth ? resolveTokenSecret(options.root) : null;
231245

232246
// The data browser URL is a version-pinned jsdelivr asset so the
233247
// running pod always loads the matching browser code for this jspod.
@@ -283,7 +297,7 @@ export async function start(userOptions = {}) {
283297
env: {
284298
...process.env,
285299
PATH: `${join(PACKAGE_ROOT, 'node_modules', '.bin')}${delimiter}${process.env.PATH}`,
286-
TOKEN_SECRET: tokenSecret,
300+
...(tokenSecret ? { TOKEN_SECRET: tokenSecret } : {}),
287301
NODE_ENV: process.env.NODE_ENV || 'development'
288302
}
289303
});
@@ -309,26 +323,32 @@ export async function start(userOptions = {}) {
309323
// Post-readiness: seed pod files and (optionally) bootstrap the
310324
// default app bundle on first run. Failures here are best-effort
311325
// — the pod is already running.
312-
const { appsDirExisted } = seedPodFiles(options.root);
313-
if (!appsDirExisted && options.bootstrap && options.auth) {
314-
console.log(chalk.bold.white(`\n📦 First run — installing the `) +
315-
chalk.yellow('default') +
316-
chalk.bold.white(` bundle...\n`));
317-
const podUrl = url.replace(/\/$/, '');
318-
const installChild = spawn(
319-
process.execPath,
320-
[CLI_SCRIPT, 'install', '--pod', podUrl, '--bundle', 'default'],
321-
{ stdio: 'inherit' }
322-
);
323-
installChild.on('exit', (code) => {
324-
if (code !== 0) {
325-
console.error(chalk.red(`\n✗ Bootstrap exited with code ${code}.`));
326-
console.error(chalk.dim(' Install apps manually with `jspod install`.'));
327-
}
328-
});
329-
installChild.on('error', (e) => {
330-
console.error(chalk.red(`\n✗ Bootstrap failed to start: ${e.message}`));
331-
});
326+
// No seeding in --no-auth mode: jspod runs JSS with --public there,
327+
// where WAC is bypassed (the seeded .acl files would never be
328+
// consulted) and a publicly served tree must not be written into —
329+
// the same rationale as JSS's own public-mode seeding skip (JSS#578).
330+
if (options.auth) {
331+
const { appsDirExisted } = seedPodFiles(options.root);
332+
if (!appsDirExisted && options.bootstrap) {
333+
console.log(chalk.bold.white(`\n📦 First run — installing the `) +
334+
chalk.yellow('default') +
335+
chalk.bold.white(` bundle...\n`));
336+
const podUrl = url.replace(/\/$/, '');
337+
const installChild = spawn(
338+
process.execPath,
339+
[CLI_SCRIPT, 'install', '--pod', podUrl, '--bundle', 'default'],
340+
{ stdio: 'inherit' }
341+
);
342+
installChild.on('exit', (code) => {
343+
if (code !== 0) {
344+
console.error(chalk.red(`\n✗ Bootstrap exited with code ${code}.`));
345+
console.error(chalk.dim(' Install apps manually with `jspod install`.'));
346+
}
347+
});
348+
installChild.on('error', (e) => {
349+
console.error(chalk.red(`\n✗ Bootstrap failed to start: ${e.message}`));
350+
});
351+
}
332352
}
333353
if (options.open && shouldAutoOpen()) {
334354
console.log(chalk.green(`\n🌐 Opening ${url} in your browser...`));

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@
6060
},
6161
"dependencies": {
6262
"chalk": "^5.6.2",
63-
"javascript-solid-server": ">=0.0.208 <0.0.211"
63+
"javascript-solid-server": ">=0.0.212 <0.0.213"
6464
}
6565
}

0 commit comments

Comments
 (0)