@inoku/lepan 0.7.70 → 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 +28 -1
- package/USER_MANUAL.md +32 -0
- package/bin/lifedrive.js +182 -47
- package/dist/lifedrive-bundle.tar.gz +2 -2
- package/dist/lifedrive-bundle.tar.gz.sha256 +1 -1
- package/dist/lifedrive-install.ps1 +8 -0
- package/dist/lifedrive-install.sh +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,34 @@ 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
|
+
|
|
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
|
+
when connected over SSH. For npm installations, the command has this form:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx --yes @inoku/lepan@latest --show-qr '/path/to/lepan-pairing.json'
|
|
108
|
+
```
|
|
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
|
+
|
|
116
|
+
This displays the saved code without installing, upgrading, or changing device
|
|
117
|
+
authorization. Desktop packages print a command using their private Node runtime.
|
|
118
|
+
Redisplaying an invitation does **not** reset its ten-minute expiry; output shows
|
|
119
|
+
its expiry in UTC and how to renew it. A legacy `lifedrive-device-identity` file
|
|
120
|
+
is a persistent device key and has no ten-minute expiry; it remains usable while
|
|
121
|
+
authorized. Keep its original safe and offline, and delete transfer copies after
|
|
122
|
+
pairing. Upgrades keep the output brief: the existing identity path or saved-pairing
|
|
123
|
+
directory, the QR and file paths when available, and a help command. If the QR
|
|
124
|
+
cannot fit, the output gives the required width and the command to redisplay it.
|
|
125
|
+
Use `--help` for expiry, file import, and account reconnection instructions.
|
|
126
|
+
Initial setup's local browser links are labeled with the node's detected LAN
|
|
127
|
+
interfaces; these are node addresses, not router or gateway addresses.
|
|
101
128
|
|
|
102
129
|
### Renew an expired first-phone invitation
|
|
103
130
|
|
package/USER_MANUAL.md
CHANGED
|
@@ -147,6 +147,30 @@ 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
|
|
151
|
+
printed by setup, then run the printed `--show-qr` command. Over SSH, run the
|
|
152
|
+
command on the node in that same terminal. It displays the saved code without
|
|
153
|
+
installing or upgrading again. You can also open the saved PNG image or transfer
|
|
154
|
+
the saved JSON privately to your phone and use **Choose identity file**.
|
|
155
|
+
For npm installations:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx --yes @inoku/lepan@latest --show-qr '/path/to/lepan-pairing.json'
|
|
159
|
+
```
|
|
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
|
+
|
|
166
|
+
The Windows and macOS desktop installers print an equivalent command using their
|
|
167
|
+
bundled runtime. Redisplaying a code does not renew it: setup and reconnect
|
|
168
|
+
invitations expire ten minutes after creation. The output shows the expiry in
|
|
169
|
+
UTC. Older browser-owner installations use a `lifedrive-device-identity` file
|
|
170
|
+
containing a persistent device private key; that identity has no ten-minute
|
|
171
|
+
expiry and remains usable while authorized. Keep its original safe and offline;
|
|
172
|
+
delete transfer copies after pairing.
|
|
173
|
+
|
|
150
174
|
### 2.3 Create the first user from your phone
|
|
151
175
|
|
|
152
176
|
Within ten minutes of the invitation being printed:
|
|
@@ -204,6 +228,14 @@ To install a specific release instead of the latest, name it, for example
|
|
|
204
228
|
`npx --yes @inoku/lepan@0.7.35 --upgrade`. Release versions are never reused,
|
|
205
229
|
so a version number always means the same files.
|
|
206
230
|
|
|
231
|
+
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
|
|
233
|
+
`--show-qr` command to run after resizing the window. Use `--help` for file import,
|
|
234
|
+
identity expiry, and device-management instructions. Household
|
|
235
|
+
upgrades preserve paired phones and do not issue or renew an invitation; to
|
|
236
|
+
reconnect to an existing household account, run `--recover-device` with its name
|
|
237
|
+
or user ID on the node. For another device, use **My devices** on a paired phone.
|
|
238
|
+
|
|
207
239
|
The installer saves the previous application files to a backup directory, prints
|
|
208
240
|
where, and then:
|
|
209
241
|
|
package/bin/lifedrive.js
CHANGED
|
@@ -11,6 +11,42 @@ const { pathToFileURL } = require("node:url");
|
|
|
11
11
|
const distributionDirectory = path.resolve(__dirname, "..", "dist");
|
|
12
12
|
const bundlePath = path.join(distributionDirectory, "lifedrive-bundle.tar.gz");
|
|
13
13
|
const checksumPath = `${bundlePath}.sha256`;
|
|
14
|
+
const packageVersion = require("../package.json").version;
|
|
15
|
+
// Desktop installers carry Node privately; their users need not have npx.
|
|
16
|
+
const pairingCommand = path.basename(path.resolve(__dirname, "..")) === "package"
|
|
17
|
+
? `${process.platform === "win32" ? "& " : ""}${quoteArgument(process.execPath)} ${quoteArgument(__filename)}`
|
|
18
|
+
: `npx --yes @inoku/lepan@${/^\d+\.\d+\.\d+$/.test(packageVersion) ? packageVersion : "latest"}`;
|
|
19
|
+
|
|
20
|
+
function quoteArgument(value) {
|
|
21
|
+
return process.platform === "win32"
|
|
22
|
+
? `'${value.replace(/'/g, "''")}'`
|
|
23
|
+
: `'${value.replace(/'/g, "'\\''")}'`;
|
|
24
|
+
}
|
|
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
|
+
}
|
|
14
50
|
|
|
15
51
|
function openPairingImage(imagePath) {
|
|
16
52
|
// A remote server must not open a window in another user's desktop session.
|
|
@@ -37,90 +73,151 @@ function openPairingImage(imagePath) {
|
|
|
37
73
|
return !result.error && result.status === 0;
|
|
38
74
|
}
|
|
39
75
|
|
|
40
|
-
|
|
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
|
+
|
|
91
|
+
async function showPairingCode(resultPath, savedFile = false) {
|
|
41
92
|
if (!fs.existsSync(resultPath)) return;
|
|
93
|
+
const compact = savedFile || (process.argv.includes("--upgrade") && !process.argv.includes("--household"));
|
|
42
94
|
// The child writes this only after setup succeeds. Never scrape its output:
|
|
43
95
|
// it can contain node keys as well as the mobile device's pairing identity.
|
|
44
96
|
const pairing = JSON.parse(fs.readFileSync(resultPath, "utf8"));
|
|
45
97
|
if (pairing.version !== 1 || !["lifedrive-device-identity", "lifedrive-node-setup", "lifedrive-user-identity"].includes(pairing.type)) {
|
|
46
98
|
throw new Error("Unsupported pairing data");
|
|
47
99
|
}
|
|
48
|
-
const directory = fs.readFileSync(`${resultPath}.directory`, "utf8");
|
|
100
|
+
const directory = savedFile ? path.dirname(resultPath) : fs.readFileSync(`${resultPath}.directory`, "utf8");
|
|
49
101
|
if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
|
|
50
102
|
const payload = JSON.stringify(pairing);
|
|
51
|
-
|
|
52
|
-
// The node enforces expiry and returns account details after a successful claim.
|
|
53
|
-
// Keep the full invitation in the import file, and leave legacy identities intact.
|
|
54
|
-
// iOS still requires certificate_sha256 even when Leither transport uses a key.
|
|
55
|
-
const qrPayload = pairing.type !== "lifedrive-device-identity"
|
|
56
|
-
? JSON.stringify(Object.fromEntries([
|
|
57
|
-
"type", "version", "app_id", "node_id", "endpoint", "transport",
|
|
58
|
-
"transport_key", "certificate_sha256", "request_id", "secret",
|
|
59
|
-
].filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]])))
|
|
60
|
-
: payload;
|
|
103
|
+
const qrPayload = pairingQrPayload(pairing);
|
|
61
104
|
const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
|
|
62
105
|
const imagePath = `${basename}.png`;
|
|
63
|
-
const identityPath = `${basename}.json`;
|
|
106
|
+
const identityPath = savedFile ? resultPath : `${basename}.json`;
|
|
64
107
|
// Older mobile releases support importing this file but have no setup scanner.
|
|
65
108
|
// Keep a private copy even when QR generation succeeds.
|
|
66
|
-
fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
|
|
109
|
+
if (!savedFile) fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
|
|
67
110
|
const options = { errorCorrectionLevel: "M", margin: 4, scale: 8 };
|
|
68
111
|
const privacyNote = pairing.type !== "lifedrive-device-identity"
|
|
69
112
|
? "Keep it private. This invitation expires ten minutes after creation."
|
|
70
|
-
: "This contains your device private key. Keep
|
|
113
|
+
: "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
|
+
const expiry = pairing.type !== "lifedrive-device-identity" && Number.isFinite(pairing.expires)
|
|
115
|
+
? `\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`;
|
|
117
|
+
const nodeRoot = process.platform === "win32" && path.basename(directory) === ".lifedrive-household"
|
|
118
|
+
? path.dirname(directory) : directory;
|
|
119
|
+
// A saved file may have been moved off the node. Its new parent is not proof
|
|
120
|
+
// of the original Leither root; let node discovery choose in that case.
|
|
121
|
+
const renewalRoot = savedFile ? "" : ` --leither-root ${quoteArgument(nodeRoot)}`;
|
|
71
122
|
const renewalHint = pairing.type === "lifedrive-node-setup"
|
|
72
|
-
?
|
|
73
|
-
: pairing.type === "lifedrive-user-identity"
|
|
123
|
+
? `\nInvitation expired? Run this again on the node before the first owner is created:\n ${pairingCommand}${renewalRoot} --household\n`
|
|
124
|
+
: pairing.type === "lifedrive-user-identity"
|
|
125
|
+
? `\nInvitation expired? Run the same reconnect command again on the node${pairing.user_id ? `:\n ${pairingCommand}${renewalRoot} --recover-device ${quoteArgument(pairing.user_id)}` : "."}\n` : "";
|
|
74
126
|
let modules;
|
|
75
127
|
try {
|
|
76
128
|
const QRCode = require("qrcode");
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
79
132
|
// Unique names and exclusive creation avoid overwriting another device's
|
|
80
133
|
// code or following a pre-existing symlink. Keep this outside the app bundle.
|
|
81
|
-
|
|
134
|
+
if (!savedFile) {
|
|
135
|
+
const png = await QRCode.toBuffer(qrPayload, options);
|
|
136
|
+
fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
|
|
137
|
+
}
|
|
82
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
|
+
}
|
|
83
144
|
// The child no longer prints secrets. Preserve an importable fallback
|
|
84
145
|
// before runInstaller removes the temporary handoff directory.
|
|
146
|
+
if (compact) {
|
|
147
|
+
process.stderr.write(`\nThe QR code could not be created. In LePan, open Settings > Set up users > Choose identity file.\nPairing file: ${identityPath}\nKeep it private.\n`);
|
|
148
|
+
process.stderr.write(expiry);
|
|
149
|
+
process.stderr.write(`\nHelp:\n ${pairingCommand} --help\n`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
85
152
|
process.stderr.write("\nNext: pair your phone\n\nThe QR code could not be created. Use the saved pairing file instead.\n");
|
|
86
153
|
process.stderr.write(` ${identityPath}\n\n`);
|
|
87
154
|
process.stderr.write("Transfer this file to your phone, then select it in LePan > Settings > Set up users > Choose identity file.\n");
|
|
88
155
|
process.stderr.write(`${privacyNote}\n`);
|
|
156
|
+
process.stderr.write(expiry);
|
|
157
|
+
process.stderr.write(reopenHint);
|
|
89
158
|
process.stderr.write(renewalHint);
|
|
159
|
+
if (pairing.type !== "lifedrive-device-identity") {
|
|
160
|
+
process.stderr.write("Redisplaying the QR does not extend the invitation's ten-minute expiry.\n");
|
|
161
|
+
}
|
|
90
162
|
return;
|
|
91
163
|
}
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// 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.
|
|
95
166
|
const width = modules.size + options.margin * 2;
|
|
167
|
+
const widthMarker = `|${"-".repeat(width - 1)}|`;
|
|
96
168
|
const height = Math.ceil(width / 2);
|
|
97
169
|
const fitsWidth = process.stdout.isTTY && process.stdout.columns > width;
|
|
98
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
|
+
}
|
|
99
184
|
const imageOpened = !fitsScreen && openPairingImage(imagePath);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
process.stdout.write(`Pairing file (Choose identity file): ${identityPath}\n`);
|
|
109
|
-
process.stdout.write("Keep this computer on and connected while using LePan.\n");
|
|
110
|
-
process.stdout.write(renewalHint);
|
|
111
|
-
if (fitsWidth) {
|
|
112
|
-
process.stdout.write("\n");
|
|
113
|
-
// Explicit colors and a four-module quiet zone work on light/dark themes.
|
|
114
|
-
const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
|
|
115
|
-
let terminal = "";
|
|
116
|
-
for (let y = -options.margin; y < modules.size + options.margin; y += 2) {
|
|
117
|
-
terminal += "\x1b[30;47m";
|
|
118
|
-
for (let x = -options.margin; x < modules.size + options.margin; x += 1) {
|
|
119
|
-
terminal += dark(x, y) ? (dark(x, y + 1) ? "█" : "▀") : (dark(x, y + 1) ? "▄" : " ");
|
|
120
|
-
}
|
|
121
|
-
terminal += "\x1b[0m\n";
|
|
185
|
+
if (compact) {
|
|
186
|
+
process.stdout.write("\nHow to pair your phone:\nOpen LePan > Settings > Set up users > Scan QR code.\n");
|
|
187
|
+
if (imageOpened) process.stdout.write("\nThe QR image has opened automatically.\n");
|
|
188
|
+
if (!fitsScreen) {
|
|
189
|
+
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`
|
|
191
|
+
: "\nTo display the QR code, run in an interactive terminal:\n");
|
|
192
|
+
process.stdout.write(`\n ${pairingCommand} --show-qr ${quoteArgument(identityPath)}\n`);
|
|
122
193
|
}
|
|
123
|
-
process.stdout.write(
|
|
194
|
+
process.stdout.write(`\nSaved QR image: ${imagePath}\nPairing file (Choose identity file): ${identityPath}\n`);
|
|
195
|
+
process.stdout.write(expiry);
|
|
196
|
+
process.stdout.write(`\nHelp:\n ${pairingCommand} --help\n`);
|
|
197
|
+
} else {
|
|
198
|
+
process.stdout.write("\nPair your phone\n\n");
|
|
199
|
+
process.stdout.write("Open LePan > Settings > Set up users > Scan QR code.\n");
|
|
200
|
+
process.stdout.write(imageOpened
|
|
201
|
+
? "The QR image has opened automatically. Scan it with your phone.\n"
|
|
202
|
+
: fitsScreen ? "Scan the QR code below with your phone.\n"
|
|
203
|
+
: process.stdout.isTTY
|
|
204
|
+
? `The QR code needs ${width + 1} columns and ${height + 3} rows. Resize this terminal or open the saved image.\n`
|
|
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");
|
|
206
|
+
if (!fitsScreen && process.stdout.isTTY) process.stdout.write(`${widthMarker}\n`);
|
|
207
|
+
process.stdout.write(`${privacyNote}\n`);
|
|
208
|
+
process.stdout.write(expiry);
|
|
209
|
+
process.stdout.write(`\nSaved QR image: ${imagePath}\n`);
|
|
210
|
+
process.stdout.write(`Pairing file (Choose identity file): ${identityPath}\n`);
|
|
211
|
+
process.stdout.write("To import the file, transfer it privately to your phone, then open\nLePan > Settings > Set up users > Choose identity file.\n");
|
|
212
|
+
process.stdout.write(reopenHint);
|
|
213
|
+
process.stdout.write("Keep this computer on and connected while using LePan.\n");
|
|
214
|
+
process.stdout.write(renewalHint);
|
|
215
|
+
if (pairing.type !== "lifedrive-device-identity") {
|
|
216
|
+
process.stdout.write("Redisplaying the QR does not extend the invitation's ten-minute expiry.\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (fitsScreen) {
|
|
220
|
+
printTerminalQr(modules, options.margin);
|
|
124
221
|
}
|
|
125
222
|
}
|
|
126
223
|
|
|
@@ -130,7 +227,7 @@ async function runInstaller(command, args) {
|
|
|
130
227
|
try {
|
|
131
228
|
const result = spawnSync(command, args, {
|
|
132
229
|
stdio: "inherit",
|
|
133
|
-
env: { ...process.env, LIFEDRIVE_PAIRING_RESULT: resultPath },
|
|
230
|
+
env: { ...process.env, LIFEDRIVE_PAIRING_RESULT: resultPath, LIFEDRIVE_PAIRING_COMMAND: pairingCommand },
|
|
134
231
|
});
|
|
135
232
|
if (result.error) throw result.error;
|
|
136
233
|
if (typeof result.status === "number") process.exitCode = result.status;
|
|
@@ -142,6 +239,7 @@ async function runInstaller(command, args) {
|
|
|
142
239
|
catch {
|
|
143
240
|
// Parser errors may contain identity text; never print them.
|
|
144
241
|
process.stderr.write("\nPhone pairing could not be prepared. Use your existing identity file, or create a new setup invitation from the node.\n");
|
|
242
|
+
process.stderr.write(`\nHelp:\n ${pairingCommand} --help\n`);
|
|
145
243
|
}
|
|
146
244
|
}
|
|
147
245
|
} finally {
|
|
@@ -182,6 +280,43 @@ function windowsArguments(values) {
|
|
|
182
280
|
}
|
|
183
281
|
|
|
184
282
|
async function main() {
|
|
283
|
+
const argumentsList = process.argv.slice(2);
|
|
284
|
+
if (argumentsList.some(argument => argument === "--show-qr" || argument.startsWith("--show-qr="))) {
|
|
285
|
+
const file = argumentsList[0] === "--show-qr" && argumentsList.length === 2
|
|
286
|
+
? argumentsList[1]
|
|
287
|
+
: argumentsList.length === 1 && argumentsList[0].startsWith("--show-qr=")
|
|
288
|
+
? argumentsList[0].slice("--show-qr=".length) : "";
|
|
289
|
+
if (!file || file.startsWith("--")) throw new Error("Use --show-qr FILE by itself to display a saved identity or invitation.");
|
|
290
|
+
const identityPath = path.resolve(file);
|
|
291
|
+
if (!fs.existsSync(identityPath)) throw new Error("The saved identity or invitation file was not found. Use the path printed during setup or upgrade.");
|
|
292
|
+
// JSON parse failures can contain private identity data. Keep them off stderr.
|
|
293
|
+
try { await showPairingCode(identityPath, true); }
|
|
294
|
+
catch { throw new Error("Could not display the pairing file. Choose a readable LePan identity or invitation JSON file."); }
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (argumentsList.includes("--help") || argumentsList.includes("-h")) {
|
|
298
|
+
process.stdout.write(`Pairing help:
|
|
299
|
+
--show-qr FILE Display a saved identity or invitation as a QR code.
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
${pairingCommand} --show-qr '/path/to/lepan-pairing.json'
|
|
303
|
+
|
|
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.
|
|
306
|
+
You can also open the saved PNG, or transfer the JSON privately to your phone
|
|
307
|
+
and use LePan > Settings > Set up users > Choose identity file.
|
|
308
|
+
|
|
309
|
+
Device identity files contain persistent private keys and have no ten-minute
|
|
310
|
+
expiry. Keep the original safe and offline; delete transfer copies after pairing.
|
|
311
|
+
Setup and reconnect invitations expire ten minutes after creation. Showing the
|
|
312
|
+
QR again does not renew them. Use --household to renew an unclaimed first-owner
|
|
313
|
+
invitation, or --recover-device ACCOUNT to reconnect an existing account.
|
|
314
|
+
For another household device, use Settings > My devices on a paired phone.
|
|
315
|
+
If a saved identity file is missing, restore your private backup or use the
|
|
316
|
+
device-management options below.
|
|
317
|
+
|
|
318
|
+
`);
|
|
319
|
+
}
|
|
185
320
|
if (process.platform === "win32") {
|
|
186
321
|
const installerPath = path.join(distributionDirectory, "lifedrive-install.ps1");
|
|
187
322
|
if (![installerPath, bundlePath, checksumPath].every(candidate => fs.existsSync(candidate))) {
|
|
@@ -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,15 @@ 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)"
|
|
478
|
+
$pairingLauncher = if ($env:LIFEDRIVE_PAIRING_COMMAND) { $env:LIFEDRIVE_PAIRING_COMMAND } else { "npx --yes @inoku/lepan@latest" }
|
|
479
|
+
Write-Host "Saved pairing files, if retained: $root\.lifedrive-household\lepan-pairing-*.json (with matching .png images)."
|
|
480
|
+
Write-Host ""
|
|
481
|
+
Write-Host "Help:"
|
|
482
|
+
Write-Host " $pairingLauncher --help"
|
|
475
483
|
return
|
|
476
484
|
}
|
|
477
485
|
try {
|
|
@@ -32,15 +32,24 @@ Installer options:
|
|
|
32
32
|
--household Set up household users or renew an unclaimed setup invitation.
|
|
33
33
|
--recover-device USER Reconnect a phone to an existing account, using its name or user ID.
|
|
34
34
|
--household-config FILE Install mobile-led household setup using a private service configuration.
|
|
35
|
+
--add-device LABEL Create a separate device identity (legacy browser-owner setup, Linux/macOS).
|
|
35
36
|
|
|
36
37
|
Setup options are forwarded to lifeDrive-setup.sh. Common examples:
|
|
37
38
|
--registry-url URL
|
|
38
39
|
--publisher-key FILE
|
|
39
|
-
--add-device LABEL
|
|
40
40
|
--identity-out FILE
|
|
41
41
|
--list-devices
|
|
42
42
|
--revoke-device UID
|
|
43
43
|
--skip-domain
|
|
44
|
+
|
|
45
|
+
Create an identity for another phone or browser:
|
|
46
|
+
npx --yes @inoku/lepan@latest --add-device "My iPhone"
|
|
47
|
+
|
|
48
|
+
LABEL is a name you choose to recognize the device, such as "My iPhone".
|
|
49
|
+
Each device gets its own key and can be revoked independently. The npm installer
|
|
50
|
+
displays the new identity's QR code and file path for pairing.
|
|
51
|
+
Run this command on the computer running Leither.
|
|
52
|
+
For household accounts, invite another device from Settings > My devices on a paired phone.
|
|
44
53
|
EOF
|
|
45
54
|
}
|
|
46
55
|
|