@inoku/lepan 0.7.71 → 0.7.73

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
@@ -97,16 +97,22 @@ The Linux/macOS legacy browser-owner setup uses the built-in AV1 registration en
97
97
 
98
98
  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.
99
99
 
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.
100
+ After successful npm setup, the installer displays the QR code at the end of its output whenever the terminal is wide enough, regardless of its height. You can scroll vertically. If the terminal is too narrow, 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.
101
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
102
+ If the terminal is too narrow, the output gives the required number of columns,
103
+ a width marker, and a command to redisplay the code after resizing. Run it on the node
104
104
  when connected over SSH. For npm installations, the command has this form:
105
105
 
106
106
  ```bash
107
107
  npx --yes @inoku/lepan@latest --show-qr '/path/to/lepan-pairing.json'
108
108
  ```
109
109
 
110
+ Redisplaying shows only the QR and a privacy reminder, or a short width warning
111
+ and width marker if it cannot fit. It does not repeat the setup instructions or
112
+ create another image file. Terminal QR codes use a smaller payload and lower
113
+ error correction; the saved PNG keeps medium error correction. The full original
114
+ identity remains in the JSON file, and both mobile apps accept the QR encoding.
115
+
110
116
  This displays the saved code without installing, upgrading, or changing device
111
117
  authorization. Desktop packages print a command using their private Node runtime.
112
118
  Redisplaying an invitation does **not** reset its ten-minute expiry; output shows
@@ -154,9 +160,9 @@ private and expires in ten minutes; rerun the same command if it expires before
154
160
  being claimed. The account service briefly restarts, while Leither keeps running.
155
161
  Normal phone-to-phone invitations still require device-manager approval.
156
162
 
157
- First-account pairing prepares the file catalog before opening Files and adds
158
- `Getting started with LePan.txt` once. The guide can be renamed or deleted and is
159
- not duplicated when another phone joins. First setup also grants each account
163
+ First-account pairing prepares the file catalog before opening Files. The phone app
164
+ then adds `LePan Guide.md`, and a copy in the phone's language when LePan is translated
165
+ into it. A guide already in Files is not added again. First setup also grants each account
160
166
  write access only to its own node staging directory when Leither requires it.
161
167
 
162
168
  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.
package/USER_MANUAL.md CHANGED
@@ -158,6 +158,12 @@ For npm installations:
158
158
  npx --yes @inoku/lepan@latest --show-qr '/path/to/lepan-pairing.json'
159
159
  ```
160
160
 
161
+ This command shows only the QR and a privacy reminder. There is no minimum
162
+ height; scroll vertically as needed. If the terminal is too narrow, it prints
163
+ the required width and a width marker so you can widen it and retry. It does not
164
+ repeat the setup instructions or create another image file. Terminal QR codes use
165
+ a smaller encoding; the saved PNG retains stronger error correction.
166
+
161
167
  The Windows and macOS desktop installers print an equivalent command using their
162
168
  bundled runtime. Redisplaying a code does not renew it: setup and reconnect
163
169
  invitations expire ten minutes after creation. The output shows the expiry in
@@ -176,10 +182,12 @@ Within ten minutes of the invitation being printed:
176
182
  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**.
177
183
 
178
184
  The phone creates its own device key, claims the node, and opens your personal
179
- drive after its file list is ready. Files contains **Getting started with LePan.txt**,
180
- a short guide to Files, Backups, connecting other phones, and keeping access to
181
- your account. It is added once per account; reconnecting a phone does not duplicate
182
- it, and deleting or renaming it does not cause it to return. You are now the node's first user and its **administrator**, and
185
+ drive after its file list is ready. When a phone first pairs, the app adds **LePan Guide.md**
186
+ to Files, a short guide to the tabs, photo backup, sharing, adding devices and keeping
187
+ the computer on. If LePan is translated into the phone's language (简体中文, 繁體中文 or
188
+ 日本語) it adds a copy in that language too. A guide already in Files is not added
189
+ again, so another phone does not duplicate it; one you delete comes back the next
190
+ time a phone pairs. Tap a Markdown file in Files to read it formatted. You are now the node's first user and its **administrator**, and
183
191
  this phone is a **device manager**.
184
192
 
185
193
  You can close the setup screen without pairing to look around the app. Tapping an
@@ -225,7 +233,7 @@ so a version number always means the same files.
225
233
 
226
234
  Upgrade output shows the existing identity path or saved-pairing directory and a
227
235
  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,
236
+ `--show-qr` command to run after resizing the window. Use `--help` for file import,
229
237
  identity expiry, and device-management instructions. Household
230
238
  upgrades preserve paired phones and do not issue or renew an invitation; to
231
239
  reconnect to an existing household account, run `--recover-device` with its name
package/bin/lifedrive.js CHANGED
@@ -17,12 +17,103 @@ const pairingCommand = path.basename(path.resolve(__dirname, "..")) === "package
17
17
  ? `${process.platform === "win32" ? "& " : ""}${quoteArgument(process.execPath)} ${quoteArgument(__filename)}`
18
18
  : `npx --yes @inoku/lepan@${/^\d+\.\d+\.\d+$/.test(packageVersion) ? packageVersion : "latest"}`;
19
19
 
20
+ // Opt-in: print the smaller device QR that only newer LePan apps can read. Older
21
+ // apps accept the full identity QR, so that stays the default. The flag belongs
22
+ // to this wrapper; the shell and PowerShell installers do not know it.
23
+ const compactQr = process.argv.includes("--compact-qr");
24
+ if (compactQr) process.argv = process.argv.filter(argument => argument !== "--compact-qr");
25
+ const qrFlag = compactQr ? " --compact-qr" : "";
26
+
20
27
  function quoteArgument(value) {
21
28
  return process.platform === "win32"
22
29
  ? `'${value.replace(/'/g, "''")}'`
