@inoku/lepan 0.7.72 → 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,9 +97,9 @@ 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 complete code fits in the terminal. 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 small, the output gives the required columns and rows,
102
+ If the terminal is too narrow, the output gives the required number of columns,
103
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
 
@@ -107,7 +107,7 @@ when connected over SSH. For npm installations, the command has this form:
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 size warning
110
+ Redisplaying shows only the QR and a privacy reminder, or a short width warning
111
111
  and width marker if it cannot fit. It does not repeat the setup instructions or
112
112
  create another image file. Terminal QR codes use a smaller payload and lower
113
113
  error correction; the saved PNG keeps medium error correction. The full original
@@ -160,9 +160,9 @@ private and expires in ten minutes; rerun the same command if it expires before
160
160
  being claimed. The account service briefly restarts, while Leither keeps running.
161
161
  Normal phone-to-phone invitations still require device-manager approval.
162
162
 
163
- First-account pairing prepares the file catalog before opening Files and adds
164
- `Getting started with LePan.txt` once. The guide can be renamed or deleted and is
165
- 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
166
166
  write access only to its own node staging directory when Leither requires it.
167
167
 
168
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
@@ -147,7 +147,7 @@ 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 small for the QR, resize it to the columns and rows
150
+ If the terminal is too narrow for the QR, widen it to the number of columns
151
151
  printed by setup, then run the printed `--show-qr` command. Over SSH, run the
152
152
  command on the node in that same terminal. It displays the saved code without
153
153
  installing or upgrading again. You can also open the saved PNG image or transfer
@@ -158,8 +158,9 @@ 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. If it cannot fit, it prints
162
- the required size and a width marker so you can resize and retry. It does not
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
163
164
  repeat the setup instructions or create another image file. Terminal QR codes use
164
165
  a smaller encoding; the saved PNG retains stronger error correction.
165
166
 
@@ -181,10 +182,12 @@ Within ten minutes of the invitation being printed:
181
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**.
182
183
 
183
184
  The phone creates its own device key, claims the node, and opens your personal
184
- drive after its file list is ready. Files contains **Getting started with LePan.txt**,
185
- a short guide to Files, Backups, connecting other phones, and keeping access to
186
- your account. It is added once per account; reconnecting a phone does not duplicate
187
- 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
188
191
  this phone is a **device manager**.
189
192
 
190
193
  You can close the setup screen without pairing to look around the app. Tapping an
@@ -229,7 +232,7 @@ To install a specific release instead of the latest, name it, for example
229
232
  so a version number always means the same files.
230
233
 
231
234
  Upgrade output shows the existing identity path or saved-pairing directory and a
232
- help command. When a QR cannot fit, it shows the required terminal size and the
235
+ help command. When a QR cannot fit, it shows the required terminal width and the
233
236
  `--show-qr` command to run after resizing the window. Use `--help` for file import,
234
237
  identity expiry, and device-management instructions. Household
235
238
  upgrades preserve paired phones and do not issue or renew an invitation; to
package/bin/lifedrive.js CHANGED
@@ -17,12 +17,73 @@ 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
+
26
87
  function pairingQrPayload(pairing) {
27
88
  const legacy = pairing.type === "lifedrive-device-identity";
28
89
  // Preserve every field consumed by the existing mobile importers. The full
@@ -34,6 +95,11 @@ function pairingQrPayload(pairing) {
34
95
  "type", "version", "app_id", "node_id", "endpoint", "transport",
35
96
  "transport_key", "certificate_sha256", "request_id", "secret",
36
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
+ }
37
103
  const qr = Object.fromEntries(fields.filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]]));
