friend-assistant 0.1.13 → 0.1.15
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 +2 -0
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +104 -3
- package/dist/web/assets/{index-D-IsArMn.js → index-1xKxmOaM.js} +27 -27
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ Every command starts with `friend`. If you run Friend from the source code, writ
|
|
|
85
85
|
|
|
86
86
|
| Command | What it does | Use it when |
|
|
87
87
|
|---|---|---|
|
|
88
|
+
| `friend password` | Sets a new sign-in password (asks twice, nothing is shown while you type). The old one is not needed, because you are at this PC, so it is also the way back in if you forgot it. Every browser and phone is signed out. | You forgot the password, or want a new one. From the screen: Esc → Settings → Security. |
|
|
89
|
+
| `friend pin` | Sets a new PIN, the number Friend asks for before risky actions. | You forgot the PIN, or want a new one. |
|
|
88
90
|
| `friend daemon start` | Runs Friend in the background, with no terminal window needed. Prints the address, keeps a log in the data folder (`friend.log`). | You want Friend always available, even after you close the terminal. |
|
|
89
91
|
| `friend daemon stop` | Stops the background Friend and the speech engine with it. | Shutting it down. |
|
|
90
92
|
| `friend daemon restart` | Stops it and starts it again. | After changing a setting such as `friend lan on` or a different model. |
|
package/dist/BUILD.json
CHANGED
package/dist/friend.mjs
CHANGED
|
@@ -3031,7 +3031,7 @@ async function verifySecret(secret, stored) {
|
|
|
3031
3031
|
const actual = await scryptAsync(secret, Buffer.from(saltHex, "hex"));
|
|
3032
3032
|
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
3033
3033
|
}
|
|
3034
|
-
var Auth = class {
|
|
3034
|
+
var Auth = class _Auth {
|
|
3035
3035
|
constructor(db, audit, clock = Date.now) {
|
|
3036
3036
|
this.db = db;
|
|
3037
3037
|
this.audit = audit;
|
|
@@ -3116,6 +3116,48 @@ var Auth = class {
|
|
|
3116
3116
|
else this.audit.log("owner", "pin.fail", { ip });
|
|
3117
3117
|
return ok;
|
|
3118
3118
|
}
|
|
3119
|
+
/** Checks a new password and/or PIN the same way setup does. Returns the problem, or null when both are fine. */
|
|
3120
|
+
static checkNewSecrets(input) {
|
|
3121
|
+
if (input.newPassword === void 0 && input.newPin === void 0) return "Give a new password or a new PIN.";
|
|
3122
|
+
if (input.newPin !== void 0 && !/^\d{6,}$/.test(input.newPin)) return "PIN must be at least 6 digits";
|
|
3123
|
+
if (input.newPassword !== void 0 && input.newPassword.length < 8) return "Password must be at least 8 characters";
|
|
3124
|
+
return null;
|
|
3125
|
+
}
|
|
3126
|
+
async store(input) {
|
|
3127
|
+
if (input.newPassword !== void 0) this.db.prepare("update owner set password_hash = ? where id = 1").run(await hashSecret(input.newPassword));
|
|
3128
|
+
if (input.newPin !== void 0) this.db.prepare("update owner set pin_hash = ? where id = 1").run(await hashSecret(input.newPin));
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Changes the password and/or PIN of a signed-in owner who proves it with the current password. Wrong guesses count toward
|
|
3132
|
+
* the same sign-in lock. A new password signs every other browser out (the one in `keepToken` stays signed in).
|
|
3133
|
+
*/
|
|
3134
|
+
async changeSecrets(input, ip, keepToken) {
|
|
3135
|
+
const problem = _Auth.checkNewSecrets(input);
|
|
3136
|
+
if (problem) throw new Error(problem);
|
|
3137
|
+
const succeeded = this.begin("login", ip);
|
|
3138
|
+
const row = this.db.prepare("select password_hash h from owner where id = 1").get();
|
|
3139
|
+
const ok = row ? await verifySecret(input.current, row.h) : false;
|
|
3140
|
+
if (!ok) {
|
|
3141
|
+
this.audit.log("owner", "secrets.fail", { ip });
|
|
3142
|
+
throw new Error("Invalid credentials");
|
|
3143
|
+
}
|
|
3144
|
+
succeeded();
|
|
3145
|
+
await this.store(input);
|
|
3146
|
+
if (input.newPassword !== void 0) {
|
|
3147
|
+
if (keepToken) this.db.prepare("update sessions set revoked = 1 where token_hash != ?").run(sha256(keepToken));
|
|
3148
|
+
else this.db.prepare("update sessions set revoked = 1").run();
|
|
3149
|
+
}
|
|
3150
|
+
this.audit.log("owner", "secrets.changed", { password: input.newPassword !== void 0, pin: input.newPin !== void 0 });
|
|
3151
|
+
}
|
|
3152
|
+
/** For `friend password` / `friend pin` run on this PC: no current password needed (being on the PC is the proof). Signs everyone out. */
|
|
3153
|
+
async resetSecrets(input) {
|
|
3154
|
+
if (!this.hasOwner()) throw new Error("There is no owner yet. Run: friend setup");
|
|
3155
|
+
const problem = _Auth.checkNewSecrets(input);
|
|
3156
|
+
if (problem) throw new Error(problem);
|
|
3157
|
+
await this.store(input);
|
|
3158
|
+
if (input.newPassword !== void 0) this.db.prepare("update sessions set revoked = 1").run();
|
|
3159
|
+
this.audit.log("system", "secrets.reset", { password: input.newPassword !== void 0, pin: input.newPin !== void 0 });
|
|
3160
|
+
}
|
|
3119
3161
|
createPairCode() {
|
|
3120
3162
|
const code = randomBytes(5).toString("hex").toUpperCase();
|
|
3121
3163
|
this.db.prepare("insert into pair_codes (code_hash, expires_at) values (?, ?)").run(sha256(code), this.clock() + PAIR_TTL_MS);
|
|
@@ -4078,6 +4120,30 @@ function createGateway(deps) {
|
|
|
4078
4120
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
4079
4121
|
return { ok: true };
|
|
4080
4122
|
});
|
|
4123
|
+
app.post("/api/account/secret", async (req, reply) => {
|
|
4124
|
+
reply.header("cache-control", "no-store");
|
|
4125
|
+
const token = req.cookies[COOKIE_NAME];
|
|
4126
|
+
if (!token || !auth.resolveSession(token)) return reply.code(401).send({ error: "Please sign in again." });
|
|
4127
|
+
const body = z10.object({ current: z10.string().min(1).max(1024), newPassword: z10.string().max(1024).optional(), newPin: z10.string().max(128).optional() }).safeParse(req.body);
|
|
4128
|
+
if (!body.success) return reply.code(400).send({ error: "Invalid input" });
|
|
4129
|
+
const { current, newPassword, newPin } = body.data;
|
|
4130
|
+
try {
|
|
4131
|
+
await auth.changeSecrets({ current, newPassword, newPin }, req.ip, token);
|
|
4132
|
+
return { ok: true, signedOutOthers: newPassword !== void 0 };
|
|
4133
|
+
} catch (e) {
|
|
4134
|
+
if (e instanceof LockoutError) {
|
|
4135
|
+
const seconds = Math.max(1, Math.ceil(e.retryAfterMs / 1e3));
|
|
4136
|
+
return reply.code(429).header("retry-after", seconds).send({ error: `Too many wrong passwords. Try again in ${waitText(seconds)}.`, retryAfterSeconds: seconds });
|
|
4137
|
+
}
|
|
4138
|
+
if (e.message === "Invalid credentials") {
|
|
4139
|
+
const state = auth.loginState(req.ip);
|
|
4140
|
+
const seconds = Math.ceil(state.retryAfterMs / 1e3);
|
|
4141
|
+
if (seconds > 0) return reply.code(401).send({ error: `Wrong password. Locked for ${waitText(seconds)}.`, triesLeft: 0, retryAfterSeconds: seconds });
|
|
4142
|
+
return reply.code(401).send({ error: "The current password is wrong.", triesLeft: state.triesLeft });
|
|
4143
|
+
}
|
|
4144
|
+
return reply.code(400).send({ error: e.message });
|
|
4145
|
+
}
|
|
4146
|
+
});
|
|
4081
4147
|
const pairLimiter = new RateLimiter(Date.now);
|
|
4082
4148
|
app.post("/api/pair", async (req, reply) => {
|
|
4083
4149
|
const body = z10.object({ code: z10.string().min(1).max(64), name: z10.string().min(1).max(64) }).safeParse(req.body);
|
|
@@ -9594,7 +9660,7 @@ async function daemonCommand(args, io2, deps = realDeps) {
|
|
|
9594
9660
|
import crossSpawn5 from "cross-spawn";
|
|
9595
9661
|
var PACKAGE = "friend-assistant";
|
|
9596
9662
|
var real = {
|
|
9597
|
-
current: () => true ? "0.1.
|
|
9663
|
+
current: () => true ? "0.1.15" : "development",
|
|
9598
9664
|
latest: () => new Promise((resolve10) => {
|
|
9599
9665
|
const child = crossSpawn5("npm", ["view", PACKAGE, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
9600
9666
|
let out = "";
|
|
@@ -9772,6 +9838,35 @@ async function setupCommand3(io2) {
|
|
|
9772
9838
|
}
|
|
9773
9839
|
}
|
|
9774
9840
|
|
|
9841
|
+
// ../agent-cli/src/commands/secret.ts
|
|
9842
|
+
async function secretCommand(kind, io2) {
|
|
9843
|
+
const db = openDb(io2.dataDir);
|
|
9844
|
+
try {
|
|
9845
|
+
const auth = new Auth(db, new Audit(db));
|
|
9846
|
+
if (!auth.hasOwner()) {
|
|
9847
|
+
io2.err("There is no owner yet. Run: friend setup");
|
|
9848
|
+
return 1;
|
|
9849
|
+
}
|
|
9850
|
+
const label2 = kind === "password" ? "password" : "PIN";
|
|
9851
|
+
const first2 = await io2.ask(`New ${label2} (${kind === "password" ? "at least 8 characters" : "at least 6 digits"}): `, { hidden: true });
|
|
9852
|
+
const problem = Auth.checkNewSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
|
|
9853
|
+
if (problem) {
|
|
9854
|
+
io2.err(problem);
|
|
9855
|
+
return 1;
|
|
9856
|
+
}
|
|
9857
|
+
const again = await io2.ask(`Type the new ${label2} again: `, { hidden: true });
|
|
9858
|
+
if (again !== first2) {
|
|
9859
|
+
io2.err(`The two ${label2}s are not the same. Nothing was changed.`);
|
|
9860
|
+
return 1;
|
|
9861
|
+
}
|
|
9862
|
+
await auth.resetSecrets(kind === "password" ? { newPassword: first2 } : { newPin: first2 });
|
|
9863
|
+
io2.out(kind === "password" ? "Password changed. Every browser and phone has been signed out; sign in again with the new password." : "PIN changed.");
|
|
9864
|
+
return 0;
|
|
9865
|
+
} finally {
|
|
9866
|
+
db.close();
|
|
9867
|
+
}
|
|
9868
|
+
}
|
|
9869
|
+
|
|
9775
9870
|
// ../agent-cli/src/commands/status.ts
|
|
9776
9871
|
async function statusCommand3(io2) {
|
|
9777
9872
|
const cfg = loadConfig(io2.dataDir);
|
|
@@ -9796,6 +9891,8 @@ var USAGE4 = `Usage: friend <command>
|
|
|
9796
9891
|
onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
|
|
9797
9892
|
doctor check what this PC has and what is missing (changes nothing)
|
|
9798
9893
|
setup create the owner account (password, PIN, assistant name)
|
|
9894
|
+
password change the sign-in password (signs every browser out)
|
|
9895
|
+
pin change the PIN for risky actions
|
|
9799
9896
|
start run the gateway and a local device node
|
|
9800
9897
|
daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
|
|
9801
9898
|
upgrade [--force] stop the background Friend, install the newest version, start it again
|
|
@@ -9821,6 +9918,10 @@ async function run(argv, io2) {
|
|
|
9821
9918
|
switch (cmd) {
|
|
9822
9919
|
case "setup":
|
|
9823
9920
|
return await setupCommand3(io2);
|
|
9921
|
+
case "password":
|
|
9922
|
+
return await secretCommand("password", io2);
|
|
9923
|
+
case "pin":
|
|
9924
|
+
return await secretCommand("pin", io2);
|
|
9824
9925
|
case "start":
|
|
9825
9926
|
return await startCommand(io2);
|
|
9826
9927
|
case "daemon":
|
|
@@ -9847,7 +9948,7 @@ async function run(argv, io2) {
|
|
|
9847
9948
|
case "version":
|
|
9848
9949
|
case "--version":
|
|
9849
9950
|
case "-v":
|
|
9850
|
-
io2.out(true ? "0.1.
|
|
9951
|
+
io2.out(true ? "0.1.15" : "development");
|
|
9851
9952
|
return 0;
|
|
9852
9953
|
case "lan":
|
|
9853
9954
|
return await lanCommand(rest, io2);
|