@inoku/lepan 0.7.71 → 0.7.72
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 +9 -3
- package/USER_MANUAL.md +8 -3
- package/bin/lifedrive.js +78 -39
- 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/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
|
|
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.
|
|
101
101
|
|
|
102
|
-
If the terminal is too
|
|
103
|
-
command to redisplay the
|
|
102
|
+
If the terminal is too small, the output gives the required columns and rows,
|
|
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 size 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
|
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
|
|
150
|
+
If the terminal is too small for the QR, resize it to the columns and rows
|
|
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,6 +158,11 @@ 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
|
|
163
|
+
repeat the setup instructions or create another image file. Terminal QR codes use
|
|
164
|
+
a smaller encoding; the saved PNG retains stronger error correction.
|
|
165
|
+
|
|
161
166
|
The Windows and macOS desktop installers print an equivalent command using their
|
|
162
167
|
bundled runtime. Redisplaying a code does not renew it: setup and reconnect
|
|
163
168
|
invitations expire ten minutes after creation. The output shows the expiry in
|
|
@@ -224,8 +229,8 @@ To install a specific release instead of the latest, name it, for example
|
|
|
224
229
|
so a version number always means the same files.
|
|
225
230
|
|
|
226
231
|
Upgrade output shows the existing identity path or saved-pairing directory and a
|
|
227
|
-
help command. When a QR cannot fit, it shows the required terminal
|
|
228
|
-
`--show-qr` command to run after
|
|
232
|
+
help command. When a QR cannot fit, it shows the required terminal size and the
|
|
233
|
+
`--show-qr` command to run after resizing the window. Use `--help` for file import,
|
|
229
234
|
identity expiry, and device-management instructions. Household
|
|
230
235
|
upgrades preserve paired phones and do not issue or renew an invitation; to
|
|
231
236
|
reconnect to an existing household account, run `--recover-device` with its name
|
package/bin/lifedrive.js
CHANGED
|
@@ -23,6 +23,31 @@ function quoteArgument(value) {
|
|
|
23
23
|
: `'${value.replace(/'/g, "'\\''")}'`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function pairingQrPayload(pairing) {
|
|
27
|
+
const legacy = pairing.type === "lifedrive-device-identity";
|
|
28
|
+
// Preserve every field consumed by the existing mobile importers. The full
|
|
29
|
+
// original identity stays in the JSON file, including unused metadata.
|
|
30
|
+
const fields = legacy ? [
|
|
31
|
+
"type", "version", "app_id", "uid", "label", "public_username", "key_type",
|
|
32
|
+
"public_key", "private_key", "signer_info", "node_id",
|
|
33
|
+
] : [
|
|
34
|
+
"type", "version", "app_id", "node_id", "endpoint", "transport",
|
|
35
|
+
"transport_key", "certificate_sha256", "request_id", "secret",
|
|
36
|
+
];
|
|
37
|
+
const qr = Object.fromEntries(fields.filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]]));
|
|
38
|
+
if (legacy && typeof qr.private_key === "string" && typeof qr.public_key === "string") {
|
|
39
|
+
// sodium's 64-byte secret includes a duplicate public key. Both Android and
|
|
40
|
+
// iOS already accept the 32-byte seed and verify the derived public key.
|
|
41
|
+
const secret = Buffer.from(qr.private_key, "base64url");
|
|
42
|
+
const publicKey = Buffer.from(qr.public_key, "base64url");
|
|
43
|
+
if (secret.length === 64 && publicKey.length === 32 && secret.subarray(32).equals(publicKey)) {
|
|
44
|
+
qr.private_key = secret.subarray(0, 32).toString("base64url");
|
|
45
|
+
}
|
|
46
|
+
secret.fill(0);
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(qr);
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
function openPairingImage(imagePath) {
|
|
27
52
|
// A remote server must not open a window in another user's desktop session.
|
|
28
53
|
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
|
|
@@ -48,6 +73,21 @@ function openPairingImage(imagePath) {
|
|
|
48
73
|
return !result.error && result.status === 0;
|
|
49
74
|
}
|
|
50
75
|
|
|
76
|
+
function printTerminalQr(modules, margin) {
|
|
77
|
+
process.stdout.write("\n");
|
|
78
|
+
// Explicit colors and a four-module quiet zone work on light/dark themes.
|
|
79
|
+
const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
|
|
80
|
+
let terminal = "";
|
|
81
|
+
for (let y = -margin; y < modules.size + margin; y += 2) {
|
|
82
|
+
terminal += "\x1b[30;47m";
|
|
83
|
+
for (let x = -margin; x < modules.size + margin; x += 1) {
|
|
84
|
+
terminal += dark(x, y) ? (dark(x, y + 1) ? "█" : "▀") : (dark(x, y + 1) ? "▄" : " ");
|
|
85
|
+
}
|
|
86
|
+
terminal += "\x1b[0m\n";
|
|
87
|
+
}
|
|
88
|
+
process.stdout.write(terminal);
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
async function showPairingCode(resultPath, savedFile = false) {
|
|
52
92
|
if (!fs.existsSync(resultPath)) return;
|
|
53
93
|
const compact = savedFile || (process.argv.includes("--upgrade") && !process.argv.includes("--household"));
|
|
@@ -60,16 +100,7 @@ async function showPairingCode(resultPath, savedFile = false) {
|
|
|
60
100
|
const directory = savedFile ? path.dirname(resultPath) : fs.readFileSync(`${resultPath}.directory`, "utf8");
|
|
61
101
|
if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
|
|
62
102
|
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;
|
|
103
|
+
const qrPayload = pairingQrPayload(pairing);
|
|
73
104
|
const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
|
|
74
105
|
const imagePath = `${basename}.png`;
|
|
75
106
|
const identityPath = savedFile ? resultPath : `${basename}.json`;
|
|
@@ -95,12 +126,21 @@ async function showPairingCode(resultPath, savedFile = false) {
|
|
|
95
126
|
let modules;
|
|
96
127
|
try {
|
|
97
128
|
const QRCode = require("qrcode");
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
// A clean terminal can use lower error correction to reduce its width.
|
|
130
|
+
// Keep medium correction and the full quiet zone in the saved PNG.
|
|
131
|
+
modules = QRCode.create(qrPayload, { ...options, errorCorrectionLevel: "L" }).modules;
|
|
100
132
|
// Unique names and exclusive creation avoid overwriting another device's
|
|
101
133
|
// code or following a pre-existing symlink. Keep this outside the app bundle.
|
|
102
|
-
|
|
134
|
+
if (!savedFile) {
|
|
135
|
+
const png = await QRCode.toBuffer(qrPayload, options);
|
|
136
|
+
fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
|
|
137
|
+
}
|
|
103
138
|
} catch {
|
|
139
|
+
if (savedFile) {
|
|
140
|
+
process.stderr.write("The QR code could not be displayed. Open the saved QR image instead.\n");
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
104
144
|
// The child no longer prints secrets. Preserve an importable fallback
|
|
105
145
|
// before runInstaller removes the temporary handoff directory.
|
|
106
146
|
if (compact) {
|
|
@@ -121,21 +161,33 @@ async function showPairingCode(resultPath, savedFile = false) {
|
|
|
121
161
|
}
|
|
122
162
|
return;
|
|
123
163
|
}
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// terminal code last so subsequent instructions cannot scroll it away.
|
|
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.
|
|
127
166
|
const width = modules.size + options.margin * 2;
|
|
167
|
+
const widthMarker = `|${"-".repeat(width - 1)}|`;
|
|
128
168
|
const height = Math.ceil(width / 2);
|
|
129
169
|
const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
|
|
130
170
|
const fitsScreen = fitsWidth && process.stdout.rows >= height + 3;
|
|
171
|
+
// Redisplaying is deliberately quiet and creates no additional files.
|
|
172
|
+
if (savedFile) {
|
|
173
|
+
if (!process.stdout.isTTY) {
|
|
174
|
+
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`);
|
|
177
|
+
} else {
|
|
178
|
+
process.stdout.write("Keep it private.\n");
|
|
179
|
+
process.stdout.write(expiry);
|
|
180
|
+
printTerminalQr(modules, options.margin);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
131
184
|
const imageOpened = !fitsScreen && openPairingImage(imagePath);
|
|
132
185
|
if (compact) {
|
|
133
|
-
if (savedFile) process.stdout.write(`\nIdentity file: ${identityPath}\nKeep it private.\n`);
|
|
134
186
|
process.stdout.write("\nHow to pair your phone:\nOpen LePan > Settings > Set up users > Scan QR code.\n");
|
|
135
187
|
if (imageOpened) process.stdout.write("\nThe QR image has opened automatically.\n");
|
|
136
|
-
if (!
|
|
188
|
+
if (!fitsScreen) {
|
|
137
189
|
process.stdout.write(process.stdout.isTTY
|
|
138
|
-
? `\
|
|
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`
|
|
139
191
|
: "\nTo display the QR code, run in an interactive terminal:\n");
|
|
140
192
|
process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}\n`);
|
|
141
193
|
}
|
|
@@ -147,13 +199,11 @@ async function showPairingCode(resultPath, savedFile = false) {
|
|
|
147
199
|
process.stdout.write("Open LePan > Settings > Set up users > Scan QR code.\n");
|
|
148
200
|
process.stdout.write(imageOpened
|
|
149
201
|
? "The QR image has opened automatically. Scan it with your phone.\n"
|
|
150
|
-
:
|
|
202
|
+
: fitsScreen ? "Scan the QR code below with your phone.\n"
|
|
151
203
|
: process.stdout.isTTY
|
|
152
|
-
?
|
|
204
|
+
? `The QR code needs ${width + 1} columns and ${height + 3} rows. Resize this terminal or open the saved image.\n`
|
|
153
205
|
: "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 (!
|
|
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
|
-
}
|
|
206
|
+
if (!fitsScreen && process.stdout.isTTY) process.stdout.write(`${widthMarker}\n`);
|
|
157
207
|
process.stdout.write(`${privacyNote}\n`);
|
|
158
208
|
process.stdout.write(expiry);
|
|
159
209
|
process.stdout.write(`\nSaved QR image: ${imagePath}\n`);
|
|
@@ -166,19 +216,8 @@ async function showPairingCode(resultPath, savedFile = false) {
|
|
|
166
216
|
process.stdout.write("Redisplaying the QR does not extend the invitation's ten-minute expiry.\n");
|
|
167
217
|
}
|
|
168
218
|
}
|
|
169
|
-
if (
|
|
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);
|
|
219
|
+
if (fitsScreen) {
|
|
220
|
+
printTerminalQr(modules, options.margin);
|
|
182
221
|
}
|
|
183
222
|
}
|
|
184
223
|
|
|
@@ -262,8 +301,8 @@ async function main() {
|
|
|
262
301
|
Example:
|
|
263
302
|
${pairingCommand} --show-qr '/path/to/lepan-pairing.json'
|
|
264
303
|
|
|
265
|
-
Run pairing commands on the computer running Leither. If the
|
|
266
|
-
|
|
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.
|
|
267
306
|
You can also open the saved PNG, or transfer the JSON privately to your phone
|
|
268
307
|
and use LePan > Settings > Set up users > Choose identity file.
|
|
269
308
|
|
|
@@ -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: 44304002 bytes
|
|
4
|
+
sha256: 76ec5e8ea0f34c6a9b45b71765e0a6dbc5fc00e282396f946fccb5e9b83910d3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
76ec5e8ea0f34c6a9b45b71765e0a6dbc5fc00e282396f946fccb5e9b83910d3 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 ""
|