23
30
  : `'${value.replace(/'/g, "'\\''")}'`;
24
31
  }
25
32
 
33
+ // A device identity's signer_info is a Go-gob certificate of about 770 characters
34
+ // that repeats the device ID and public key, plus constant type metadata. Only the
35
+ // signature and the certificate name are unique, so the compact QR carries those and
36
+ // the app rebuilds the same bytes. buildSignerInfo must stay identical to
37
+ // CompactIdentity in the Android and iOS apps.
38
+ const CERT_PREFIX = Buffer.from("307f030101085369676e496e666f01ff80000102010c426173655369676e496e666f01ff8200010444617461010c0000002eff810301010c426173655369676e496e666f01ff82000101010e426173655369676e496e666f563101ff8400000065ff830301010e426173655369676e496e666f563101ff8400010601095369676e6174757265010a0001085369676e65724944010c00010a5369676e6572496e666f010a0001074b657954797065010c00010756657273696f6e0106000102504b010a000000", "hex");
39
+ const CERT_KEY_PREFIX = Buffer.from("0b0c0008736f6469756d76322f7f0301010f536f6469756d4b657950616972563201ff80000101010d536f6469756d4b65795061697201ff8200000029ff810301010d536f6469756d4b65795061697201ff820001020102536b010a000102506b010a00000027ff80010220", "hex");
40
+ const CERT_BODY_HEAD = Buffer.from("ff8001010140", "hex");
41
+ const CERT_AFTER_ID = Buffer.concat([Buffer.from("0208", "hex"), Buffer.from("sodiumv2"), Buffer.from("0101000001", "hex")]);
42
+ // Go gob unsigned integer: one byte below 128, else a byte-count marker and the
43
+ // big-endian value.
44
+ function gobUint(n) {
45
+ if (n < 128) return Buffer.from([n]);
46
+ const bytes = [];
47
+ for (let v = n; v > 0; v = Math.floor(v / 256)) bytes.unshift(v % 256);
48
+ return Buffer.from([256 - bytes.length, ...bytes]);
49
+ }
50
+ function buildSignerInfo(signature, uid, publicKey, name) {
51
+ const keyBlob = Buffer.concat([CERT_KEY_PREFIX, publicKey, Buffer.from([0, 0])]);
52
+ const certificate = Buffer.from(`CertPK=${keyBlob.toString("base64url")}==;CertPKID=${uid};name=${name};`, "utf8");
53
+ const body = Buffer.concat([CERT_BODY_HEAD, signature, Buffer.from([1, 27]), Buffer.from(uid, "utf8"),
54
+ CERT_AFTER_ID, gobUint(certificate.length), certificate, Buffer.from([0])]);
55
+ return Buffer.concat([CERT_PREFIX, gobUint(body.length), body]).toString("base64");
56
+ }
57
+ function splitSignerInfo(signerInfo, uid, publicKey) {
58
+ const raw = Buffer.from(signerInfo, "base64");
59
+ const start = CERT_PREFIX.length + 3;
60
+ if (raw.length < start + 6 + 64 + 2 + 27) return null;
61
+ const signature = raw.subarray(start + 6, start + 70);
62
+ const text = raw.toString("utf8");
63
+ const found = /;name=([^;]*);\x00$/.exec(text);
64
+ if (!found) return null;
65
+ const rebuilt = buildSignerInfo(signature, uid, publicKey, found[1]);
66
+ return rebuilt === signerInfo ? { signature, name: found[1] } : null;
67
+ }
68
+
69
+ function compactDeviceQrPayload(pairing) {
70
+ const seed = Buffer.from(pairing.private_key, "base64url");
71
+ const publicKey = Buffer.from(pairing.public_key, "base64url");
72
+ if (seed.length === 64 && publicKey.length === 32 && seed.subarray(32).equals(publicKey)) {
73
+ const split = splitSignerInfo(pairing.signer_info, pairing.uid, publicKey);
74
+ if (split) {
75
+ const compact = {
76
+ t: "ldq", a: pairing.app_id, n: pairing.node_id, u: pairing.uid, s: seed.subarray(0, 32).toString("base64url"),
77
+ g: split.signature.toString("base64url"), l: pairing.label, o: pairing.public_username, c: split.name,
78
+ };
79
+ seed.fill(0);
80
+ return JSON.stringify(compact);
81
+ }
82
+ }
83
+ seed.fill(0);
84
+ return null;
85
+ }
86
+
87
+ function pairingQrPayload(pairing) {
88
+ const legacy = pairing.type === "lifedrive-device-identity";
89
+ // Preserve every field consumed by the existing mobile importers. The full
90
+ // original identity stays in the JSON file, including unused metadata.
91
+ const fields = legacy ? [
92
+ "type", "version", "app_id", "uid", "label", "public_username", "key_type",
93
+ "public_key", "private_key", "signer_info", "node_id",
94
+ ] : [
95
+ "type", "version", "app_id", "node_id", "endpoint", "transport",
96
+ "transport_key", "certificate_sha256", "request_id", "secret",
97
+ ];
98
+ // A certificate this wrapper cannot rebuild exactly keeps the full identity QR.
99
+ if (legacy && compactQr) {
100
+ const compact = compactDeviceQrPayload(pairing);
101
+ if (compact) return compact;
102
+ }
103
+ const qr = Object.fromEntries(fields.filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]]));
104
+ if (legacy && typeof qr.private_key === "string" && typeof qr.public_key === "string") {
105
+ // sodium's 64-byte secret includes a duplicate public key. Both Android and
106
+ // iOS already accept the 32-byte seed and verify the derived public key.
107
+ const secret = Buffer.from(qr.private_key, "base64url");
108
+ const publicKey = Buffer.from(qr.public_key, "base64url");
109
+ if (secret.length === 64 && publicKey.length === 32 && secret.subarray(32).equals(publicKey)) {
110
+ qr.private_key = secret.subarray(0, 32).toString("base64url");
111
+ }
112
+ secret.fill(0);
113
+ }
114
+ return JSON.stringify(qr);
115
+ }
116
+
26
117
  function openPairingImage(imagePath) {
27
118
  // A remote server must not open a window in another user's desktop session.
28
119
  if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
@@ -48,6 +139,21 @@ function openPairingImage(imagePath) {
48
139
  return !result.error && result.status === 0;
49
140
  }
50
141
 
142
+ function printTerminalQr(modules, margin) {
143
+ process.stdout.write("\n");
144
+ // Explicit colors and a four-module quiet zone work on light/dark themes.
145
+ const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
146
+ let terminal = "";
147
+ for (let y = -margin; y < modules.size + margin; y += 2) {
148
+ terminal += "\x1b[30;47m";
149
+ for (let x = -margin; x < modules.size + margin; x += 1) {
150
+ terminal += dark(x, y) ? (dark(x, y + 1) ? "█" : "▀") : (dark(x, y + 1) ? "▄" : " ");
151
+ }
152
+ terminal += "\x1b[0m\n";
153
+ }
154
+ process.stdout.write(terminal);
155
+ }
156
+
51
157
  async function showPairingCode(resultPath, savedFile = false) {
52
158
  if (!fs.existsSync(resultPath)) return;
53
159
  const compact = savedFile || (process.argv.includes("--upgrade") && !process.argv.includes("--household"));
@@ -60,16 +166,7 @@ async function showPairingCode(resultPath, savedFile = false) {
60
166
  const directory = savedFile ? path.dirname(resultPath) : fs.readFileSync(`${resultPath}.directory`, "utf8");
61
167
  if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
62
168
  const payload = JSON.stringify(pairing);
63
- // Only encode fields consumed by the existing Android and iOS setup parsers.
64
- // The node enforces expiry and returns account details after a successful claim.
65
- // Keep the full invitation in the import file, and leave legacy identities intact.
66
- // iOS still requires certificate_sha256 even when Leither transport uses a key.
67
- const qrPayload = pairing.type !== "lifedrive-device-identity"
68
- ? JSON.stringify(Object.fromEntries([
69
- "type", "version", "app_id", "node_id", "endpoint", "transport",
70
- "transport_key", "certificate_sha256", "request_id", "secret",
71
- ].filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]])))
72
- : payload;
169
+ const qrPayload = pairingQrPayload(pairing);
73
170
  const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
