@inoku/lepan 0.7.65 → 0.7.67

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
@@ -86,7 +86,31 @@ The Linux/macOS legacy browser-owner setup uses the built-in AV1 registration en
86
86
 
87
87
  LifeDrive requires Leither V0.24.11 or newer. Linux/macOS browser-owner setup creates a separate Ed25519 `sodiumv2` identity for the primary browser. Windows prints a private ten-minute invitation for the first paired phone; later users and devices are managed from a paired device.
88
88
 
89
- After successful npm setup, the installer saves a private `lepan-pairing-<id>.png` and prints its full path. It also displays the QR code when the terminal is wide enough. In the iPhone or Android app, open **Settings → Set up users → Scan QR code** and scan the terminal or the saved image. The PNG is 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 image 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.
89
+ 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
+
91
+ ### Renew an expired first-phone invitation
92
+
93
+ Run this again **on the computer running Leither**:
94
+
95
+ ```bash
96
+ npx --yes @inoku/lepan@latest --household
97
+ ```
98
+
99
+ If more than one node is running, also pass `--leither-root` with the same node
100
+ directory used during installation. On Windows, use an administrator terminal.
101
+ The macOS setup package also supports renewal when run again.
102
+
103
+ Before the first owner has been created, setup replaces the previous invitation
104
+ and produces a fresh QR image and import file with a new ten-minute expiry. It
105
+ briefly stops only the managed account service while issuing the invitation,
106
+ then starts it and verifies readiness before displaying the new code. If issuance
107
+ fails, it attempts to restore the service. Leither keeps running; existing node
108
+ keys and drive data are preserved.
109
+
110
+ Setup never replaces an existing owner or an interrupted account claim. If users
111
+ already exist, use **My devices** on a paired phone to invite another device. An
112
+ interrupted first claim must be resumed on the original phone with its original
113
+ invitation. Ordinary `--upgrade` does not renew invitations.
90
114
 
91
115
  An existing installation is backed up before replacement. The explicit `--upgrade` path requires the saved application and key-auth owner state before changing files, synchronizes the same published application MID, and does not rerun device or domain setup. This release intentionally does not migrate the earlier password-owner prototype. Application files are active only after Leither advances the application from `cur` to `last`; a publication timeout leaves the prior `last` version serving users.
92
116
 
package/USER_MANUAL.md CHANGED
@@ -142,8 +142,7 @@ This keeps what step 2.1 installed and adds users and phones:
142
142
 
143
143
  1. Creates LifeDrive's private configuration in `<Leither root>/.lifedrive-household/`.
144
144
  2. Installs and starts the identity service: `lifedrive-identity` through systemd on Linux, `uk.inoku.lifedrive-identity` through launchd on macOS, or `LifeDrive Identity` through Task Scheduler on Windows. Leither itself is not restarted and must be running separately.
145
- 3. Prints a **setup invitation**: a block of text, followed by
146
- *"Keep the invitation above private. It expires in ten minutes."*
145
+ 3. Displays a **setup invitation** as a QR code. If the terminal is too small, the full-size QR image opens automatically on this computer. The invitation expires in ten minutes.
147
146
 
148
147
  The setup invitation is what makes a phone the first user of this node. Treat it
149
148
  like a key: do not post it, email it to others, or paste it anywhere public.
@@ -152,7 +151,7 @@ like a key: do not post it, email it to others, or paste it anywhere public.
152
151
 
153
152
  Within ten minutes of the invitation being printed:
154
153
 
155
- 1. Keep the installer’s final QR code visible, or open the saved `lepan-pairing-<id>.png` using the path it prints. If you used the standalone shell installer, copy the whole invitation block instead.
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.
156
155
  2. Open LifeDrive on the phone. The **Set up your LifeDrive** screen opens by itself (or open **Settings → Set up users**).
157
156
  3. Enter **Your name** and a name for this device.
158
157
  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**.
@@ -168,7 +167,7 @@ empty page, or **Settings → Set up users**, opens it again.
168
167
 
169
168
  | Situation | What to do |
170
169
  |---|---|
171
- | The invitation expired before you pasted it | On the node, stop the identity service (`sudo systemctl stop lifedrive-identity` on Linux, or `sudo launchctl bootout system/uk.inoku.lifedrive-identity` on macOS) and run the command from 2.2 again for a fresh invitation. |
170
+ | The invitation expired before you pasted it | On the node, run `npx --yes @inoku/lepan@latest --household` again for a fresh QR code. Setup renews an unclaimed invitation and preserves the existing identity; no manual service commands are needed. |
172
171
  | You pasted it, but the app lost its connection or was closed | Open the app on **the same phone** and paste **the same invitation** again. A claim that has started can finish after the ten minutes have passed. Do not delete the app or ask for a new invitation: that would abandon the phone's pending key. |
