create-wordjs 2.8.0 → 2.10.0

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.
Files changed (3) hide show
  1. package/README.md +44 -13
  2. package/index.js +37 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -53,7 +53,7 @@ in CI — plus a pure-JS *SQLite (legacy / WASM)* fallback for hosts where the n
53
53
  | --- | --- |
54
54
  | `--zip <path-or-url>` | Use a local release ZIP (or a direct **`https://`** ZIP URL — plain `http://` is refused) instead of querying the GitHub API. Handy offline or when rate-limited. |
55
55
  | `--sha256 <hex>` | Refuse the ZIP unless its SHA-256 is exactly this (the value in the release's `.sha256` asset). Use it to pin a `--zip` file or URL; with a GitHub download it is checked on top of the release checksum. |
56
- | `--version <tag>` | Install a specific release (e.g. `--version v2.1.0`) instead of the latest. |
56
+ | `--version <tag>` | Install a specific release (e.g. `--version v2.9.0`) instead of the latest. |
57
57
  | `--http` | Serve plain HTTP instead of self-signed HTTPS (sets `WORDJS_HTTP=1`). |
58
58
  | `--no-start` | Scaffold and install dependencies only — start the server yourself later. |
59
59
  | `--yes`, `-y` | Skip the confirmation prompt (required when `upgrade` runs non-interactively). |
@@ -62,6 +62,7 @@ in CI — plus a pure-JS *SQLite (legacy / WASM)* fallback for hosts where the n
62
62
  | `--systemd` | (create, `upgrade`; Linux) Also write a systemd unit, `wordjs.service`, to a private staging directory outside the site and print how to install it. The unit runs WordJS as a dedicated non-root account with **no Linux capabilities**. With create it implies `--no-start`. See [Run it as a service](#run-it-as-a-service-linux). |
63
63
  | `--port <n>` | (with `--systemd`) Public port written into the unit (`PORT=<n>`); default: the site's configured port, else `3000`. Below 1024 it also stages `60-wordjs-ports.conf`. |
64
64
  | `--service-user <name>` | (with `--systemd`) The account the service runs as (default `wordjs`; `root` is refused). |
65
+ | `--mail` | (with `--systemd`) The site will receive mail (mail-server plugin, SMTP on 25): the drop-in also allows 25. Implied on `upgrade` when that plugin is installed. |
65
66
  | `-h`, `--help` | Show usage. |
66
67
 
67
68
  Separate-mode options:
@@ -77,15 +78,39 @@ Separate-mode options:
77
78
 
78
79
  ## Upgrade an existing site
79
80
 
81
+ **From the admin panel first.** On a standard install (the monolith started by its service, with its
82
+ own files — the `--systemd` layout) **Admin → Updates** lists the newer releases with their notes and
83
+ installs one with your password and a two-factor code: it takes a backup, runs this same command with
84
+ `--version <tag>`, restarts WordJS by itself and puts the previous version back if the new one fails to
85
+ install or to start. In a container, a git checkout, split or separate mode, or when the code belongs
86
+ to another account, the screen says so and gives you the command instead.
87
+
88
+ **From the command line:**
89
+
80
90
  ```bash
81
91
  cd .. && npx create-wordjs@latest upgrade my-site # or run it from inside: npx create-wordjs@latest upgrade .
82
92
  ```
83
93
 
84
94
  Downloads the newest release and replaces the app code while **preserving your data**: the database
85
- directory (`backend/data`), `backend/uploads/`, `wordjs-config.json`, `.env`, gateway secrets
86
- (`gateway/gateway-config.json`) and any user-installed plugins survive. It asks for confirmation
87
- before touching an existing install — on a non-interactive shell it refuses unless you pass `--yes`.
88
- Restart the server afterwards (schema migrations run automatically on the next start).
95
+ directory (`backend/data`), `backend/uploads/`, `backend/backups/`, `wordjs-config.json`, `.env`,
96
+ gateway secrets (`gateway/gateway-config.json`) and any user-installed plugins survive; the small config
97
+ files are also copied to a `.upgrade-backup-<time>/` folder first. It asks for confirmation before
98
+ touching an existing install — on a non-interactive shell it refuses unless you pass `--yes`. On a
99
+ site that is already on the target version it does nothing unless you pass `--force`. Restart the
100
+ server afterwards (`sudo systemctl restart wordjs` for a service); schema migrations run automatically
101
+ on the next start.
102
+
103
+ ```bash
104
+ npx create-wordjs@latest upgrade /srv/wordjs --yes # unattended
105
+ npx create-wordjs@latest upgrade /srv/wordjs --version v2.9.0 --yes # a given release
106
+ npx create-wordjs@latest upgrade /srv/wordjs --zip ./wordjs-v2.9.0.zip --sha256 <hex> --yes --force # offline, re-apply
107
+ ```
108
+
109
+ `--zip` takes a release ZIP you downloaded (or an `https://` URL) instead of asking GitHub, and
110
+ `--sha256` pins it to the value in `wordjs-<tag>.zip.sha256`. To go back, run `upgrade` with
111
+ `--version <old-tag>` (add `--force` if the site already reports that version); your data stays
112
+ intact. Run it as root (`sudo npx …`) when the site's files belong to the service account: the upgraded
113
+ files are given back to the site's owner.
89
114
 
90
115
  ## Run it as a service (Linux)
91
116
 
@@ -108,7 +133,10 @@ boot must run as the service account so that account owns the database and certi
108
133
 
109
134
  **Ports below 1024 (80/443).** The unit never grants `CAP_NET_BIND_SERVICE`. For a port below 1024,
110
135
  `--systemd` also stages `60-wordjs-ports.conf`, a `sysctl.d` drop-in that sets
111
- `net.ipv4.ip_unprivileged_port_start` to that port. Read its trade-off before installing it: it lets
136
+ `net.ipv4.ip_unprivileged_port_start` low enough for every port the site will bind: that port, `80` too
137
+ for an HTTPS site (Let's Encrypt HTTP-01, turned on later in the admin panel) and `25` with `--mail`
138
+ (incoming mail; implied on `upgrade` when the mail-server plugin is installed). Read its trade-off before
139
+ installing it: it lets
112
140
  **every** unprivileged process on the host bind ports from that value up to 1023, not just WordJS. On a
113
141
  shared host, prefer a reverse proxy (nginx, Caddy) on 80/443 forwarding to WordJS on a high port.
114
142
 
@@ -144,8 +172,11 @@ npx create-wordjs@latest join frontend --gateway 10.0.0.1 --token <t> --ca-hash
144
172
  `--ca-hash` is required: `join` refuses to enroll without it, and there is no trust-on-first-use
145
173
  opt-out. Each `join` downloads the release, enrolls against the gateway (the token authorizes exactly one
146
174
  certificate signing; it is burned afterwards, and the ones `gateway` printed also expire after 120
147
- minutes — mint more on the gateway with `node scripts/cluster.js token <backend|frontend>`, which
148
- defaults to a 60-minute TTL and takes `--ttl <minutes>`), then starts the service, which registers
175
+ minutes — once the first backend has joined, mint more from **Admin → Servers → Add a server**, which
176
+ also prints the `node-join` command for the new machine
177
+ ([managing the cluster from the admin panel](https://github.com/jaimemartinez/wordjs/blob/main/documentation/separate-mode.md#managing-the-cluster-from-the-admin-panel));
178
+ the second way is `node scripts/cluster.js token <backend|frontend>` on the gateway, which defaults to
179
+ a 60-minute TTL and takes `--ttl <minutes>`), then starts the service, which registers
149
180
  with the gateway over mTLS. Browse `https://<gateway>:3000` when all three are up. `join` machines
150
181
  need `openssl` on the PATH. Full details, port matrix and the manual (source-checkout) procedure:
151
182
  [documentation/separate-mode.md](https://github.com/jaimemartinez/wordjs/blob/main/documentation/separate-mode.md).
@@ -165,8 +196,8 @@ need `openssl` on the PATH. Full details, port matrix and the manual (source-che
165
196
  - **GitHub rate limit / offline**: the release lookup uses the unauthenticated GitHub API. If it
166
197
  is rate-limited or you're offline, download `wordjs-v*.zip` from the
167
198
  [releases page](https://github.com/jaimemartinez/wordjs/releases) and run
168
- `npx create-wordjs@latest my-site --zip ./wordjs-v2.1.0.zip` — add
169
- `--sha256 <hex>` with the value from `wordjs-v2.1.0.zip.sha256` to have it verified.
199
+ `npx create-wordjs@latest my-site --zip ./wordjs-v2.9.0.zip` — add
200
+ `--sha256 <hex>` with the value from `wordjs-v2.9.0.zip.sha256` to have it verified.
170
201
  - **Existing directories**: the target directory must not exist (or must be empty) — the tool
171
202
  refuses to overwrite anything.
172
203
 
@@ -189,9 +220,9 @@ need `openssl` on the PATH. Full details, port matrix and the manual (source-che
189
220
  ## What gets created
190
221
 
191
222
  A ready-to-run WordJS bundle: backend (pre-compiled to `dist/`), frontend (pre-built `.next`),
192
- gateway, the bundled plugins and the four bundled themes (`circuito`, `default`, `gaceta`,
193
- `vergel`). Marketplace plugins are **not** in the bundle — they ship as separate release assets and
194
- are installed from the admin. Secrets (JWT, DB password, install token) are generated
223
+ gateway and the four bundled themes (`circuito`, `default`, `gaceta`,
224
+ `vergel`). No plugin ships in the bundle: plugins come from the marketplace (separate release assets),
225
+ installed from **Admin → Plugins**. Secrets (JWT, DB password, install token) are generated
195
226
  locally during install — nothing sensitive ships in the bundle. See `INSTALL.md` inside the
196
227
  scaffolded directory for the manual steps and `documentation/deployment.md` for production
197
228
  deployment.
package/index.js CHANGED
@@ -84,8 +84,13 @@ Options:
84
84
  the first boot must run as that account.
85
85
  --port <n> (with --systemd) Public port, written into the unit as PORT=<n> (default:
86
86
  the site's configured port, else 3000). Below 1024 it also stages
87
- 60-wordjs-ports.conf (net.ipv4.ip_unprivileged_port_start), never a capability.
87
+ 60-wordjs-ports.conf (net.ipv4.ip_unprivileged_port_start), never a capability,
88
+ low enough for every port the site will bind: with HTTPS also 80 (Let's
89
+ Encrypt HTTP-01 from Admin -> Security), with --mail also 25.
88
90
  --service-user <name> (with --systemd) The account the service runs as (default: wordjs). Not root.
91
+ --mail (with --systemd) The site will receive mail (the mail-server plugin, SMTP on
92
+ port 25): the drop-in then lowers the floor to 25. Implied on upgrade when the
93
+ mail-server plugin is installed.
89
94
  -h, --help Show this help.
90
95
 
91
96
  Examples:
@@ -126,7 +131,7 @@ function parseArgs(argv) {
126
131
  const opts = {
127
132
  mode: 'create', dir: null, zip: null, version: null, http: false, start: true, yes: false, force: false, install: true,
128
133
  role: null, gateway: null, token: null, caHash: null, advertise: null, enrollPort: null, host: null,
129
- systemd: false, port: null, serviceUser: null,
134
+ systemd: false, port: null, serviceUser: null, mail: false,
130
135
  sha256: null,
131
136
  };
132
137
  // A leading subcommand selects the mode (default is the monolith create flow).
@@ -159,11 +164,12 @@ function parseArgs(argv) {
159
164
  else if (a === '--systemd') opts.systemd = true;
160
165
  else if (a === '--port') opts.port = argv[++i] || fail('--port needs a value (1-65535).');
161
166
  else if (a === '--service-user') opts.serviceUser = argv[++i] || fail('--service-user needs an account name.');
167
+ else if (a === '--mail') opts.mail = true;
162
168
  else if (a.startsWith('-')) fail(`Unknown option: ${a}`, 'Run with --help to see the available options.');
163
169
  else positionals.push(a);
164
170
  }
165
- if ((opts.port !== null || opts.serviceUser !== null) && !opts.systemd) {
166
- fail('--port and --service-user only apply together with --systemd.',
171
+ if ((opts.port !== null || opts.serviceUser !== null || opts.mail) && !opts.systemd) {
172
+ fail('--port, --service-user and --mail only apply together with --systemd.',
167
173
  'Without a unit, the port comes from the PORT environment variable or the site config (default 3000).');
168
174
  }
169
175
  if (opts.systemd) {
@@ -789,9 +795,10 @@ function underAny(p, roots) {
789
795
  * written into the file itself: whoever later finds it in /etc/sysctl.d/ should not have to guess why
790
796
  * it is there or what it costs.
791
797
  */
792
- function sysctlDropIn(start, { http = false } = {}) {
798
+ function sysctlDropIn(start, { http = false, mail = false, ports = [] } = {}) {
793
799
  const lines = [
794
800
  `# WordJS: allow unprivileged processes to bind ports from ${start} up (written by create-wordjs --systemd).`,
801
+ ...ports.map((p) => `# ${p.port}: ${p.why}`),
795
802
  '#',
796
803
  '# This is NOT a grant to WordJS alone. It lowers the privileged-port floor for EVERY unprivileged',
797
804
  `# process in this network namespace, so any local user or service may bind ports ${start}-1023 too, and`,
@@ -802,6 +809,9 @@ function sysctlDropIn(start, { http = false } = {}) {
802
809
  if (!http && start > 80) {
803
810
  lines.push(`# Let's Encrypt HTTP-01 (acme.http01Port: 80 in wordjs-config.json) needs this lowered to 80.`);
804
811
  }
812
+ if (!mail && start > 25) {
813
+ lines.push('# Receiving mail (the mail-server plugin, SMTP on port 25) needs this lowered to 25: re-run with --mail.');
814
+ }
805
815
  lines.push(
806
816
  '#',
807
817
  `# Install: sudo install -o root -g root -m 0644 ${SYSCTL_DROPIN_FILE} /etc/sysctl.d/ && sudo sysctl --system`,
@@ -821,8 +831,14 @@ function sysctlDropIn(start, { http = false } = {}) {
821
831
  * configuredPort the port the site's config already sets (gatewayPort), or null.
822
832
  * acmeHttp01Port acme.http01Port from the config, or null: monolith.js binds it beside an HTTPS
823
833
  * listener, so it counts toward the privileged-port floor.
834
+ * mail the site receives mail: the mail-server plugin listens for SMTP on 25.
835
+ *
836
+ * The floor goes as low as every port the site will bind, not only the ones configured today: the
837
+ * administrator turns on Let's Encrypt (HTTP-01 on 80) and installs the mail server from the panel,
838
+ * and neither may then need a file edited as root. An HTTPS site already on a port below 1024 counts
839
+ * 80 for that reason; a site on a high port gets no drop-in (it sits behind a proxy, or wants none).
824
840
  */
825
- function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER, port = null, configuredPort = null, acmeHttp01Port = null, http = false }) {
841
+ function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER, port = null, configuredPort = null, acmeHttp01Port = null, http = false, mail = false }) {
826
842
  for (const [what, p] of [['site directory', installDir], ['node binary', nodePath]]) {
827
843
  if (!unitSafePath(p)) {
828
844
  const err = new Error(`The ${what} path "${p}" cannot go into a systemd unit as is: it must be absolute, without spaces, quotes, backslashes, "%" or "$".`);
@@ -836,10 +852,14 @@ function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER,
836
852
  if (problem) { const err = new Error(problem.message); err.hint = problem.hint; throw err; }
837
853
 
838
854
  const publicPort = port || configuredPort || 3000;
839
- const bound = [publicPort];
840
- if (!http && acmeHttp01Port) bound.push(acmeHttp01Port);
841
- const privileged = bound.filter((p) => p < PRIVILEGED_PORT_LIMIT);
842
- const sysctlStart = privileged.length ? Math.min(...privileged) : null;
855
+ const bound = [{ port: publicPort, why: 'the site (its public port)' }];
856
+ if (!http && acmeHttp01Port) bound.push({ port: acmeHttp01Port, why: "Let's Encrypt HTTP-01 (acme.http01Port)" });
857
+ if (!http && publicPort < PRIVILEGED_PORT_LIMIT && !bound.some((b) => b.port === 80)) {
858
+ bound.push({ port: 80, why: "Let's Encrypt HTTP-01, when it is turned on in Admin -> Security" });
859
+ }
860
+ if (mail) bound.push({ port: 25, why: 'incoming mail (the mail-server plugin, SMTP)' });
861
+ const privileged = bound.filter((b) => b.port < PRIVILEGED_PORT_LIMIT).sort((a, b) => a.port - b.port);
862
+ const sysctlStart = privileged.length ? privileged[0].port : null;
843
863
 
844
864
  // Two hardening directives depend on WHERE things are, and each is dropped (with a warning) rather
845
865
  // than shipped in a shape that cannot start: ProtectHome= hides /home, /root and /run/user, which
@@ -915,7 +935,9 @@ function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER,
915
935
 
916
936
  return {
917
937
  unit,
918
- sysctl: sysctlStart ? { start: sysctlStart, content: sysctlDropIn(sysctlStart, { http }) } : null,
938
+ sysctl: sysctlStart
939
+ ? { start: sysctlStart, ports: privileged.map((b) => b.port), content: sysctlDropIn(sysctlStart, { http, mail, ports: privileged }) }
940
+ : null,
919
941
  publicPort,
920
942
  warnings,
921
943
  };
@@ -937,7 +959,7 @@ function systemdInstallSteps({ installDir, user = DEFAULT_SERVICE_USER, built, h
937
959
  // `install -o root -g root -m 0644` and not `cp`: root takes the staged file's CONTENT into a file it
938
960
  // creates itself, root-owned and not writable by anyone else, whatever the staged copy's mode was.
939
961
  if (built.sysctl) {
940
- step(`Let it bind port ${built.publicPort} without a capability (read the trade-off in the file first):`,
962
+ step(`Let it bind ${built.sysctl.ports.length > 1 ? `ports ${built.sysctl.ports.join(', ')}` : `port ${built.sysctl.start}`} without a capability (read the trade-off in the file first):`,
941
963
  `sudo install -o root -g root -m 0644 ${staged(SYSCTL_DROPIN_FILE)} /etc/sysctl.d/${SYSCTL_DROPIN_FILE} && sudo sysctl --system`);
942
964
  } else if (upgrade) {
943
965
  // A site that moved to a high port (behind a proxy, say) no longer needs the floor lowered, and an
@@ -1046,12 +1068,14 @@ function emitSystemd(installDir, opts) {
1046
1068
  configuredPort: Number(appConfig.gatewayPort || gwConfig.gatewayPort) || null,
1047
1069
  acmeHttp01Port: Number(appConfig.acme && appConfig.acme.http01Port) || null,
1048
1070
  http: opts.http,
1071
+ // An installed mail server (an upgrade) receives on 25 whether or not --mail was given.
1072
+ mail: !!opts.mail || fs.existsSync(path.join(installDir, 'backend', 'plugins', 'mail-server')),
1049
1073
  });
1050
1074
  const stageDir = createSystemdStagingDir();
1051
1075
  const written = writeSystemdFiles(stageDir, built);
1052
1076
  console.log('');
1053
1077
  console.log(` systemd unit staged: ${written.unitPath}`);
1054
- if (written.sysctlPath) console.log(` sysctl drop-in for port ${built.publicPort}: ${written.sysctlPath}`);
1078
+ if (written.sysctlPath) console.log(` sysctl drop-in for ports from ${built.sysctl.start} up (${built.sysctl.ports.join(', ')}): ${written.sysctlPath}`);
1055
1079
  console.log(' (staged outside the site on purpose: the service account will own the site, and must not be able to edit what root installs)');
1056
1080
  for (const w of built.warnings) console.log(` ⚠️ ${w}`);
1057
1081
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-wordjs",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
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)",