create-wordjs 2.11.9 → 2.11.11
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/README.md +7 -5
- package/index.js +86 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,11 +205,13 @@ need `openssl` on the PATH. Full details, port matrix and the manual (source-che
|
|
|
205
205
|
|
|
206
206
|
- **GitHub downloads are verified.** Every release publishes `wordjs-<tag>.zip.sha256`
|
|
207
207
|
(`sha256sum` format) next to the bundle. `create-wordjs` — including `upgrade`, `gateway` and
|
|
208
|
-
`join` — downloads it and refuses to extract a ZIP whose SHA-256 differs.
|
|
209
|
-
**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
`join` — downloads it and refuses to extract a ZIP whose SHA-256 differs. A release that
|
|
209
|
+
publishes **no** checksum (only those from before the asset was introduced) is refused unless you
|
|
210
|
+
pass `--allow-unverified`, which installs it with a clear warning that nothing was verified.
|
|
211
|
+
- **Release signature.** The checksum comes from the same release, so it proves the download is
|
|
212
|
+
intact, not who published it. A signed release also publishes `wordjs-<tag>.zip.sha256.sig`, an
|
|
213
|
+
Ed25519 signature of the checksum file made with the project's release key; `create-wordjs`
|
|
214
|
+
verifies it against the public keys embedded in this package and refuses one that does not verify.
|
|
213
215
|
- **`--zip` sources**: a URL must be `https://` (plain `http://` and other schemes are refused); a
|
|
214
216
|
local path is accepted as is. Neither is verified unless you pass `--sha256 <hex>` (a URL
|
|
215
217
|
without it prints a warning).
|
package/index.js
CHANGED
|
@@ -65,6 +65,8 @@ Options:
|
|
|
65
65
|
--sha256 <hex> Require the ZIP to have this SHA-256 (pin a checksum for --zip; also checked
|
|
66
66
|
on top of the release's own .sha256 asset).
|
|
67
67
|
--version <tag> Install/upgrade to a specific release tag (e.g. vX.Y.Z) instead of the latest.
|
|
68
|
+
--allow-unverified Install a GitHub release that publishes no .sha256 for its bundle (only very old
|
|
69
|
+
releases). Without it such a release is refused: nothing would be verified.
|
|
68
70
|
--http Serve plain HTTP instead of self-signed HTTPS (sets WORDJS_HTTP=1). (create)
|
|
69
71
|
--no-start Scaffold + install dependencies only; don't start the server.
|
|
70
72
|
--yes, -y Skip the confirmation prompt (required when upgrading non-interactively).
|
|
@@ -132,7 +134,7 @@ function parseArgs(argv) {
|
|
|
132
134
|
mode: 'create', dir: null, zip: null, version: null, http: false, start: true, yes: false, force: false, install: true,
|
|
133
135
|
role: null, gateway: null, token: null, caHash: null, advertise: null, enrollPort: null, host: null,
|
|
134
136
|
systemd: false, port: null, serviceUser: null, mail: false,
|
|
135
|
-
sha256: null,
|
|
137
|
+
sha256: null, allowUnverified: false,
|
|
136
138
|
};
|
|
137
139
|
// A leading subcommand selects the mode (default is the monolith create flow).
|
|
138
140
|
if (['upgrade', 'gateway', 'join'].includes(argv[0])) { opts.mode = argv[0]; argv = argv.slice(1); }
|
|
@@ -150,6 +152,7 @@ function parseArgs(argv) {
|
|
|
150
152
|
}
|
|
151
153
|
else if (a === '--version') { opts.version = argv[++i] || fail('--version needs a value (a release tag, e.g. vX.Y.Z).'); }
|
|
152
154
|
else if (a === '--http') opts.http = true;
|
|
155
|
+
else if (a === '--allow-unverified') opts.allowUnverified = true;
|
|
153
156
|
else if (a === '--no-start') opts.start = false;
|
|
154
157
|
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
155
158
|
else if (a === '--force') opts.force = true;
|
|
@@ -236,9 +239,62 @@ function readBody(res) {
|
|
|
236
239
|
// The bundle we download becomes the site's code, so it is verified before a single entry is
|
|
237
240
|
// extracted. release.yml publishes `wordjs-<tag>.zip.sha256` (sha256sum format: `<hex> <name>`) next
|
|
238
241
|
// to every tag-named bundle; the GitHub flow downloads it and refuses a ZIP whose digest differs.
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// band, for `--zip` sources (and on top of the release checksum
|
|
242
|
+
// A release that publishes no checksum (only those from before that asset existed) is refused unless
|
|
243
|
+
// the operator passes --allow-unverified, which says out loud that nothing was checked. `--sha256 <hex>`
|
|
244
|
+
// pins a digest the user obtained out of band, for `--zip` sources (and on top of the release checksum
|
|
245
|
+
// when both are present).
|
|
246
|
+
//
|
|
247
|
+
// WHO PUBLISHED IT. The checksum comes from the same release as the ZIP, so it proves the download is
|
|
248
|
+
// intact, not who made it. release.yml therefore signs the checksum file with the project's Ed25519
|
|
249
|
+
// release key (`<bundle>.sha256.sig`, base64), and this CLI verifies that signature against the public
|
|
250
|
+
// keys embedded below — keys that travel inside the npm package, not inside the release. A published
|
|
251
|
+
// signature that no embedded key verifies is fatal. Once SIGNATURE_REQUIRED_SINCE is set, a release at
|
|
252
|
+
// or above that version that publishes no signature is refused as well.
|
|
253
|
+
|
|
254
|
+
/** The project's release-signing public keys (PEM, Ed25519 SPKI). Several, so a key can be rotated. */
|
|
255
|
+
const RELEASE_SIGNING_KEYS = [];
|
|
256
|
+
/** The first release tag every later release must be signed from (e.g. 'v2.12.0'), or null. */
|
|
257
|
+
const SIGNATURE_REQUIRED_SINCE = null;
|
|
258
|
+
|
|
259
|
+
/** Numeric x.y.z compare of two tags (pre-release suffixes ignored); <0, 0 or >0. */
|
|
260
|
+
function compareTags(a, b) {
|
|
261
|
+
const nums = (t) => String(t || '').replace(/^v/, '').split('-')[0].split('.').map((n) => Number(n) || 0);
|
|
262
|
+
const x = nums(a); const y = nums(b);
|
|
263
|
+
for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Does `signatureText` (base64 Ed25519) sign exactly `checksumText` under one of `keys`? Never throws:
|
|
269
|
+
* an unusable key or a malformed signature is simply not a match.
|
|
270
|
+
*/
|
|
271
|
+
function verifyChecksumSignature(checksumText, signatureText, keys = RELEASE_SIGNING_KEYS) {
|
|
272
|
+
const b64 = String(signatureText == null ? '' : signatureText).trim();
|
|
273
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) return false;
|
|
274
|
+
const signature = Buffer.from(b64, 'base64');
|
|
275
|
+
const data = Buffer.from(String(checksumText == null ? '' : checksumText), 'utf8');
|
|
276
|
+
return (Array.isArray(keys) ? keys : []).some((pem) => {
|
|
277
|
+
try { return crypto.verify(null, data, crypto.createPublicKey(pem), signature); } catch { return false; }
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The signature rule for one release, as a verdict: 'verified', 'unsigned' (allowed), or a reason to
|
|
283
|
+
* refuse. `keys` and `requiredSince` are parameters so the rule can be tested with a throwaway key.
|
|
284
|
+
*/
|
|
285
|
+
function signatureVerdict({ tag, checksumText, signatureText, keys = RELEASE_SIGNING_KEYS, requiredSince = SIGNATURE_REQUIRED_SINCE }) {
|
|
286
|
+
const haveKeys = Array.isArray(keys) && keys.length > 0;
|
|
287
|
+
if (signatureText != null) {
|
|
288
|
+
if (!haveKeys) return { ok: true, state: 'unchecked' };
|
|
289
|
+
return verifyChecksumSignature(checksumText, signatureText, keys)
|
|
290
|
+
? { ok: true, state: 'verified' }
|
|
291
|
+
: { ok: false, reason: `the release's checksum signature does not match the WordJS release key` };
|
|
292
|
+
}
|
|
293
|
+
if (haveKeys && requiredSince && compareTags(tag, requiredSince) >= 0) {
|
|
294
|
+
return { ok: false, reason: `release ${tag} publishes no checksum signature, and every release since ${requiredSince} must` };
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, state: 'unsigned' };
|
|
297
|
+
}
|
|
242
298
|
|
|
243
299
|
/** Lower-case 64-char hex, or null. Accepts the `AA:BB:…` colon form some tools print. */
|
|
244
300
|
function normalizeSha256(value) {
|
|
@@ -343,6 +399,12 @@ function pickChecksumAsset(assets, bundleName) {
|
|
|
343
399
|
return (Array.isArray(assets) ? assets : []).find((a) => String(a && a.name || '').toLowerCase() === wanted) || null;
|
|
344
400
|
}
|
|
345
401
|
|
|
402
|
+
/** The `<bundle>.sha256.sig` asset (the release key's signature over the checksum file), by exact name. */
|
|
403
|
+
function pickSignatureAsset(assets, bundleName) {
|
|
404
|
+
const wanted = `${bundleName}.sha256.sig`.toLowerCase();
|
|
405
|
+
return (Array.isArray(assets) ? assets : []).find((a) => String(a && a.name || '').toLowerCase() === wanted) || null;
|
|
406
|
+
}
|
|
407
|
+
|
|
346
408
|
/** Every asset matching the loose `wordjs-*.zip` shape — used to explain an ambiguous refusal. */
|
|
347
409
|
function looseBundleCandidates(assets) {
|
|
348
410
|
const list = Array.isArray(assets) ? assets : [];
|
|
@@ -370,9 +432,11 @@ async function resolveReleaseAsset(tag) {
|
|
|
370
432
|
fail(`Release ${release.tag_name} has no wordjs-*.zip asset.`, 'Pass --zip <path-or-url> instead.');
|
|
371
433
|
}
|
|
372
434
|
const sum = pickChecksumAsset(release.assets, asset.name);
|
|
435
|
+
const sig = pickSignatureAsset(release.assets, asset.name);
|
|
373
436
|
return {
|
|
374
437
|
name: asset.name, url: asset.browser_download_url, tag: release.tag_name,
|
|
375
438
|
checksumName: sum ? sum.name : null, checksumUrl: sum ? sum.browser_download_url : null,
|
|
439
|
+
signatureName: sig ? sig.name : null, signatureUrl: sig ? sig.browser_download_url : null,
|
|
376
440
|
};
|
|
377
441
|
}
|
|
378
442
|
|
|
@@ -554,21 +618,34 @@ async function obtainBundleZip(opts) {
|
|
|
554
618
|
const asset = await resolveReleaseAsset(opts.version);
|
|
555
619
|
tag = asset.tag;
|
|
556
620
|
console.log(` Found ${asset.tag} → ${asset.name}`);
|
|
621
|
+
if (!asset.checksumUrl && !opts.allowUnverified) {
|
|
622
|
+
fail(`Release ${asset.tag} publishes no ${asset.name}.sha256, so its download cannot be verified.`,
|
|
623
|
+
'Refusing to install it. Pick a release that publishes a checksum, or pass --allow-unverified if you accept installing it unchecked.');
|
|
624
|
+
}
|
|
557
625
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-wordjs-'));
|
|
558
626
|
zipPath = path.join(tmpDir, asset.name);
|
|
559
627
|
await download(asset.url, zipPath, asset.name);
|
|
560
628
|
if (asset.checksumUrl) {
|
|
561
|
-
const
|
|
629
|
+
const checksumText = await downloadText(asset.checksumUrl);
|
|
630
|
+
const signed = signatureVerdict({
|
|
631
|
+
tag: asset.tag, checksumText,
|
|
632
|
+
signatureText: asset.signatureUrl ? await downloadText(asset.signatureUrl) : null,
|
|
633
|
+
});
|
|
634
|
+
if (!signed.ok) {
|
|
635
|
+
cleanup();
|
|
636
|
+
fail(`Refusing to install ${asset.name}: ${signed.reason}.`, 'The download may not come from the WordJS project.');
|
|
637
|
+
}
|
|
638
|
+
if (signed.state === 'verified') console.log(` ✓ Release signature verified (${asset.signatureName})`);
|
|
639
|
+
const expected = parseChecksumFile(checksumText, asset.name);
|
|
562
640
|
if (!expected) {
|
|
563
641
|
cleanup();
|
|
564
642
|
fail(`${asset.checksumName} does not contain a SHA-256 for ${asset.name}.`, 'Refusing to install a bundle that cannot be verified.');
|
|
565
643
|
}
|
|
566
644
|
checks.push({ expected, source: asset.checksumName });
|
|
567
645
|
} else {
|
|
568
|
-
//
|
|
569
|
-
// `--version <old-tag>` rollbacks keep working), but say plainly that nothing was verified.
|
|
646
|
+
// Only reachable with --allow-unverified: say plainly that nothing was verified.
|
|
570
647
|
console.warn(` ⚠️ Release ${asset.tag} publishes no ${asset.name}.sha256 — the download's integrity`);
|
|
571
|
-
console.warn(' could NOT be verified (
|
|
648
|
+
console.warn(' could NOT be verified (--allow-unverified).');
|
|
572
649
|
console.warn(' Pin one with --sha256 <hex> if you have it from a trusted source.');
|
|
573
650
|
}
|
|
574
651
|
}
|
|
@@ -1596,7 +1673,7 @@ if (require.main === module) {
|
|
|
1596
1673
|
}
|
|
1597
1674
|
|
|
1598
1675
|
module.exports = {
|
|
1599
|
-
pickBundleAsset, pickChecksumAsset, extractZip, normalizeSha256, parseChecksumFile, classifyZipSource, sha256File,
|
|
1676
|
+
pickBundleAsset, pickChecksumAsset, pickSignatureAsset, verifyChecksumSignature, signatureVerdict, compareTags, extractZip, normalizeSha256, parseChecksumFile, classifyZipSource, sha256File,
|
|
1600
1677
|
parseArgs, buildSystemdFiles, systemdInstallSteps, writeSystemdFiles,
|
|
1601
1678
|
copyMerge, PRESERVE_ON_UPGRADE,
|
|
1602
1679
|
sysctlDropIn, serviceUserProblem, systemdPreflight, createSystemdStagingDir, LOW_PORT_ADVICE, HELP, installedFileMode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-wordjs",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.11",
|
|
4
4
|
"description": "Create a WordJS site with one command — the self-hosted CMS where third-party plugins run in an OS-isolated process with per-capability permission grants. SSR/SEO out of the box, SQLite by default, no PHP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jaime Martinez (https://github.com/jaimemartinez)",
|