173
172
  | Setup says the node already has users | The node has been claimed. Add further devices from **My devices** (section 4), not with a new setup invitation. |
174
173
 
package/bin/lifedrive.js CHANGED
@@ -12,6 +12,31 @@ const distributionDirectory = path.resolve(__dirname, "..", "dist");
12
12
  const bundlePath = path.join(distributionDirectory, "lifedrive-bundle.tar.gz");
13
13
  const checksumPath = `${bundlePath}.sha256`;
14
14
 
15
+ function openPairingImage(imagePath) {
16
+ // A remote server must not open a window in another user's desktop session.
17
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
18
+ let command;
19
+ let args;
20
+ if (process.platform === "darwin") {
21
+ command = "/usr/bin/open";
22
+ args = [imagePath];
23
+ } else if (process.platform === "win32") {
24
+ command = "powershell.exe";
25
+ args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command",
26
+ "Start-Process -FilePath $env:LEPAN_PAIRING_IMAGE -ErrorAction Stop"];
27
+ } else if (process.platform === "linux" && (process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) {
28
+ command = "xdg-open";
29
+ args = [imagePath];
30
+ } else {
31
+ return false;
32
+ }
33
+ const result = spawnSync(command, args, {
34
+ stdio: "ignore", timeout: 5000,
35
+ env: { ...process.env, LEPAN_PAIRING_IMAGE: imagePath },
36
+ });
37
+ return !result.error && result.status === 0;
38
+ }
39
+
15
40
  async function showPairingCode(resultPath) {
16
41
  if (!fs.existsSync(resultPath)) return;
17
42
  // The child writes this only after setup succeeds. Never scrape its output:
@@ -22,24 +47,69 @@ async function showPairingCode(resultPath) {
22
47
  }
23
48
  const directory = fs.readFileSync(`${resultPath}.directory`, "utf8");
24
49
  if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
25
- const QRCode = require("qrcode");
26
50
  const payload = JSON.stringify(pairing);
51
+ // Only encode fields consumed by the existing Android and iOS setup parsers.
52
+ // The node enforces expiry and returns account details after a successful claim.
53
+ // Keep the full invitation in the import file, and leave legacy identities intact.
54
+ // iOS still requires certificate_sha256 even when Leither transport uses a key.
55
+ const qrPayload = pairing.type === "lifedrive-node-setup"
56
+ ? JSON.stringify(Object.fromEntries([
57
+ "type", "version", "app_id", "node_id", "endpoint", "transport",
58
+ "transport_key", "certificate_sha256", "request_id", "secret",
59
+ ].filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]])))
60
+ : payload;
61
+ const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
62
+ const imagePath = `${basename}.png`;
63
+ const identityPath = `${basename}.json`;
64
+ // Older mobile releases support importing this file but have no setup scanner.
65
+ // Keep a private copy even when QR generation succeeds.
66
+ fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
27
67
  const options = { errorCorrectionLevel: "M", margin: 4, scale: 8 };
28
- const png = await QRCode.toBuffer(payload, options);
29
- // Unique names and exclusive creation avoid overwriting another device's
30
- // code or following a pre-existing symlink. Keep this outside the app bundle.
31
- const imagePath = path.join(directory, `lepan-pairing-${randomUUID()}.png`);
32
- fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
33
- process.stdout.write(`\nPair your phone: open LePan → Settings → Set up users → Scan QR code.\n`);
34
- process.stdout.write(`Pairing image saved: ${imagePath}\n`);
35
- process.stdout.write(pairing.type === "lifedrive-node-setup"
36
- ? "Keep this code private. The invitation expires ten minutes after creation.\n"
37
- : "This code contains your device private key. Keep it private and delete the image after pairing.\n");
38
- // Small terminal codes use half-height blocks. If the terminal would wrap,
39
- // direct the user to the full-resolution PNG instead of a broken code.
40
- const modules = QRCode.create(payload, options).modules;
68
+ const privacyNote = pairing.type === "lifedrive-node-setup"
69
+ ? "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.";
71
+ 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
+ : "";
74
+ let modules;
75
+ try {
76
+ const QRCode = require("qrcode");
77
+ const png = await QRCode.toBuffer(qrPayload, options);
78
+ modules = QRCode.create(qrPayload, options).modules;
79
+ // Unique names and exclusive creation avoid overwriting another device's
80
+ // code or following a pre-existing symlink. Keep this outside the app bundle.
81
+ fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
82
+ } catch {
83
+ // The child no longer prints secrets. Preserve an importable fallback
84
+ // before runInstaller removes the temporary handoff directory.
85
+ process.stderr.write("\nNext: pair your phone\n\nThe QR code could not be created. Use the saved pairing file instead.\n");
86
+ process.stderr.write(` ${identityPath}\n\n`);
87
+ process.stderr.write("Transfer this file to your phone, then select it in LePan > Settings > Set up users > Choose identity file.\n");
88
+ process.stderr.write(`${privacyNote}\n`);
89
+ process.stderr.write(renewalHint);
90
+ return;
91
+ }
92
+ // Never hide a terminal code just because the window is short. Open the PNG
93
+ // automatically when the whole code cannot be visible at once, and put the
94
+ // terminal code last so subsequent instructions cannot scroll it away.
41
95
  const width = modules.size + options.margin * 2;
