@inoku/lepan 0.7.66 → 0.7.69

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,7 @@ 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 it fits the terminal's width and height. In the iPhone or Android app, open **Settings → Set up users → Scan QR code** and scan the terminal or the saved image. 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.
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
90
 
91
91
  ### Renew an expired first-phone invitation
92
92
 
@@ -112,6 +112,21 @@ already exist, use **My devices** on a paired phone to invite another device. An
112
112
  interrupted first claim must be resumed on the original phone with its original
113
113
  invitation. Ordinary `--upgrade` does not renew invitations.
114
114
 
115
+ ### Reconnect after forgetting the only phone
116
+
117
+ Run `npx --yes @inoku/lepan@latest --recover-device Owner` on the computer hosting
118
+ Leither, substituting the existing account name or user ID. Scan the displayed
119
+ QR code to reconnect to that same account. This node-operator action preserves
120
+ account keys, files, backups, and other devices. The reconnect invitation is
121
+ private and expires in ten minutes; rerun the same command if it expires before
122
+ being claimed. The account service briefly restarts, while Leither keeps running.
123
+ Normal phone-to-phone invitations still require device-manager approval.
124
+
125
+ First-account pairing prepares the file catalog before opening Files and adds
126
+ `Getting started with LePan.txt` once. The guide can be renamed or deleted and is
127
+ not duplicated when another phone joins. First setup also grants each account
128
+ write access only to its own node staging directory when Leither requires it.
129
+
115
130
  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.
116
131
 
117
132
  Source code and design documentation are maintained separately. No device key, identity bundle, PPT, publisher key, or node-specific application MID is included in these release assets.
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,13 +151,16 @@ 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**.
159
158
 
160
- The phone creates its own device key, claims the node, and opens your new, empty
161
- personal drive. You are now the node's first user and its **administrator**, and
159
+ The phone creates its own device key, claims the node, and opens your personal
160
+ drive after its file list is ready. Files contains **Getting started with LePan.txt**,
161
+ a short guide to Files, Backups, connecting other phones, and keeping access to
162
+ your account. It is added once per account; reconnecting a phone does not duplicate
163
+ it, and deleting or renaming it does not cause it to return. You are now the node's first user and its **administrator**, and
162
164
  this phone is a **device manager**.
163
165
 
164
166
  You can close the setup screen without pairing to look around the app. Tapping an
@@ -168,7 +170,7 @@ empty page, or **Settings → Set up users**, opens it again.
168
170
 
169
171
  | Situation | What to do |
170
172
  |---|---|
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. |
173
+ | 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
174
  | 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
175
  | 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
176
 
@@ -361,12 +363,26 @@ What remains true:
361
363
  - **Other users are unaffected.** Their drives and devices keep working.
362
364
  - **Recovery needs the node operator.** Contact whoever runs the node.
363
365
 
364
- For node operators: LifeDrive does not yet include a repair command that binds a
365
- new device to an existing user. The setup invitation (`--household`) only works
366
- on a node that has no users, so it cannot be used for this. Until the repair
367
- command exists, recovering a user who has lost every device needs help from the
368
- LifeDrive maintainers. Do not delete or edit `.lifedrive-household/` to try to
369
- work around it: it holds every user's authority and the authorization journal.
366
+ For node operators: run this on the computer hosting Leither, replacing `Owner`
367
+ with the existing account name (quote names containing spaces), or its exact user
368
+ ID if names are duplicated:
369
+
370
+ ```bash
371
+ npx --yes @inoku/lepan@latest --recover-device Owner
372
+ ```
373
+
374
+ The account service briefly stops to issue a private reconnect invitation, then
375
+ starts again before the QR code is shown. Scan it under **Settings → Set up users**
376
+ on the phone. It enrolls a new device manager into the same account; account keys,
377
+ Files, Backups, and other devices are preserved. A reconnect invitation expires
378
+ in ten minutes unless that phone has already begun claiming it. Treat it as a
379
+ private account-access key. An expired, unclaimed code can be replaced by rerunning
380
+ the same command. Once connected, remove access for the forgotten device from
381
+ **My devices**. Leither itself stays running.
382
+
383
+ The node operator's local access authorizes this recovery; ordinary device
384
+ invitations still require approval from a paired device manager. `--household`
385
+ remains first-account setup and cannot replace an existing account.
370
386
 
371
387
  ### 5.4 Prevent lockout
372
388
 
package/bin/lifedrive.js CHANGED
@@ -12,12 +12,37 @@ 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:
18
43
  // it can contain node keys as well as the mobile device's pairing identity.
19
44
  const pairing = JSON.parse(fs.readFileSync(resultPath, "utf8"));
