@inoku/lepan 0.7.69 → 0.7.71

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  This public repository contains versioned LifeDrive installation assets built from the private LifeAlbum source repository.
4
4
 
5
+ ## Version 0.7.70
6
+
7
+ The account service now serializes login renewal for each user, preventing
8
+ simultaneous requests from overwriting temporary login files. It also discovers
9
+ Leither's current local service port before each request, verifies the node's
10
+ identity before using a changed endpoint, and follows port changes without an
11
+ account-service restart. The existing mobile login protocol and stored data
12
+ formats are unchanged.
13
+
14
+ Both fixes are included in the packaged service for Linux, macOS and Windows.
15
+
5
16
  For an ordinary Windows or macOS installation, download the matching package from LePan's Cloudflare-backed download URLs:
6
17
 
7
18
  - **Windows 10/11 or Windows Server (x64):** [LePan-Setup-windows-x64.exe](https://lepan.org/download/LePan-Setup-windows-x64.exe)
@@ -88,6 +99,27 @@ LifeDrive requires Leither V0.24.11 or newer. Linux/macOS browser-owner setup cr
88
99
 
89
100
  After successful npm setup, the installer displays the QR code at the end of its output whenever the terminal is wide enough. If the whole code cannot fit on screen, it automatically opens the full-size QR image in the computer's image viewer. In the iPhone or Android app, open **Settings → Set up users → Scan QR code** and scan the displayed code. A private `lepan-pairing-<id>.png` is also saved, with its path printed as a fallback for remote sessions or computers without an image viewer. A private `.json` invitation is saved alongside it for apps without a setup scanner: transfer that file to the phone and use **Choose identity file**. These files are saved in the Leither root on Linux/macOS and its private `.lifedrive-household` directory on Windows. Household invitations expire ten minutes after creation; legacy identity codes contain a device private key, so keep them private and delete the transfer copies after pairing. Legacy `--add-device` and `--upgrade` also produce a code when an identity is available; household upgrades preserve existing pairings without issuing new invitations.
90
101
 
102
+ If the terminal is too narrow, the output gives the required width and an exact
103
+ command to redisplay the saved code after widening the window. Run it on the node
104
+ when connected over SSH. For npm installations, the command has this form:
105
+
106
+ ```bash
107
+ npx --yes @inoku/lepan@latest --show-qr '/path/to/lepan-pairing.json'
108
+ ```
109
+
110
+ This displays the saved code without installing, upgrading, or changing device
111
+ authorization. Desktop packages print a command using their private Node runtime.
112
+ Redisplaying an invitation does **not** reset its ten-minute expiry; output shows
113
+ its expiry in UTC and how to renew it. A legacy `lifedrive-device-identity` file
114
+ is a persistent device key and has no ten-minute expiry; it remains usable while
115
+ authorized. Keep its original safe and offline, and delete transfer copies after
116
+ pairing. Upgrades keep the output brief: the existing identity path or saved-pairing
117
+ directory, the QR and file paths when available, and a help command. If the QR
118
+ cannot fit, the output gives the required width and the command to redisplay it.
119
+ Use `--help` for expiry, file import, and account reconnection instructions.
120
+ Initial setup's local browser links are labeled with the node's detected LAN
121
+ interfaces; these are node addresses, not router or gateway addresses.
122
+
91
123
  ### Renew an expired first-phone invitation
92
124
 
93
125
  Run this again **on the computer running Leither**:
package/USER_MANUAL.md CHANGED
@@ -147,13 +147,32 @@ This keeps what step 2.1 installed and adds users and phones:
147
147
  The setup invitation is what makes a phone the first user of this node. Treat it
148
148
  like a key: do not post it, email it to others, or paste it anywhere public.
149
149
 
150
+ If the terminal is too narrow for the QR, widen it to the number of columns
151
+ printed by setup, then run the printed `--show-qr` command. Over SSH, run the
152
+ command on the node in that same terminal. It displays the saved code without
153
+ installing or upgrading again. You can also open the saved PNG image or transfer
154
+ the saved JSON privately to your phone and use **Choose identity file**.
155
+ For npm installations:
156
+
157
+ ```bash
158
+ npx --yes @inoku/lepan@latest --show-qr '/path/to/lepan-pairing.json'
159
+ ```
160
+
161
+ The Windows and macOS desktop installers print an equivalent command using their
162
+ bundled runtime. Redisplaying a code does not renew it: setup and reconnect
163
+ invitations expire ten minutes after creation. The output shows the expiry in
164
+ UTC. Older browser-owner installations use a `lifedrive-device-identity` file
165
+ containing a persistent device private key; that identity has no ten-minute
166
+ expiry and remains usable while authorized. Keep its original safe and offline;
167
+ delete transfer copies after pairing.
168
+
150
169
  ### 2.3 Create the first user from your phone
151
170
 
152
171
  Within ten minutes of the invitation being printed:
153
172
 
154
- 1. Scan the installer’s final QR code or the QR image that opens automatically. The saved image path is a fallback for remote sessions or computers without an image viewer. If you used the standalone shell installer, copy the whole invitation block instead.
173
+ 1. Leave the installer’s final QR code or the QR image that opens automatically visible on the computer. The saved image path is a fallback for remote sessions or computers without an image viewer. If you used the standalone shell installer, copy the whole invitation block instead.
155
174
  2. Open LifeDrive on the phone. The **Set up your LifeDrive** screen opens by itself (or open **Settings → Set up users**).
156
- 3. Enter **Your name** and a name for this device.
175
+ 3. Review **Your name** and the name for this device. Android fills in **Owner** and the phone model; you can change either.
157
176
  4. Tap **Scan QR code** and point the phone at the terminal or saved image. You can also use **Paste node identity** or **Choose identity file**.
158
177
 
159
178
  The phone creates its own device key, claims the node, and opens your personal
@@ -204,6 +223,14 @@ To install a specific release instead of the latest, name it, for example
204
223
  `npx --yes @inoku/lepan@0.7.35 --upgrade`. Release versions are never reused,
205
224
  so a version number always means the same files.
206
225
 
226
+ Upgrade output shows the existing identity path or saved-pairing directory and a
227
+ help command. When a QR cannot fit, it shows the required terminal width and the
228
+ `--show-qr` command to run after widening the window. Use `--help` for file import,
229
+ identity expiry, and device-management instructions. Household
230
+ upgrades preserve paired phones and do not issue or renew an invitation; to
231
+ reconnect to an existing household account, run `--recover-device` with its name
232
+ or user ID on the node. For another device, use **My devices** on a paired phone.
233
+
207
234
  The installer saves the previous application files to a backup directory, prints
208
235
  where, and then:
209
236
 
package/bin/lifedrive.js CHANGED
@@ -11,6 +11,17 @@ const { pathToFileURL } = require("node:url");
11
11
  const distributionDirectory = path.resolve(__dirname, "..", "dist");
12
12
  const bundlePath = path.join(distributionDirectory, "lifedrive-bundle.tar.gz");
13
13
  const checksumPath = `${bundlePath}.sha256`;
14
+ const packageVersion = require("../package.json").version;
15
+ // Desktop installers carry Node privately; their users need not have npx.
16
+ const pairingCommand = path.basename(path.resolve(__dirname, "..")) === "package"
17
+ ? `${process.platform === "win32" ? "& " : ""}${quoteArgument(process.execPath)} ${quoteArgument(__filename)}`
18
+ : `npx --yes @inoku/lepan@${/^\d+\.\d+\.\d+$/.test(packageVersion) ? packageVersion : "latest"}`;
19
+
20
+ function quoteArgument(value) {
21
+ return process.platform === "win32"
22
+ ? `'${value.replace(/'/g, "''")}'`
23
+ : `'${value.replace(/'/g, "'\\''")}'`;
24
+ }
14
25
 
15
26
  function openPairingImage(imagePath) {
16
27
  // A remote server must not open a window in another user's desktop session.
@@ -37,15 +48,16 @@ function openPairingImage(imagePath) {
37
48
  return !result.error && result.status === 0;
38
49
  }
39
50
 
40
- async function showPairingCode(resultPath) {
51
+ async function showPairingCode(resultPath, savedFile = false) {
41
52
  if (!fs.existsSync(resultPath)) return;
53
+ const compact = savedFile || (process.argv.includes("--upgrade") && !process.argv.includes("--household"));
42
54
  // The child writes this only after setup succeeds. Never scrape its output:
43
55
  // it can contain node keys as well as the mobile device's pairing identity.
44
56
  const pairing = JSON.parse(fs.readFileSync(resultPath, "utf8"));
45
57
  if (pairing.version !== 1 || !["lifedrive-device-identity", "lifedrive-node-setup", "lifedrive-user-identity"].includes(pairing.type)) {
46
58
  throw new Error("Unsupported pairing data");
47
59
  }
48
- const directory = fs.readFileSync(`${resultPath}.directory`, "utf8");
60
+ const directory = savedFile ? path.dirname(resultPath) : fs.readFileSync(`${resultPath}.directory`, "utf8");
49
61
  if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
50
62
  const payload = JSON.stringify(pairing);
51
63
  // Only encode fields consumed by the existing Android and iOS setup parsers.
@@ -60,17 +72,26 @@ async function showPairingCode(resultPath) {
60
72
  : payload;
61
73
  const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
62
74
  const imagePath = `${basename}.png`;
63
- const identityPath = `${basename}.json`;
75
+ const identityPath = savedFile ? resultPath : `${basename}.json`;
64
76
  // Older mobile releases support importing this file but have no setup scanner.
65
77
  // Keep a private copy even when QR generation succeeds.
66
- fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
78
+ if (!savedFile) fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
67
79
  const options = { errorCorrectionLevel: "M", margin: 4, scale: 8 };
68
80
  const privacyNote = pairing.type !== "lifedrive-device-identity"
69
81
  ? "Keep it private. This invitation expires ten minutes after creation."
70
- : "This contains your device private key. Keep it private and delete it after pairing.";
82
+ : "This is a persistent device identity; it does not expire after ten minutes. It remains usable while the device is authorized.\nIt contains your device private key. Keep the original safe and offline; delete transfer copies after pairing.";
83
+ const expiry = pairing.type !== "lifedrive-device-identity" && Number.isFinite(pairing.expires)
84
+ ? `\nInvitation expiry: ${new Date(pairing.expires * 1000).toISOString()} (UTC).\n${pairing.expires <= Date.now() / 1000 ? "This invitation has expired. Create a fresh invitation before pairing.\n" : ""}` : "";
85
+ const reopenHint = `\nTo show this QR again, run on this computer (on the node for an SSH session):\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}\nThis only displays the saved pairing code; it does not install or upgrade LePan.\n`;
86
+ const nodeRoot = process.platform === "win32" && path.basename(directory) === ".lifedrive-household"
87
+ ? path.dirname(directory) : directory;
88
+ // A saved file may have been moved off the node. Its new parent is not proof
89
+ // of the original Leither root; let node discovery choose in that case.
90
+ const renewalRoot = savedFile ? "" : ` --leither-root ${quoteArgument(nodeRoot)}`;
71
91
  const renewalHint = pairing.type === "lifedrive-node-setup"
72
- ? "\nInvitation expired? Run this again on the node:\n npx --yes @inoku/lepan@latest --household\n"
73
- : pairing.type === "lifedrive-user-identity" ? "\nInvitation expired? Run the same reconnect command again on this computer.\n" : "";
92
+ ? `\nInvitation expired? Run this again on the node before the first owner is created:\n ${pairingCommand}${renewalRoot} --household\n`
93
+ : pairing.type === "lifedrive-user-identity"
94
+ ? `\nInvitation expired? Run the same reconnect command again on the node${pairing.user_id ? `:\n ${pairingCommand}${renewalRoot} --recover-device ${quoteArgument(pairing.user_id)}` : "."}\n` : "";
74
95
  let modules;
75
96
  try {
76
97
  const QRCode = require("qrcode");
@@ -82,11 +103,22 @@ async function showPairingCode(resultPath) {
82
103
  } catch {
83
104
  // The child no longer prints secrets. Preserve an importable fallback
84
105
  // before runInstaller removes the temporary handoff directory.
106
+ if (compact) {
107
+ process.stderr.write(`\nThe QR code could not be created. In LePan, open Settings > Set up users > Choose identity file.\nPairing file: ${identityPath}\nKeep it private.\n`);
108
+ process.stderr.write(expiry);
109
+ process.stderr.write(`\nHelp:\n ${pairingCommand} --help\n`);
110
+ return;
111
+ }
85
112
  process.stderr.write("\nNext: pair your phone\n\nThe QR code could not be created. Use the saved pairing file instead.\n");
86
113
  process.stderr.write(` ${identityPath}\n\n`);
87
114
  process.stderr.write("Transfer this file to your phone, then select it in LePan > Settings > Set up users > Choose identity file.\n");
88
115
  process.stderr.write(`${privacyNote}\n`);
116
+ process.stderr.write(expiry);
117
+ process.stderr.write(reopenHint);
89
118
  process.stderr.write(renewalHint);
119
+ if (pairing.type !== "lifedrive-device-identity") {
120
+ process.stderr.write("Redisplaying the QR does not extend the invitation's ten-minute expiry.\n");
121
+ }
90
122
  return;
91
123
  }
92
124
  // Never hide a terminal code just because the window is short. Open the PNG
@@ -97,17 +129,43 @@ async function showPairingCode(resultPath) {
97
129
  const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
98
130
  const fitsScreen = fitsWidth && process.stdout.rows >= height + 3;
99
131
  const imageOpened = !fitsScreen && openPairingImage(imagePath);
100
- process.stdout.write("\nPair your phone\n\n");
101
- process.stdout.write("Open LePan > Settings > Set up users > Scan QR code.\n");
102
- process.stdout.write(imageOpened
103
- ? "The QR image has opened automatically. Scan it with your phone.\n"
104
- : fitsWidth ? "Scan the QR code below with your phone.\n"
105
- : "This terminal is too narrow for the QR code and no image viewer could be opened. Open the saved image below.\n");
106
- process.stdout.write(`${privacyNote}\n`);
107
- process.stdout.write(`\nSaved QR image: ${imagePath}\n`);
108
- process.stdout.write(`Pairing file (Choose identity file): ${identityPath}\n`);
109
- process.stdout.write("Keep this computer on and connected while using LePan.\n");
110
- process.stdout.write(renewalHint);
132
+ if (compact) {
133
+ if (savedFile) process.stdout.write(`\nIdentity file: ${identityPath}\nKeep it private.\n`);
134
+ process.stdout.write("\nHow to pair your phone:\nOpen LePan > Settings > Set up users > Scan QR code.\n");
135
+ if (imageOpened) process.stdout.write("\nThe QR image has opened automatically.\n");
136
+ if (!fitsWidth) {
137
+ process.stdout.write(process.stdout.isTTY
138
+ ? `\nThis terminal is too narrow for the QR code. To display it here, widen the terminal\nto at least ${width + 1} columns, then run:\n`
139
+ : "\nTo display the QR code, run in an interactive terminal:\n");
140
+ process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}\n`);
141
+ }
142
+ process.stdout.write(`\nSaved QR image: ${imagePath}\nPairing file (Choose identity file): ${identityPath}\n`);
143
+ process.stdout.write(expiry);
144
+ process.stdout.write(`\nHelp:\n ${pairingCommand} --help\n`);
145
+ } else {
146
+ process.stdout.write("\nPair your phone\n\n");
147
+ process.stdout.write("Open LePan > Settings > Set up users > Scan QR code.\n");
148
+ process.stdout.write(imageOpened
149
+ ? "The QR image has opened automatically. Scan it with your phone.\n"
150
+ : fitsWidth ? "Scan the QR code below with your phone.\n"
151
+ : process.stdout.isTTY
152
+ ? "This terminal is too narrow for the QR code. You can open the saved QR image.\n"
153
+ : "A QR code cannot be displayed in this output. Run the --show-qr command below in an interactive terminal, or open the saved QR image.\n");
154
+ if (!fitsWidth && process.stdout.isTTY) {
155
+ process.stdout.write(`To display it here, widen the terminal to at least ${width + 1} columns,\nthen run the --show-qr command below.\n`);
156
+ }
157
+ process.stdout.write(`${privacyNote}\n`);
158
+ process.stdout.write(expiry);
159
+ process.stdout.write(`\nSaved QR image: ${imagePath}\n`);
160
+ process.stdout.write(`Pairing file (Choose identity file): ${identityPath}\n`);
161
+ process.stdout.write("To import the file, transfer it privately to your phone, then open\nLePan > Settings > Set up users > Choose identity file.\n");
162
+ process.stdout.write(reopenHint);
163
+ process.stdout.write("Keep this computer on and connected while using LePan.\n");
164
+ process.stdout.write(renewalHint);
165
+ if (pairing.type !== "lifedrive-device-identity") {
166
+ process.stdout.write("Redisplaying the QR does not extend the invitation's ten-minute expiry.\n");
167
+ }
168
+ }
111
169
  if (fitsWidth) {
112
170
  process.stdout.write("\n");
113
171
  // Explicit colors and a four-module quiet zone work on light/dark themes.
@@ -130,7 +188,7 @@ async function runInstaller(command, args) {
130
188
  try {
131
189
  const result = spawnSync(command, args, {
132
190
  stdio: "inherit",
133
- env: { ...process.env, LIFEDRIVE_PAIRING_RESULT: resultPath },
191
+ env: { ...process.env, LIFEDRIVE_PAIRING_RESULT: resultPath, LIFEDRIVE_PAIRING_COMMAND: pairingCommand },
134
192
  });
135
193
  if (result.error) throw result.error;
136
194
  if (typeof result.status === "number") process.exitCode = result.status;
@@ -142,6 +200,7 @@ async function runInstaller(command, args) {
142
200
  catch {
143
201
  // Parser errors may contain identity text; never print them.
144
202
  process.stderr.write("\nPhone pairing could not be prepared. Use your existing identity file, or create a new setup invitation from the node.\n");
203
+ process.stderr.write(`\nHelp:\n ${pairingCommand} --help\n`);
145
204
  }
146
205
  }
147
206
  } finally {
@@ -182,6 +241,43 @@ function windowsArguments(values) {
182
241
  }
183
242
 
184
243
  async function main() {
244
+ const argumentsList = process.argv.slice(2);
245
+ if (argumentsList.some(argument => argument === "--show-qr" || argument.startsWith("--show-qr="))) {
246
+ const file = argumentsList[0] === "--show-qr" && argumentsList.length === 2
247
+ ? argumentsList[1]
248
+ : argumentsList.length === 1 && argumentsList[0].startsWith("--show-qr=")
249
+ ? argumentsList[0].slice("--show-qr=".length) : "";
250
+ if (!file || file.startsWith("--")) throw new Error("Use --show-qr FILE by itself to display a saved identity or invitation.");
251
+ const identityPath = path.resolve(file);
252
+ if (!fs.existsSync(identityPath)) throw new Error("The saved identity or invitation file was not found. Use the path printed during setup or upgrade.");
253
+ // JSON parse failures can contain private identity data. Keep them off stderr.
254
+ try { await showPairingCode(identityPath, true); }
255
+ catch { throw new Error("Could not display the pairing file. Choose a readable LePan identity or invitation JSON file."); }
256
+ return;
257
+ }
258
+ if (argumentsList.includes("--help") || argumentsList.includes("-h")) {
259
+ process.stdout.write(`Pairing help:
260
+ --show-qr FILE Display a saved identity or invitation as a QR code.
261
+
262
+ Example:
263
+ ${pairingCommand} --show-qr '/path/to/lepan-pairing.json'
264
+
265
+ Run pairing commands on the computer running Leither. If the terminal is too
266
+ narrow, widen it to the printed number of columns and run --show-qr again.
267
+ You can also open the saved PNG, or transfer the JSON privately to your phone
268
+ and use LePan > Settings > Set up users > Choose identity file.
269
+
270
+ Device identity files contain persistent private keys and have no ten-minute
271
+ expiry. Keep the original safe and offline; delete transfer copies after pairing.
272
+ Setup and reconnect invitations expire ten minutes after creation. Showing the
273
+ QR again does not renew them. Use --household to renew an unclaimed first-owner
274
+ invitation, or --recover-device ACCOUNT to reconnect an existing account.
275
+ For another household device, use Settings > My devices on a paired phone.
276
+ If a saved identity file is missing, restore your private backup or use the
277
+ device-management options below.
278
+
279
+ `);
280
+ }
185
281
  if (process.platform === "win32") {
186
282
  const installerPath = path.join(distributionDirectory, "lifedrive-install.ps1");
187
283
  if (![installerPath, bundlePath, checksumPath].every(candidate => fs.existsSync(candidate))) {
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/lifedrive-bundle.tar.gz
3
- size: 44204388 bytes
4
- sha256: b1d6c652dbda51b836a30778abb722774fd28559373cee7efe2670991d7297b7
3
+ size: 44302951 bytes
4
+ sha256: 093f14e7fe7816a667e81c2ae300a2b0c1b0c6577e81b7eb83eac6a1d38f6b16
@@ -1 +1 @@
1
- b1d6c652dbda51b836a30778abb722774fd28559373cee7efe2670991d7297b7 lifedrive-bundle.tar.gz
1
+ 093f14e7fe7816a667e81c2ae300a2b0c1b0c6577e81b7eb83eac6a1d38f6b16 lifedrive-bundle.tar.gz
@@ -472,6 +472,11 @@ if ($Upgrade) {
472
472
  throw
473
473
  }
474
474
  Write-Host "LePan Windows upgrade complete. Leither was not restarted."
475
+ $pairingLauncher = if ($env:LIFEDRIVE_PAIRING_COMMAND) { $env:LIFEDRIVE_PAIRING_COMMAND } else { "npx --yes @inoku/lepan@latest" }
476
+ Write-Host "Saved pairing files, if retained: $root\.lifedrive-household\lepan-pairing-*.json (with matching .png images)."
477
+ Write-Host ""
478
+ Write-Host "Help:"
479
+ Write-Host " $pairingLauncher --help"
475
480
  return
476
481
  }
477
482
  try {
@@ -32,15 +32,24 @@ Installer options:
32
32
  --household Set up household users or renew an unclaimed setup invitation.
33
33
  --recover-device USER Reconnect a phone to an existing account, using its name or user ID.
34
34
  --household-config FILE Install mobile-led household setup using a private service configuration.
35
+ --add-device LABEL Create a separate device identity (legacy browser-owner setup, Linux/macOS).
35
36
 
36
37
  Setup options are forwarded to lifeDrive-setup.sh. Common examples:
37
38
  --registry-url URL
38
39
  --publisher-key FILE
39
- --add-device LABEL
40
40
  --identity-out FILE
41
41
  --list-devices
42
42
  --revoke-device UID
43
43
  --skip-domain
44
+
45
+ Create an identity for another phone or browser:
46
+ npx --yes @inoku/lepan@latest --add-device "My iPhone"
47
+
48
+ LABEL is a name you choose to recognize the device, such as "My iPhone".
49
+ Each device gets its own key and can be revoked independently. The npm installer
50
+ displays the new identity's QR code and file path for pairing.
51
+ Run this command on the computer running Leither.
52
+ For household accounts, invite another device from Settings > My devices on a paired phone.
44
53
  EOF
45
54
  }
46
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoku/lepan",
3
- "version": "0.7.69",
3
+ "version": "0.7.71",
4
4
  "description": "Install LePan and its Leither runtime on Linux, macOS, or Windows",
5
5
  "os": [
6
6
  "linux",