42
- if (process.stdout.isTTY && (!process.stdout.columns || process.stdout.columns > width)) {
96
+ const height = Math.ceil(width / 2);
97
+ const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
98
+ const fitsScreen = fitsWidth && process.stdout.rows >= height + 3;
99
+ 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);
111
+ if (fitsWidth) {
112
+ process.stdout.write("\n");
43
113
  // Explicit colors and a four-module quiet zone work on light/dark themes.
44
114
  const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
45
115
  let terminal = "";
@@ -51,8 +121,6 @@ async function showPairingCode(resultPath) {
51
121
  terminal += "\x1b[0m\n";
52
122
  }
53
123
  process.stdout.write(terminal);
54
- } else {
55
- process.stdout.write("Open the saved PNG on this computer to scan it.\n");
56
124
  }
57
125
  }
58
126
 
@@ -73,7 +141,7 @@ async function runInstaller(command, args) {
73
141
  try { await showPairingCode(resultPath); }
74
142
  catch {
75
143
  // Parser errors may contain identity text; never print them.
76
- process.stderr.write("LePan is installed, but its pairing QR code could not be created. Use the identity file or invitation shown above.\n");
144
+ process.stderr.write("\nPhone pairing could not be prepared. Use your existing identity file, or create a new setup invitation from the node.\n");
77
145
  }
78
146
  }