74
171
  const imagePath = `${basename}.png`;
75
172
  const identityPath = savedFile ? resultPath : `${basename}.json`;
@@ -82,7 +179,7 @@ async function showPairingCode(resultPath, savedFile = false) {
82
179
  : "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
180
  const expiry = pairing.type !== "lifedrive-device-identity" && Number.isFinite(pairing.expires)
84
181
  ? `\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`;
182
+ const reopenHint = `\nTo show this QR again, run on this computer (on the node for an SSH session):\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}${qrFlag}\nThis only displays the saved pairing code; it does not install or upgrade LePan.\n`;
86
183
  const nodeRoot = process.platform === "win32" && path.basename(directory) === ".lifedrive-household"
87
184
  ? path.dirname(directory) : directory;
88
185
  // A saved file may have been moved off the node. Its new parent is not proof
@@ -95,12 +192,21 @@ async function showPairingCode(resultPath, savedFile = false) {
95
192
  let modules;
96
193
  try {
97
194
  const QRCode = require("qrcode");
98
- const png = await QRCode.toBuffer(qrPayload, options);
99
- modules = QRCode.create(qrPayload, options).modules;
195
+ // A clean terminal can use lower error correction to reduce its width.
196
+ // Keep medium correction and the full quiet zone in the saved PNG.
197
+ modules = QRCode.create(qrPayload, { ...options, errorCorrectionLevel: "L" }).modules;
100
198
  // Unique names and exclusive creation avoid overwriting another device's
101
199
  // code or following a pre-existing symlink. Keep this outside the app bundle.
102
- fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
200
+ if (!savedFile) {
201
+ const png = await QRCode.toBuffer(qrPayload, options);
202
+ fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
203
+ }
103
204
  } catch {
205
+ if (savedFile) {
206
+ process.stderr.write("The QR code could not be displayed. Open the saved QR image instead.\n");
207
+ process.exitCode = 1;
208
+ return;
209
+ }
104
210
  // The child no longer prints secrets. Preserve an importable fallback
105
211
  // before runInstaller removes the temporary handoff directory.
106
212
  if (compact) {
@@ -121,23 +227,33 @@ async function showPairingCode(resultPath, savedFile = false) {
121
227
  }
122
228
  return;
123
229
  }
124
- // Never hide a terminal code just because the window is short. Open the PNG
125
- // automatically when the whole code cannot be visible at once, and put the
126
- // terminal code last so subsequent instructions cannot scroll it away.
230
+ // Prevent horizontal wrapping; terminal height does not restrict printing.
231
+ // Users can scroll vertically when the QR is taller than the window.
127
232
  const width = modules.size + options.margin * 2;
128
- const height = Math.ceil(width / 2);
233
+ const widthMarker = `|${"-".repeat(width - 1)}|`;
129
234
  const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
130
- const fitsScreen = fitsWidth && process.stdout.rows >= height + 3;
131
- const imageOpened = !fitsScreen && openPairingImage(imagePath);
235
+ // Redisplaying is deliberately quiet and creates no additional files.
236
+ if (savedFile) {
237
+ if (!process.stdout.isTTY) {
238
+ process.stdout.write("Run --show-qr in an interactive terminal.\n");
239
+ } else if (!fitsWidth) {
240
+ process.stdout.write(`Terminal too narrow for the QR code: ${width + 1} columns needed.\n${widthMarker}\nWiden the terminal, then run the same command again.\n`);
241
+ } else {
242
+ process.stdout.write("Keep it private.\n");
243
+ process.stdout.write(expiry);
244
+ printTerminalQr(modules, options.margin);
245
+ }
246
+ return;
247
+ }
248
+ const imageOpened = !fitsWidth && openPairingImage(imagePath);
132
249
  if (compact) {
133
- if (savedFile) process.stdout.write(`\nIdentity file: ${identityPath}\nKeep it private.\n`);
134
250
  process.stdout.write("\nHow to pair your phone:\nOpen LePan > Settings > Set up users > Scan QR code.\n");
135
251
  if (imageOpened) process.stdout.write("\nThe QR image has opened automatically.\n");
136
252
  if (!fitsWidth) {
137
253
  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`
254
+ ? `\nThe QR code needs ${width + 1} columns. Widen this terminal or open the saved image.\n${widthMarker}\nTo display it here, widen and run:\n`
139
255
  : "\nTo display the QR code, run in an interactive terminal:\n");
140
- process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}\n`);
256
+ process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}${qrFlag}\n`);
141
257
  }
142
258
  process.stdout.write(`\nSaved QR image: ${imagePath}\nPairing file (Choose identity file): ${identityPath}\n`);
143
259
  process.stdout.write(expiry);
@@ -149,11 +265,9 @@ async function showPairingCode(resultPath, savedFile = false) {
149
265
  ? "The QR image has opened automatically. Scan it with your phone.\n"
150
266
  : fitsWidth ? "Scan the QR code below with your phone.\n"
151
267
  : process.stdout.isTTY
152
- ? "This terminal is too narrow for the QR code. You can open the saved QR image.\n"
268
+ ? `The QR code needs ${width + 1} columns. Widen this terminal or open the saved image.\n`
153
269
  : "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
- }
270
+ if (!fitsWidth && process.stdout.isTTY) process.stdout.write(`${widthMarker}\n`);
157
271
  process.stdout.write(`${privacyNote}\n`);
158
272
  process.stdout.write(expiry);
159
273
  process.stdout.write(`\nSaved QR image: ${imagePath}\n`);
@@ -167,18 +281,7 @@ async function showPairingCode(resultPath, savedFile = false) {
167
281
  }
168
282
  }
169
283
  if (fitsWidth) {
170
- process.stdout.write("\n");
171
- // Explicit colors and a four-module quiet zone work on light/dark themes.
172
- const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
173
- let terminal = "";
174
- for (let y = -options.margin; y < modules.size + options.margin; y += 2) {
175
- terminal += "\x1b[30;47m";
176
- for (let x = -options.margin; x < modules.size + options.margin; x += 1) {
177
- terminal += dark(x, y) ? (dark(x, y + 1) ? "█" : "▀") : (dark(x, y + 1) ? "▄" : " ");
178
- }
179
- terminal += "\x1b[0m\n";
180
- }
181
- process.stdout.write(terminal);
284
+ printTerminalQr(modules, options.margin);
182
285
  }
183
286
  }
184
287
 
@@ -258,12 +361,16 @@ async function main() {
258
361
  if (argumentsList.includes("--help") || argumentsList.includes("-h")) {
259
362
  process.stdout.write(`Pairing help:
260
363
  --show-qr FILE Display a saved identity or invitation as a QR code.
364
+ --compact-qr Use the smaller device QR (a much smaller code to scan).
365
+ Needs a LePan app version that supports it; add it to an
366
+ install, upgrade or --show-qr command.
261
367
 
262
368
  Example:
263
369
  ${pairingCommand} --show-qr '/path/to/lepan-pairing.json'
264
370
 
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.
371
+ Run pairing commands on the computer running Leither. If the terminal is too narrow,
372
+ widen it to the printed number of columns and run --show-qr again.
373
+ There is no minimum height; scroll vertically as needed.
267
374
  You can also open the saved PNG, or transfer the JSON privately to your phone
268
375
  and use LePan > Settings > Set up users > Choose identity file.
269
376
 
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/lifedrive-bundle.tar.gz
3
- size: 44302951 bytes
4
- sha256: 093f14e7fe7816a667e81c2ae300a2b0c1b0c6577e81b7eb83eac6a1d38f6b16
3
+ size: 44296194 bytes
4
+ sha256: 9dd6f47e4ffcabaae7f4d16b9372db15010252283c003fe576bd4fabb614e5b9
@@ -1 +1 @@
1
- 093f14e7fe7816a667e81c2ae300a2b0c1b0c6577e81b7eb83eac6a1d38f6b16 lifedrive-bundle.tar.gz
1
+ 9dd6f47e4ffcabaae7f4d16b9372db15010252283c003fe576bd4fabb614e5b9 lifedrive-bundle.tar.gz
@@ -471,7 +471,10 @@ if ($Upgrade) {
471
471
  }
472
472
  throw
473
473
  }
474
+ Write-Host ""
475
+ Write-Host "------------------------------------------------------------"
474
476
  Write-Host "LePan Windows upgrade complete. Leither was not restarted."
477
+ Write-Host "Node: $([Environment]::MachineName)"
475
478
  $pairingLauncher = if ($env:LIFEDRIVE_PAIRING_COMMAND) { $env:LIFEDRIVE_PAIRING_COMMAND } else { "npx --yes @inoku/lepan@latest" }
476
479
  Write-Host "Saved pairing files, if retained: $root\.lifedrive-household\lepan-pairing-*.json (with matching .png images)."
477
480
  Write-Host ""
@@ -535,7 +535,7 @@ wait_for_leither() {
535
535
  local version="$1" port="$2" previous_pid="${3:-}" attempt response current_pid
536
536
  for attempt in {1..30}; do
537
537
  current_pid=$(leither_service_pid || true)
538
- response=$(curl --noproxy '*' -fsS --max-time 2 "http://127.0.0.1:$port/getvar?name=ver" 2>/dev/null || true)
538
+ response=$(curl -q --noproxy '*' -fsS --max-time 2 "http://127.0.0.1:$port/getvar?name=ver" 2>/dev/null || true)
539
539
  if [[ -z "$previous_pid" || "$current_pid" != "$previous_pid" ]] &&
540
540
  leither_service_owns_process &&
541
541
  node -e 'try { process.exit(JSON.parse(process.argv[1]).replace(/^V/, "") === process.argv[2] ? 0 : 1); } catch { process.exit(1); }' "$response" "$version"; then
@@ -799,7 +799,10 @@ if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && "$(sed -n '1p' "$LEITHER_WORKDIR/
799
799
  echo "This key-auth release intentionally requires a fresh LePan owner state." >&2
800
800
  exit 1
801
801
  fi
802
- if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && -z "$HOUSEHOLD_CONFIG" ]] && (( ! UPGRADE_ONLY && ! MANAGEMENT_ONLY )); then
802
+ # --recover-device reconnects an account of a household that already exists; it creates
803
+ # no owner, so key-auth state left behind from before the node became a household must
804
+ # not stop it (minipc kept its lifeDrive.owner and answered "an initialized LePan exists").
805
+ if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && -z "$HOUSEHOLD_CONFIG" && -z "$RECOVER_DEVICE" ]] && (( ! UPGRADE_ONLY && ! MANAGEMENT_ONLY )); then
803
806
  echo "LePan installation stopped: an initialized LePan already exists at $LEITHER_WORKDIR." >&2
804
807
  echo "Use --upgrade for application files or --add-device for another browser." >&2
805
808
  exit 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoku/lepan",
3
- "version": "0.7.71",
3
+ "version": "0.7.73",
4
4
  "description": "Install LePan and its Leither runtime on Linux, macOS, or Windows",
5
5
  "os": [
6
6
  "linux",