38
104
  if (legacy && typeof qr.private_key === "string" && typeof qr.public_key === "string") {
39
105
  // sodium's 64-byte secret includes a duplicate public key. Both Android and
@@ -113,7 +179,7 @@ async function showPairingCode(resultPath, savedFile = false) {
113
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.";
114
180
  const expiry = pairing.type !== "lifedrive-device-identity" && Number.isFinite(pairing.expires)
115
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" : ""}` : "";
116
- 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`;
117
183
  const nodeRoot = process.platform === "win32" && path.basename(directory) === ".lifedrive-household"
118
184
  ? path.dirname(directory) : directory;
119
185
  // A saved file may have been moved off the node. Its new parent is not proof
@@ -161,19 +227,17 @@ async function showPairingCode(resultPath, savedFile = false) {
161
227
  }
162
228
  return;
163
229
  }
164
- // Only print a terminal QR when the complete code fits. Otherwise, explain
165
- // the required size and offer the saved image instead of a wrapped/clipped QR.
230
+ // Prevent horizontal wrapping; terminal height does not restrict printing.
231
+ // Users can scroll vertically when the QR is taller than the window.
166
232
  const width = modules.size + options.margin * 2;
167
233
  const widthMarker = `|${"-".repeat(width - 1)}|`;
168
- const height = Math.ceil(width / 2);
169
234
  const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
170
- const fitsScreen = fitsWidth && process.stdout.rows >= height + 3;
171
235
  // Redisplaying is deliberately quiet and creates no additional files.
172
236
  if (savedFile) {
173
237
  if (!process.stdout.isTTY) {
174
238
  process.stdout.write("Run --show-qr in an interactive terminal.\n");
175
- } else if (!fitsScreen) {
176
- process.stdout.write(`Terminal too small for the QR code: ${width + 1} columns and ${height + 3} rows needed.\n${widthMarker}\nResize the terminal, then run the same command again.\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`);
177
241
  } else {
178
242
  process.stdout.write("Keep it private.\n");
179
243
  process.stdout.write(expiry);
@@ -181,15 +245,15 @@ async function showPairingCode(resultPath, savedFile = false) {
181
245
  }
182
246
  return;
183
247
  }
184
- const imageOpened = !fitsScreen && openPairingImage(imagePath);
248
+ const imageOpened = !fitsWidth && openPairingImage(imagePath);
185
249
  if (compact) {
186
250
  process.stdout.write("\nHow to pair your phone:\nOpen LePan > Settings > Set up users > Scan QR code.\n");
187
251
  if (imageOpened) process.stdout.write("\nThe QR image has opened automatically.\n");
188
- if (!fitsScreen) {
252
+ if (!fitsWidth) {
189
253
  process.stdout.write(process.stdout.isTTY
190
- ? `\nThe QR code needs ${width + 1} columns and ${height + 3} rows. Resize this terminal or open the saved image.\n${widthMarker}\nTo display it here, resize and 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`
191
255
  : "\nTo display the QR code, run in an interactive terminal:\n");
192
- process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}\n`);
256
+ process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}${qrFlag}\n`);
193
257
  }
194
258
  process.stdout.write(`\nSaved QR image: ${imagePath}\nPairing file (Choose identity file): ${identityPath}\n`);
195
259
  process.stdout.write(expiry);
@@ -199,11 +263,11 @@ async function showPairingCode(resultPath, savedFile = false) {
199
263
  process.stdout.write("Open LePan > Settings > Set up users > Scan QR code.\n");
200
264
  process.stdout.write(imageOpened
201
265
  ? "The QR image has opened automatically. Scan it with your phone.\n"
202
- : fitsScreen ? "Scan the QR code below with your phone.\n"
266
+ : fitsWidth ? "Scan the QR code below with your phone.\n"
203
267
  : process.stdout.isTTY
204
- ? `The QR code needs ${width + 1} columns and ${height + 3} rows. Resize this terminal or open the saved image.\n`
268
+ ? `The QR code needs ${width + 1} columns. Widen this terminal or open the saved image.\n`
205
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");
206
- if (!fitsScreen && process.stdout.isTTY) process.stdout.write(`${widthMarker}\n`);
270
+ if (!fitsWidth && process.stdout.isTTY) process.stdout.write(`${widthMarker}\n`);
207
271
  process.stdout.write(`${privacyNote}\n`);
208
272
  process.stdout.write(expiry);
209
273
  process.stdout.write(`\nSaved QR image: ${imagePath}\n`);
@@ -216,7 +280,7 @@ async function showPairingCode(resultPath, savedFile = false) {
216
280
  process.stdout.write("Redisplaying the QR does not extend the invitation's ten-minute expiry.\n");
217
281
  }
218
282
  }
219
- if (fitsScreen) {
283
+ if (fitsWidth) {
220
284
  printTerminalQr(modules, options.margin);
221
285
  }
222
286
  }
@@ -297,12 +361,16 @@ async function main() {
297
361
  if (argumentsList.includes("--help") || argumentsList.includes("-h")) {
298
362
  process.stdout.write(`Pairing help:
299
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.
300
367
 
301
368
  Example:
302
369
  ${pairingCommand} --show-qr '/path/to/lepan-pairing.json'
303
370
 
304
- Run pairing commands on the computer running Leither. If the QR does not fit,
305
- resize to the printed columns and rows 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.
306
374
  You can also open the saved PNG, or transfer the JSON privately to your phone
307
375
  and use LePan > Settings > Set up users > Choose identity file.
308
376
 
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/lifedrive-bundle.tar.gz
3
- size: 44304002 bytes
4
- sha256: 76ec5e8ea0f34c6a9b45b71765e0a6dbc5fc00e282396f946fccb5e9b83910d3
3
+ size: 44296194 bytes
4
+ sha256: 9dd6f47e4ffcabaae7f4d16b9372db15010252283c003fe576bd4fabb614e5b9
@@ -1 +1 @@
1
- 76ec5e8ea0f34c6a9b45b71765e0a6dbc5fc00e282396f946fccb5e9b83910d3 lifedrive-bundle.tar.gz
1
+ 9dd6f47e4ffcabaae7f4d16b9372db15010252283c003fe576bd4fabb614e5b9 lifedrive-bundle.tar.gz
@@ -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.72",
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",