20
- if (pairing.version !== 1 || !["lifedrive-device-identity", "lifedrive-node-setup"].includes(pairing.type)) {
45
+ if (pairing.version !== 1 || !["lifedrive-device-identity", "lifedrive-node-setup", "lifedrive-user-identity"].includes(pairing.type)) {
21
46
  throw new Error("Unsupported pairing data");
22
47
  }
23
48
  const directory = fs.readFileSync(`${resultPath}.directory`, "utf8");
@@ -27,7 +52,7 @@ async function showPairingCode(resultPath) {
27
52
  // The node enforces expiry and returns account details after a successful claim.
28
53
  // Keep the full invitation in the import file, and leave legacy identities intact.
29
54
  // iOS still requires certificate_sha256 even when Leither transport uses a key.
30
- const qrPayload = pairing.type === "lifedrive-node-setup"
55
+ const qrPayload = pairing.type !== "lifedrive-device-identity"
31
56
  ? JSON.stringify(Object.fromEntries([
32
57
  "type", "version", "app_id", "node_id", "endpoint", "transport",
33
58
  "transport_key", "certificate_sha256", "request_id", "secret",
@@ -40,12 +65,12 @@ async function showPairingCode(resultPath) {
40
65
  // Keep a private copy even when QR generation succeeds.
41
66
  fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
42
67
  const options = { errorCorrectionLevel: "M", margin: 4, scale: 8 };
43
- const privacyNote = pairing.type === "lifedrive-node-setup"
68
+ const privacyNote = pairing.type !== "lifedrive-device-identity"
44
69
  ? "Keep it private. This invitation expires ten minutes after creation."
45
70
  : "This contains your device private key. Keep it private and delete it after pairing.";
46
71
  const renewalHint = pairing.type === "lifedrive-node-setup"
47
72
  ? "\nInvitation expired? Run this again on the node:\n npx --yes @inoku/lepan@latest --household\n"
48
- : "";
73
+ : pairing.type === "lifedrive-user-identity" ? "\nInvitation expired? Run the same reconnect command again on this computer.\n" : "";
49
74
  let modules;
50
75
  try {
51
76
  const QRCode = require("qrcode");
@@ -64,17 +89,27 @@ async function showPairingCode(resultPath) {
64
89
  process.stderr.write(renewalHint);
65
90
  return;
66
91
  }
67
- process.stdout.write("\nNext: pair your phone\n\n");
68
- process.stdout.write(" 1. Open LePan on your iPhone or Android phone.\n");
69
- process.stdout.write(" 2. Go to Settings > Set up users > Scan QR code.\n");
70
- // Small terminal codes use half-height blocks. If the terminal would wrap,
71
- // or scroll the instructions out of view, use the full-resolution PNG.
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.
72
95
  const width = modules.size + options.margin * 2;
73
96
  const height = Math.ceil(width / 2);
74
- const fitsTerminal = process.stdout.isTTY && process.stdout.columns > width &&
75
- process.stdout.rows >= height + 26;
76
- if (fitsTerminal) {
77
- process.stdout.write(" 3. Scan the code below.\n\n");
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");
78
113
  // Explicit colors and a four-module quiet zone work on light/dark themes.
79
114
  const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
80
115
  let terminal = "";
@@ -86,16 +121,7 @@ async function showPairingCode(resultPath) {
86
121
  terminal += "\x1b[0m\n";
87
122
  }
88
123
  process.stdout.write(terminal);
89
- } else {
90
- process.stdout.write(" 3. Open the saved QR image on this computer and scan it.\n");
91
124
  }
92
- process.stdout.write(`\nQR image:\n ${imagePath}\n\n${privacyNote}\n`);
93
- process.stdout.write("\nNo Scan QR code button? Transfer this pairing file to your phone:\n");
94
- process.stdout.write(` ${identityPath}\n`);
95
- process.stdout.write("Then open Settings > Set up users > Choose identity file.\n");
96
- process.stdout.write("Keep the pairing file private; delete the transfer copy after import.\n");
97
- process.stdout.write("Keep this computer on and connected while using LePan.\n");
98
- process.stdout.write(renewalHint);
99
125
  }
100
126
 
101
127
  async function runInstaller(command, args) {
@@ -127,6 +153,7 @@ function windowsArguments(values) {
127
153
  const valueOptions = new Map([
128
154
  ["--leither-root", "-LeitherRoot"],
129
155
  ["--storage-max-gb", "-StorageMaxGB"],
156
+ ["--recover-device", "-RecoverDevice"],
130
157
  ]);
131
158
  const switches = new Map([
132
159
  ["--no-install-leither", "-NoInstallLeither"],
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/lifedrive-bundle.tar.gz
3
- size: 44139710 bytes
4
- sha256: 134de0bb85330eb3192e837e5054074969a80cbb4eaa1ee0b7870e55b9a3b310
3
+ size: 44204388 bytes
4
+ sha256: b1d6c652dbda51b836a30778abb722774fd28559373cee7efe2670991d7297b7
@@ -1 +1 @@
1
- 134de0bb85330eb3192e837e5054074969a80cbb4eaa1ee0b7870e55b9a3b310 lifedrive-bundle.tar.gz
1
+ b1d6c652dbda51b836a30778abb722774fd28559373cee7efe2670991d7297b7 lifedrive-bundle.tar.gz
@@ -8,6 +8,7 @@ param(
8
8
  [switch]$LeitherService,
9
9
  [switch]$Upgrade,
10
10
  [switch]$Household,
11
+ [string]$RecoverDevice,
11
12
  [switch]$Help
12
13
  )
13
14
 
@@ -28,6 +29,7 @@ Windows options:
28
29
  --leither-service Configure Leither startup only.
29
30
  --upgrade Upgrade an existing Windows LePan installation.
30
31
  --household Set up household users or renew an unclaimed setup invitation.
32
+ --recover-device USER Reconnect a phone to an existing account name or user ID.
31
33
 
32
34
  Run Windows Terminal or PowerShell as Administrator. Windows setup supports
33
35
  native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
@@ -35,6 +37,10 @@ native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
35
37
  }
36
38
 
37
39
  if ($Help) { Show-Usage; return }
40
+ if ($RecoverDevice) {
41
+ if ($Upgrade -or $LeitherService) { throw "Use --recover-device separately from --upgrade and --leither-service." }
42
+ $Household = $true
43
+ }
38
44
  if ($Upgrade -and $LeitherService) { throw "Use -Upgrade and -LeitherService separately." }
39
45
  if ($StorageMaxGB -lt 0) { throw "Leither storage must be a positive whole number of gigabytes." }
40
46
  if ($StorageMaxGB -eq 0 -and $env:LIFEDRIVE_STORAGE_MAX_GB) {
@@ -469,7 +475,7 @@ if ($Upgrade) {
469
475
  return
470
476
  }
471
477
  try {
472
- & (Join-Path $root "lifedrive-identity\setup-node.ps1") -LeitherRoot $root
478
+ & (Join-Path $root "lifedrive-identity\setup-node.ps1") -LeitherRoot $root -RecoverDevice $RecoverDevice
473
479
  } catch {
474
480
  if ($identityTaskStopped) {
475
481
  try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
@@ -12,6 +12,7 @@ LEITHER_SERVICE_ONLY=0
12
12
  LEITHER_SERVICE_BOOTSTRAPPED=0
13
13
  HOUSEHOLD_CONFIG=""
14
14
  HOUSEHOLD_SETUP=0
15
+ RECOVER_DEVICE=""
15
16
  STORAGE_MAX_GB="${LIFEDRIVE_STORAGE_MAX_GB:-}"
16
17
  setup_command_args=()
17
18
  setup_arg_count=0
@@ -29,6 +30,7 @@ Installer options:
29
30
  --release-base URL Alternate GitHub Release asset base URL.
30
31
  --upgrade Upgrade an existing LePan without changing device authorization.
31
32
  --household Set up household users or renew an unclaimed setup invitation.
33
+ --recover-device USER Reconnect a phone to an existing account, using its name or user ID.
32
34
  --household-config FILE Install mobile-led household setup using a private service configuration.
33
35
 
34
36
  Setup options are forwarded to lifeDrive-setup.sh. Common examples:
@@ -53,6 +55,12 @@ while (( $# )); do
53
55
  --household)
54
56
  HOUSEHOLD_SETUP=1
55
57
  ;;
58
+ --recover-device)
59
+ shift
60
+ [[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--recover-device requires an existing account name or user ID" >&2; exit 2; }
61
+ RECOVER_DEVICE="$1"
62
+ HOUSEHOLD_SETUP=1
63
+ ;;
56
64
  --household-config)
57
65
  shift
58
66
  [[ $# -gt 0 && -f "$1" ]] || { echo "--household-config requires a configuration file" >&2; exit 2; }
@@ -103,6 +111,10 @@ while (( $# )); do
103
111
  shift
104
112
  done
105
113
 
114
+ if [[ -n "$RECOVER_DEVICE" ]] && (( operation_count )); then
115
+ echo "Use --recover-device separately from --upgrade and device-management options." >&2
116
+ exit 2
117
+ fi
106
118
  if (( operation_count > 1 )); then
107
119
  echo "Choose only one of --upgrade, --add-device, --list-devices, or --revoke-device." >&2
108
120
  exit 2
@@ -942,7 +954,7 @@ chmod 700 "$LEITHER_WORKDIR/lifeDrive/"*.sh
942
954
  export LIFEDRIVE_WORKDIR="$LEITHER_WORKDIR"
943
955
  export LIFEDRIVE_LEITHER_PATH="$LEITHER_WORKDIR/Leither"
944
956
  if (( HOUSEHOLD_SETUP )); then
945
- /bin/bash "$LEITHER_WORKDIR/lifedrive-identity/setup-node.sh" "$LEITHER_WORKDIR"
957
+ /bin/bash "$LEITHER_WORKDIR/lifedrive-identity/setup-node.sh" "$LEITHER_WORKDIR" "$RECOVER_DEVICE"
946
958
  exit 0
947
959
  fi
948
960
  if [[ -n "$HOUSEHOLD_CONFIG" ]]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoku/lepan",
3
- "version": "0.7.66",
3
+ "version": "0.7.69",
4
4
  "description": "Install LePan and its Leither runtime on Linux, macOS, or Windows",
5
5
  "os": [
6
6
  "linux",