@floomhq/signaldash 0.13.0 → 0.27.0

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/lib/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { chmod, mkdir, readFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { updateConfigFile } from "./config-file.js";
5
6
  import { readSecrets, writeSecrets } from "./secrets.js";
6
7
  import { runMcp } from "./mcp.js";
7
8
  import { UnipileClient, accountIsReady } from "./unipile.js";
@@ -49,16 +50,6 @@ async function ensureWorkspace(workspace) {
49
50
  await chmod(workspace, 0o700);
50
51
  }
51
52
 
52
- async function writeJsonPrivate(target, value) {
53
- const temporary = `${target}.${process.pid}.tmp`;
54
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
55
- mode: 0o600,
56
- });
57
- await chmod(temporary, 0o600);
58
- await rename(temporary, target);
59
- await chmod(target, 0o600);
60
- }
61
-
62
53
  export async function readConfig(workspace, { optional = false } = {}) {
63
54
  try {
64
55
  return JSON.parse(
@@ -276,18 +267,21 @@ export async function connectCommand(channel, options, dependencies = {}) {
276
267
  return { connected: false, hostedUrl };
277
268
  }
278
269
 
279
- const nextConfig = {
280
- version: 1,
281
- channels: {
282
- ...(config.channels || {}),
283
- [channel]: {
284
- accountId: selected.id,
285
- name: selected.name || null,
286
- connectedAt: new Date().toISOString(),
270
+ updateConfigFile(
271
+ path.join(workspace, "config.json"),
272
+ latest => ({
273
+ ...latest,
274
+ version: 1,
275
+ channels: {
276
+ ...(latest.channels || {}),
277
+ [channel]: {
278
+ accountId: selected.id,
279
+ name: selected.name || null,
280
+ connectedAt: new Date().toISOString(),
281
+ },
287
282
  },
288
- },
289
- };
290
- await writeJsonPrivate(path.join(workspace, "config.json"), nextConfig);
283
+ }),
284
+ );
291
285
  io.stdout.write(
292
286
  `${definition.label} connected: ${selected.name || selected.id} (${selected.id})\n`,
293
287
  );
@@ -0,0 +1,114 @@
1
+ import {
2
+ chmodSync,
3
+ closeSync,
4
+ fsyncSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+ import { randomBytes } from "node:crypto";
14
+
15
+
16
+ const WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
17
+ const LOCK_TIMEOUT_MS = 5_000;
18
+
19
+
20
+ function wait(milliseconds) {
21
+ Atomics.wait(WAIT_ARRAY, 0, 0, milliseconds);
22
+ }
23
+
24
+
25
+ function acquireLock(lockFile) {
26
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
27
+ for (;;) {
28
+ try {
29
+ return openSync(lockFile, "wx", 0o600);
30
+ } catch (error) {
31
+ if (error.code !== "EEXIST") throw error;
32
+ if (Date.now() >= deadline) {
33
+ throw new Error(
34
+ `timed out waiting for SignalDash config lock: ${lockFile}; ` +
35
+ "the lock is never stolen automatically because that could corrupt the session",
36
+ );
37
+ }
38
+ wait(25);
39
+ }
40
+ }
41
+ }
42
+
43
+
44
+ function releaseLock(lockFile, descriptor) {
45
+ try {
46
+ closeSync(descriptor);
47
+ } finally {
48
+ try {
49
+ unlinkSync(lockFile);
50
+ } catch (error) {
51
+ if (error.code !== "ENOENT") throw error;
52
+ }
53
+ }
54
+ }
55
+
56
+
57
+ export function readConfigFile(file, { optional = false } = {}) {
58
+ try {
59
+ return JSON.parse(readFileSync(file, "utf8"));
60
+ } catch (error) {
61
+ if (error.code === "ENOENT" && optional) return {};
62
+ throw error;
63
+ }
64
+ }
65
+
66
+
67
+ export function updateConfigFile(file, update) {
68
+ const directory = path.dirname(file);
69
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
70
+ chmodSync(directory, 0o700);
71
+ const lockFile = `${file}.lock`;
72
+ const descriptor = acquireLock(lockFile);
73
+ let temporary;
74
+ try {
75
+ const current = readConfigFile(file, { optional: true });
76
+ const next = update({ ...current });
77
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
78
+ throw new TypeError("SignalDash config update must return an object");
79
+ }
80
+ temporary = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
81
+ writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, {
82
+ mode: 0o600,
83
+ flag: "wx",
84
+ });
85
+ const temporaryDescriptor = openSync(temporary, "r");
86
+ try {
87
+ fsyncSync(temporaryDescriptor);
88
+ } finally {
89
+ closeSync(temporaryDescriptor);
90
+ }
91
+ renameSync(temporary, file);
92
+ temporary = undefined;
93
+ chmodSync(file, 0o600);
94
+ const directoryDescriptor = openSync(directory, "r");
95
+ try {
96
+ fsyncSync(directoryDescriptor);
97
+ } finally {
98
+ closeSync(directoryDescriptor);
99
+ }
100
+ return next;
101
+ } finally {
102
+ try {
103
+ if (temporary) {
104
+ try {
105
+ unlinkSync(temporary);
106
+ } catch (error) {
107
+ if (error.code !== "ENOENT") throw error;
108
+ }
109
+ }
110
+ } finally {
111
+ releaseLock(lockFile, descriptor);
112
+ }
113
+ }
114
+ }
package/lib/mcp.js CHANGED
@@ -127,7 +127,7 @@ function cleanMessage(message) {
127
127
  id: message.id ?? message.provider_id ?? null,
128
128
  text: message.text ?? null,
129
129
  timestamp: message.timestamp ?? null,
130
- is_sender: Boolean(message.is_sender),
130
+ is_sender: providerIsOwn(message.is_sender),
131
131
  sender_id: message.sender_id ?? null,
132
132
  attachments: (message.attachments || []).map((attachment) => ({
133
133
  id: attachment.id ?? null,
@@ -140,6 +140,16 @@ function cleanMessage(message) {
140
140
  };
141
141
  }
142
142
 
143
+ function providerIsOwn(value) {
144
+ if (typeof value === "boolean") return value;
145
+ if (typeof value === "number") return value !== 0;
146
+ if (typeof value === "string") {
147
+ const normalized = value.trim().toLowerCase();
148
+ return ["1", "true", "yes", "y"].includes(normalized);
149
+ }
150
+ return false;
151
+ }
152
+
143
153
  function itemList(payload) {
144
154
  return Array.isArray(payload?.items)
145
155
  ? payload.items
@@ -207,7 +217,7 @@ async function executeTool({ name, args, config, client, guard }) {
207
217
  const recent = itemList(await client.listMessages(chatId, 20));
208
218
  const duplicate = recent.find(
209
219
  (message) =>
210
- message.is_sender &&
220
+ providerIsOwn(message.is_sender) &&
211
221
  typeof message.text === "string" &&
212
222
  message.text.trim() === text,
213
223
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/signaldash",
3
- "version": "0.13.0",
3
+ "version": "0.27.0",
4
4
  "description": "Secure LinkedIn, WhatsApp, and email access for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "scripts": {
20
20
  "test": "node --test",
21
- "check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
21
+ "check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/attachments.cjs && node --check server/write-control.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
22
22
  },
23
23
  "keywords": [
24
24
  "mcp",
@@ -34,4 +34,4 @@
34
34
  "ora": "8.0.1",
35
35
  "open": "10.1.0"
36
36
  }
37
- }
37
+ }