@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 +12 -6
- package/USER_MANUAL.md +13 -5
- package/bin/lifedrive.js +148 -41
- package/dist/lifedrive-bundle.tar.gz +2 -2
- package/dist/lifedrive-bundle.tar.gz.sha256 +1 -1
- package/dist/lifedrive-install.ps1 +3 -0
- package/dist/lifedrive-install.sh +5 -2
- package/package.json +1 -1
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
|
|
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
|
|
103
|
-
command to redisplay the
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
180
|
-
a short guide to
|
|
181
|
-
|
|
182
|
-
it
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
125
|
-
//
|
|
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
|
|
233
|
+
const widthMarker = `|${"-".repeat(width - 1)}|`;
|
|
129
234
|
const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
? `\
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 44296194 bytes
|
|
4
|
+
sha256: 9dd6f47e4ffcabaae7f4d16b9372db15010252283c003fe576bd4fabb614e5b9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|