Skip to content

Commit a0e829b

Browse files
fix: seed pod files skip-if-exists so user edits survive restarts (#81)
* fix: seed pod files skip-if-exists so user edits survive restarts seedPodFiles() copied welcome.html (as index.html), signin, account and docs pages into the pod root unconditionally on every start, silently clobbering any edits the pod owner had made to those files. Seed them only when the destination does not exist, the same skip-if-exists behaviour JSS uses for its root landing page (and the same rule this function already applied to links.jsonld and apps/). * 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). * fix: non-clobbering apps dir copy; document --no-auth writes nothing - cpSync the apps bundle with force:false + errorOnExist and treat a raced-into-existence dir as already seeded (Copilot review). - docs.html: note .token-secret is not created with --no-auth.
1 parent b217774 commit a0e829b

3 files changed

Lines changed: 68 additions & 45 deletions

File tree

docs.html

Lines changed: 7 additions & 12 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>
@@ -70,10 +70,10 @@ <h2 id="layout">File layout (<code>pod-data/</code>)</h2>
7070
├── <span class="label">settings/</span> <span class="note">prefs + type indexes (JSS-seeded)</span>
7171
├── <span class="label">.acl</span> <span class="note">root container ACL — owner read/write, no public default</span>
7272
├── <span class="label">.idp/</span> <span class="note">JSS internal identity storage (see below)</span>
73-
└── <span class="label">.token-secret</span> <span class="note">JWT signing secret (auto-generated, mode 0600)</span></pre>
73+
└── <span class="label">.token-secret</span> <span class="note">JWT signing secret (auto-generated, mode 0600; not created with --no-auth)</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 jspod writes nothing at all — no seeded pages and no <code>.token-secret</code> (auth is bypassed, so no tokens are ever minted).
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: 60 additions & 32 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,36 +130,55 @@ 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
134-
// finished its init. Same overwrite/skip-if-exists matrix as the
135-
// pre-refactor CLI implementation — unchanged behaviour.
145+
// finished its init. All seeding is skip-if-exists, matching JSS's own
146+
// root-landing-page seeding: a pod owner who edits index.html (or any
147+
// other seeded page) must not have it clobbered on the next start.
136148
function seedPodFiles(root) {
137149
try {
138-
const overwrite = [
150+
const seed = [
139151
'welcome.html', // becomes pod-data/index.html
140152
'signin.html', 'signin.html.acl',
141153
'account.html', 'account.html.acl',
142154
'docs.html', 'docs.html.acl'
143155
];
144-
for (const name of overwrite) {
156+
for (const name of seed) {
145157
const src = join(PACKAGE_ROOT, name);
146158
// welcome.html maps onto index.html; everything else is a passthrough
147159
const destName = name === 'welcome.html' ? 'index.html' : name;
148160
const dst = join(root, destName);
149-
if (existsSync(src)) copyFileSync(src, dst);
161+
if (existsSync(src)) copyIfAbsent(src, dst);
150162
}
151163

152164
const linksSrc = join(PACKAGE_ROOT, 'links.jsonld');
153165
const linksDst = join(root, 'public', 'links.jsonld');
154-
if (existsSync(linksSrc) && !existsSync(linksDst)) {
155-
copyFileSync(linksSrc, linksDst);
166+
if (existsSync(linksSrc)) {
167+
copyIfAbsent(linksSrc, linksDst);
156168
}
157169

158170
const appsSrc = join(PACKAGE_ROOT, 'apps');
159171
const appsDst = join(root, 'public', 'apps');
160-
const appsDirExisted = existsSync(appsDst);
172+
let appsDirExisted = existsSync(appsDst);
161173
if (existsSync(appsSrc) && !appsDirExisted) {
162-
cpSync(appsSrc, appsDst, { recursive: true });
174+
try {
175+
cpSync(appsSrc, appsDst, { recursive: true, force: false, errorOnExist: true });
176+
} catch (e) {
177+
// Same atomicity rule as copyIfAbsent: a concurrently-created
178+
// apps dir means it was already seeded, never overwrite it.
179+
if (e.code !== 'ERR_FS_CP_EEXIST' && e.code !== 'EEXIST') throw e;
180+
appsDirExisted = true;
181+
}
163182
}
164183
return { appsDirExisted };
165184
} catch {
@@ -226,7 +245,10 @@ export async function start(userOptions = {}) {
226245
}
227246
options.port = freePort;
228247

229-
const tokenSecret = resolveTokenSecret(options.root);
248+
// No token secret in --no-auth mode: JSS runs --public there (auth is
249+
// bypassed, so no tokens are ever minted) and persisting the secret
250+
// would write <root>/.token-secret into a publicly served tree.
251+
const tokenSecret = options.auth ? resolveTokenSecret(options.root) : null;
230252

231253
// The data browser URL is a version-pinned jsdelivr asset so the
232254
// running pod always loads the matching browser code for this jspod.
@@ -282,7 +304,7 @@ export async function start(userOptions = {}) {
282304
env: {
283305
...process.env,
284306
PATH: `${join(PACKAGE_ROOT, 'node_modules', '.bin')}${delimiter}${process.env.PATH}`,
285-
TOKEN_SECRET: tokenSecret,
307+
...(tokenSecret ? { TOKEN_SECRET: tokenSecret } : {}),
286308
NODE_ENV: process.env.NODE_ENV || 'development'
287309
}
288310
});
@@ -308,26 +330,32 @@ export async function start(userOptions = {}) {
308330
// Post-readiness: seed pod files and (optionally) bootstrap the
309331
// default app bundle on first run. Failures here are best-effort
310332
// — the pod is already running.
311-
const { appsDirExisted } = seedPodFiles(options.root);
312-
if (!appsDirExisted && options.bootstrap && options.auth) {
313-
console.log(chalk.bold.white(`\n📦 First run — installing the `) +
314-
chalk.yellow('default') +
315-
chalk.bold.white(` bundle...\n`));
316-
const podUrl = url.replace(/\/$/, '');
317-
const installChild = spawn(
318-
process.execPath,
319-
[CLI_SCRIPT, 'install', '--pod', podUrl, '--bundle', 'default'],
320-
{ stdio: 'inherit' }
321-
);
322-
installChild.on('exit', (code) => {
323-
if (code !== 0) {
324-
console.error(chalk.red(`\n✗ Bootstrap exited with code ${code}.`));
325-
console.error(chalk.dim(' Install apps manually with `jspod install`.'));
326-
}
327-
});
328-
installChild.on('error', (e) => {
329-
console.error(chalk.red(`\n✗ Bootstrap failed to start: ${e.message}`));
330-
});
333+
// No seeding in --no-auth mode: jspod runs JSS with --public there,
334+
// where WAC is bypassed (the seeded .acl files would never be
335+
// consulted) and a publicly served tree must not be written into —
336+
// the same rationale as JSS's own public-mode seeding skip (JSS#578).
337+
if (options.auth) {
338+
const { appsDirExisted } = seedPodFiles(options.root);
339+
if (!appsDirExisted && options.bootstrap) {
340+
console.log(chalk.bold.white(`\n📦 First run — installing the `) +
341+
chalk.yellow('default') +
342+
chalk.bold.white(` bundle...\n`));
343+
const podUrl = url.replace(/\/$/, '');
344+
const installChild = spawn(
345+
process.execPath,
346+
[CLI_SCRIPT, 'install', '--pod', podUrl, '--bundle', 'default'],
347+
{ stdio: 'inherit' }
348+
);
349+
installChild.on('exit', (code) => {
350+
if (code !== 0) {
351+
console.error(chalk.red(`\n✗ Bootstrap exited with code ${code}.`));
352+
console.error(chalk.dim(' Install apps manually with `jspod install`.'));
353+
}
354+
});
355+
installChild.on('error', (e) => {
356+
console.error(chalk.red(`\n✗ Bootstrap failed to start: ${e.message}`));
357+
});
358+
}
331359
}
332360
if (options.open && shouldAutoOpen()) {
333361
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)