pi-harness-whatsapp 0.1.1 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 — 2026-10-16
4
+
5
+ - **Fixed file permissions:** the data folder and the WhatsApp login in it were readable by other users on the same machine. Every folder the extension creates is now private (only your user can open it), and existing installs are fixed the next time Pi starts.
6
+
3
7
  ## 0.1.1 — 2026-10-15
4
8
 
5
9
  - **Voice is optional, and off by default.** `/whatsapp` → Voice → **Voice notes** has two ticks: understand voice notes (speech-to-text, 1.0 GB) and reply with voice notes (text-to-speech, 0.6 GB), plus "Delete all speech files". Leaving the menu asks before deleting the files of a part you unticked ("You set speech-to-text for removal. 828 MB will be freed."; No puts the tick back) and offers to download a part you ticked (now, or when first needed). Nothing is downloaded while voice is off.
package/README.md CHANGED
@@ -218,7 +218,7 @@ Everything lives in `~/.pi/agent/pi-harness-whatsapp/`:
218
218
  | `setup.json` | Only if the installer recorded it: programs installed just for the extension (see [Uninstall](#what-was-installed-for-it)) |
219
219
  | `log/pi-harness-whatsapp.log` | Log: connection events and errors, no message text (unless debug logging is on). Rotated at 5 MB into `.log.1`. |
220
220
 
221
- If you set `PI_HARNESS_WHATSAPP_DIR`, everything is in that folder instead. The extension writes nothing anywhere else, apart from Pi's own files (your sessions and its package list).
221
+ The folder is private: only your user can open it. If you set `PI_HARNESS_WHATSAPP_DIR`, everything is in that folder instead. The extension writes nothing anywhere else, apart from Pi's own files (your sessions and its package list).
222
222
 
223
223
  ## Uninstall
224
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-whatsapp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Talk to Pi from WhatsApp: images, files and optional voice notes in and out, automatic replies, group chats, poll menus to control Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/controller.ts CHANGED
@@ -20,7 +20,7 @@ import { cleanInbox, Media } from "./media.ts";
20
20
  import type { SpeechInfo } from "./panel-model.ts";
21
21
  import { getLanguage } from "./languages.ts";
22
22
  import { type ConnectionCommand, type PanelHost, showPanel } from "./panel.ts";
23
- import { dataDir, paths, removeDataDir } from "./paths.ts";
23
+ import { dataDir, makePrivate, paths, removeDataDir } from "./paths.ts";
24
24
  import { isRemoved, markRemoved } from "./removed.ts";
25
25
  import { detectService } from "./commands/power.ts";
26
26
  import {
@@ -130,6 +130,9 @@ export class Controller implements PanelHost, CommandEnv {
130
130
  ctx.ui.setStatus(STATUS_KEY, REMOVED_STATUS);
131
131
  return;
132
132
  }
133
+ // Installs from before 0.1.2 left the data folder (and the login in it) readable by other users.
134
+ makePrivate(dataDir());
135
+ makePrivate(paths.auth(), true);
133
136
  let settings = this.settings.load();
134
137
  this.recents.load();
135
138
  cleanInbox(paths.inbox());
package/src/log.ts CHANGED
@@ -39,7 +39,7 @@ function serialize(value: unknown): string {
39
39
  function write(level: Level, scope: string, parts: unknown[]): void {
40
40
  if (LEVELS[level] < LEVELS[minLevel] || isRemoved()) return;
41
41
  try {
42
- mkdirSync(paths.logs(), { recursive: true });
42
+ mkdirSync(paths.logs(), { recursive: true, mode: 0o700 });
43
43
  const target = file();
44
44
  rotateIfNeeded(target);
45
45
  const line = `${new Date().toISOString()} ${level.toUpperCase().padEnd(5)} [${scope}] ${parts.map(serialize).join(" ")}\n`;
package/src/media.ts CHANGED
@@ -171,7 +171,7 @@ export class Media implements MediaHandler {
171
171
  try {
172
172
  const data = await this.deps.connection.download(message.raw);
173
173
  if (data.length > MAX_FILE_BYTES) return { content: `${header}${quoted} (sent ${what} too large to save)`, voice: false };
174
- mkdirSync(this.deps.inboxDir, { recursive: true });
174
+ mkdirSync(this.deps.inboxDir, { recursive: true, mode: 0o700 });
175
175
  const name = safeFileName(message.fileName, EXT_BY_MIME[message.mimeType ?? ""] ?? (message.kind === "video" ? ".mp4" : ".bin"));
176
176
  const path = join(this.deps.inboxDir, name);
177
177
  writeFileSync(path, data, { mode: 0o600 });
package/src/paths.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, rmdirSync, rmSync } from "node:fs";
1
+ import { chmodSync, existsSync, readdirSync, rmdirSync, rmSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -12,6 +12,26 @@ export function dataDir(): string {
12
12
  return join(agentDir, "pi-harness-whatsapp");
13
13
  }
14
14
 
15
+ /** Folders the extension creates: only this user may open them (the data folder holds the WhatsApp login). */
16
+ export const PRIVATE_DIR_MODE = 0o700;
17
+
18
+ /**
19
+ * Makes a folder private (owner only), and with `files` also the files directly
20
+ * in it. Fixes installs from before 0.1.2, whose login was readable by other users.
21
+ */
22
+ export function makePrivate(dir: string, files = false): void {
23
+ try {
24
+ if (!existsSync(dir)) return;
25
+ chmodSync(dir, PRIVATE_DIR_MODE);
26
+ if (!files) return;
27
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
28
+ if (entry.isFile()) chmodSync(join(dir, entry.name), 0o600);
29
+ }
30
+ } catch {
31
+ // Not every file system has Unix permissions (e.g. Windows); nothing to fix there.
32
+ }
33
+ }
34
+
15
35
  export const paths = {
16
36
  config: () => join(dataDir(), "config.json"),
17
37
  auth: () => join(dataDir(), "auth"),
package/src/settings.ts CHANGED
@@ -271,7 +271,7 @@ export class SettingsStore {
271
271
 
272
272
  private save(): void {
273
273
  if (isRemoved()) return;
274
- mkdirSync(dirname(this.filePath), { recursive: true });
274
+ mkdirSync(dirname(this.filePath), { recursive: true, mode: 0o700 });
275
275
  const temporary = `${this.filePath}.${process.pid}.tmp`;
276
276
  writeFileSync(temporary, `${JSON.stringify(this.settings, null, 2)}\n`, { mode: 0o600 });
277
277
  renameSync(temporary, this.filePath);
@@ -202,7 +202,7 @@ export class ModelStore {
202
202
 
203
203
  private async install(spec: ModelSpec): Promise<string> {
204
204
  if (!spec.hf) throw new Error(`${spec.name} can't be downloaded`);
205
- mkdirSync(this.root, { recursive: true });
205
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
206
206
  const staging = this.staging(spec);
207
207
  log.info(`downloading ${spec.hf.repo}@${spec.hf.revision.slice(0, 8)}`);
208
208
  this.onProgress?.(spec, 0);
@@ -244,7 +244,7 @@ export class ModelStore {
244
244
  if (!runtimeSupported(os, cpu)) {
245
245
  throw new Error(`OpenVINO has no build for ${os}/${cpu}, so voice notes can't work on this machine`);
246
246
  }
247
- mkdirSync(this.root, { recursive: true });
247
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
248
248
  const staging = join(this.root, `.${spec.name}.downloading`);
249
249
  this.onProgress?.(spec, 0);
250
250
  try {
@@ -4,8 +4,9 @@
4
4
  * talks to Baileys' socket directly, so a Baileys upgrade stays contained.
5
5
  */
6
6
 
7
- import { existsSync, readFileSync, rmSync } from "node:fs";
8
- import { join } from "node:path";
7
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { makePrivate } from "../paths.ts";
9
10
  import makeWASocket, {
10
11
  type AnyMessageContent,
11
12
  Browsers,
@@ -298,6 +299,10 @@ export class WhatsAppConnection {
298
299
  const generation = ++this.generation;
299
300
  this.setState({ kind: "connecting", attempt: this.attempt });
300
301
  try {
302
+ // The login lets anyone use the account: only this user may open its folder.
303
+ mkdirSync(this.authDir, { recursive: true, mode: 0o700 });
304
+ makePrivate(dirname(this.authDir));
305
+ makePrivate(this.authDir, true);
301
306
  const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
302
307
  if (generation !== this.generation || !this.wantRunning) return;
303
308
  this.version ??= await this.fetchVersion();
package/src/wa/lock.ts CHANGED
@@ -74,7 +74,7 @@ export class ConnectionLock {
74
74
 
75
75
  /** Takes the lock unless another live process on this machine holds it. */
76
76
  acquire(): LockResult {
77
- mkdirSync(dirname(this.file), { recursive: true });
77
+ mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
78
78
  const me: LockHolder = { pid: this.pid, host: hostname(), since: Date.now() };
79
79
  for (let attempt = 0; attempt < 2; attempt++) {
80
80
  try {
package/src/wa/recents.ts CHANGED
@@ -99,7 +99,7 @@ export class RecentsStore {
99
99
  if (!this.dirty || isRemoved()) return;
100
100
  this.dirty = false;
101
101
  try {
102
- mkdirSync(dirname(this.file), { recursive: true });
102
+ mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
103
103
  const temporary = `${this.file}.${process.pid}.tmp`;
104
104
  const data = { chats: this.list().slice(0, MAX_CHATS), lids: [...this.lidToPhone] };
105
105
  writeFileSync(temporary, JSON.stringify(data), { mode: 0o600 });