79
147
  } finally {
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/lifedrive-bundle.tar.gz
3
- size: 44150306 bytes
4
- sha256: ef901f174a43f1a7288516db7834821dbdde61a2eb46d4916c82f6ec880972cd
3
+ size: 44139682 bytes
4
+ sha256: 2173717546eb881f72dce0fdd7a5a6727bfc5286b4c23f948528b9d8f1a2fde8
@@ -1 +1 @@
1
- ef901f174a43f1a7288516db7834821dbdde61a2eb46d4916c82f6ec880972cd lifedrive-bundle.tar.gz
1
+ 2173717546eb881f72dce0fdd7a5a6727bfc5286b4c23f948528b9d8f1a2fde8 lifedrive-bundle.tar.gz
@@ -27,7 +27,7 @@ Windows options:
27
27
  --no-install-leither Require an already running Leither node.
28
28
  --leither-service Configure Leither startup only.
29
29
  --upgrade Upgrade an existing Windows LePan installation.
30
- --household Accepted for parity; Windows uses household mode by default.
30
+ --household Set up household users or renew an unclaimed setup invitation.
31
31
 
32
32
  Run Windows Terminal or PowerShell as Administrator. Windows setup supports
33
33
  native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
@@ -382,8 +382,8 @@ if ($Upgrade) {
382
382
  if (Test-Path -LiteralPath (Join-Path $root "lifeDrive.owner") -PathType Leaf) {
383
383
  throw "This node uses the legacy browser-owner setup, which Windows does not migrate automatically. Its files were left unchanged."
384
384
  }
385
- if (Test-Path -LiteralPath $routes -PathType Leaf) {
386
- throw "An initialized LePan household already exists. Rerun with --upgrade."
385
+ if ((Test-Path -LiteralPath $routes -PathType Leaf) -and -not $Household) {
386
+ throw "A LePan household already exists. Use --household for phone setup, or --upgrade for application updates."
387
387
  }
388
388
  }
389
389
 
@@ -28,7 +28,7 @@ Installer options:
28
28
  --storage-max-gb GB Maximum disk space for a newly installed Leither node (default: 100 GB).
29
29
  --release-base URL Alternate GitHub Release asset base URL.
30
30
  --upgrade Upgrade an existing LePan without changing device authorization.
31
- --household Prepare mobile household setup using the existing node address.
31
+ --household Set up household users or renew an unclaimed setup invitation.
32
32
  --household-config FILE Install mobile-led household setup using a private service configuration.
33
33
 
34
34
  Setup options are forwarded to lifeDrive-setup.sh. Common examples:
@@ -172,14 +172,14 @@ choose_new_node_storage() {
172
172
  valid_storage_gb "$STORAGE_MAX_GB" || { echo "Invalid saved Leither storage choice." >&2; return 1; }
173
173
  fi
174
174
  available_gb=$(available_storage_gb)
175
- echo
176
- echo "Choose the maximum hard-drive space Leither may use for LePan and any other data on this node."
175
+ printf '\nStorage\n'
176
+ echo "Set the disk space limit for this node (LePan and other Leither data)."
177
177
  if [[ "$available_gb" =~ ^[0-9]+$ ]]; then
178
- echo "Available now on the selected drive: ${available_gb} GB."
178
+ echo " Available: ${available_gb} GB"
179
179
  fi
180
180
  if [[ -n "$STORAGE_MAX_GB" ]]; then
181
181
  selected_gb="$STORAGE_MAX_GB"
182
- echo "Leither storage limit: ${selected_gb} GB."
182
+ echo " Selected limit: ${selected_gb} GB"
183
183
  elif [[ -t 0 ]]; then
184
184
  while true; do
185
185
  read -r -p "Maximum Leither storage in GB [$default_gb]: " selected_gb
@@ -252,7 +252,6 @@ try {
252
252
  throw error;
253
253
  }
254
254
  NODE
255
- echo "Leither storage limit saved as ${STORAGE_MAX_GB} GB."
256
255
  }
257
256
 
258
257
  install_leither_binary() (
@@ -475,7 +474,8 @@ configure_macos_leither_firewall() {
475
474
  return 1
476
475
  }
477
476
 
478
- echo "Allowing Leither through the macOS Application Firewall..."
477
+ printf '\nNetwork access\n'
478
+ echo "Allowing connections through the macOS firewall..."
479
479
  firewall_apps=$("$firewall" --listapps) || {
480
480
  echo "LePan setup could not read the macOS Application Firewall rules." >&2
481
481
  return 1
@@ -501,7 +501,7 @@ configure_macos_leither_firewall() {
501
501
  echo "The macOS Application Firewall still reports Leither as blocked." >&2
502
502
  return 1
503
503
  }
504
- echo "macOS Application Firewall allows incoming connections to Leither."
504
+ echo " Firewall access ready."
505
505
 
506
506
  block_all_state=$("$firewall" --getblockall 2>/dev/null || true)
507
507
  if [[ "$block_all_state" == *"enabled"* ]]; then
@@ -566,7 +566,7 @@ finish_new_node_storage() {
566
566
  version=$(leither_version "$LEITHER_WORKDIR") || return 1
567
567
  port=$(node -e 'const fs=require("node:fs"),p=require("node:path"),r=process.argv[1];const f=["SystemVars.json","systemvars.json"].map(n=>p.join(r,n)).find(f=>fs.existsSync(f));const v=JSON.parse(fs.readFileSync(f)).ServicePort??4800;if(!Number.isInteger(v)||v<1||v>65535)process.exit(1);process.stdout.write(String(v));' "$LEITHER_WORKDIR") || return 1
568
568
  previous_pid=$(leither_service_pid) || return 1
569
- echo "Restarting the newly created Leither node to activate its storage limit..."
569
+ echo "Applying the storage limit (restarting the newly created node)..."
570
570
  case "$(uname -s)" in
571
571
  Linux) sudo systemctl restart lifedrive-leither.service || return 1 ;;
572
572
  Darwin) sudo launchctl kickstart -k system/uk.inoku.leither || return 1 ;;
@@ -578,7 +578,7 @@ finish_new_node_storage() {
578
578
  return 1
579
579
  }
580
580
  rm -f -- "$pending" || return 1
581
- echo "Leither restarted with the ${STORAGE_MAX_GB} GB storage limit."
581
+ echo " Storage ready: ${STORAGE_MAX_GB} GB limit."
582
582
  }
583
583
 
584
584
  bootstrap_leither() {
@@ -801,6 +801,7 @@ cleanup() {
801
801
  }
802
802
  trap cleanup EXIT HUP INT TERM
803
803
 
804
+ printf '\nInstall LePan\n'
804
805
  if [[ "$RELEASE_BASE" == file://* ]]; then
805
806
  echo "Loading the LePan release included with the npm package..."
806
807
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoku/lepan",
3
- "version": "0.7.65",
3
+ "version": "0.7.67",
4
4
  "description": "Install LePan and its Leither runtime on Linux, macOS, or Windows",
5
5
  "os": [
6
6
  "linux",