@floomhq/signaldash 0.12.0 → 0.22.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@floomhq/signaldash",
3
- "version": "0.12.0",
4
- "description": "Secure LinkedIn and WhatsApp MCP access for AI agents",
3
+ "version": "0.22.0",
4
+ "description": "Secure LinkedIn, WhatsApp, and email access for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "signaldash": "bin/sd.mjs"
@@ -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/write-control.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",