@securecoms/agent-bridge 0.3.1 → 0.4.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.
Files changed (45) hide show
  1. package/README.md +11 -1
  2. package/dist/bin.js +892 -118
  3. package/dist/bridge-agent.d.ts +69 -8
  4. package/dist/config.d.ts +6 -0
  5. package/dist/http-server.d.ts +42 -0
  6. package/dist/index.js +844 -62
  7. package/dist/memory/hermes-provider.d.ts +21 -0
  8. package/dist/memory/hermes-session-map.d.ts +8 -0
  9. package/dist/memory/hermes-store.d.ts +32 -0
  10. package/dist/memory/hermes-test-fixture.d.ts +5 -0
  11. package/dist/memory/openclaw-provider.d.ts +24 -0
  12. package/dist/memory/openclaw-session-log.d.ts +15 -0
  13. package/dist/memory/openclaw-session-map.d.ts +7 -0
  14. package/dist/memory/provider.d.ts +29 -0
  15. package/dist/memory-taps.d.ts +13 -0
  16. package/dist/preflight-modern.cjs +35 -0
  17. package/dist/preflight-modern.d.cts +1 -0
  18. package/dist/preflight-upgrade.d.cts +30 -0
  19. package/dist/preflight.cjs +277 -0
  20. package/dist/preflight.d.cts +14 -0
  21. package/package.json +3 -5
  22. package/dist/attachments.js +0 -49
  23. package/dist/auth.js +0 -56
  24. package/dist/bin.js.pre-c1-f803007 +0 -5795
  25. package/dist/bin.js.pre-c3-096a9a3 +0 -5832
  26. package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
  27. package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
  28. package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
  29. package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
  30. package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
  31. package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
  32. package/dist/bin.js.pretrace-113205 +0 -5395
  33. package/dist/bridge-agent.js +0 -831
  34. package/dist/config.js +0 -74
  35. package/dist/coordination.js +0 -63
  36. package/dist/errors.js +0 -25
  37. package/dist/http-server.js +0 -220
  38. package/dist/index.js.pre-p0-20260708-102415 +0 -4879
  39. package/dist/latency.js +0 -87
  40. package/dist/rate-limit.js +0 -56
  41. package/dist/service/config.js +0 -26
  42. package/dist/service/install.js +0 -60
  43. package/dist/service/launchd.js +0 -30
  44. package/dist/service/systemd.js +0 -25
  45. package/dist/ws-server.js +0 -254
@@ -0,0 +1,21 @@
1
+ import type { MemoryProvider } from "./provider.js";
2
+ import type { MemoryRecordInput, MemoryContextResult } from "@agentvault/connect-mls-store";
3
+ /**
4
+ * Backs the MemoryProvider interface with Hermes's own ~/.hermes brain:
5
+ * context() reads state.db recent turns; record() writes voice turns back.
6
+ * Text records are no-ops (Hermes persists text natively). Fully fail-safe.
7
+ */
8
+ export declare class HermesMemoryProvider implements MemoryProvider {
9
+ private readonly baseDir;
10
+ private readonly store;
11
+ private noSession;
12
+ private skipped;
13
+ private writeFailed;
14
+ constructor(baseDir: string);
15
+ record(input: MemoryRecordInput): Promise<{
16
+ seq: number;
17
+ deduped: boolean;
18
+ }>;
19
+ context(conversationId: string, limit?: number): Promise<MemoryContextResult>;
20
+ stats(): Record<string, number>;
21
+ }
@@ -0,0 +1,8 @@
1
+ /** Expand a leading ~ to the user's home dir. Absolute/relative paths pass through. */
2
+ export declare function expandHome(p: string): string;
3
+ /**
4
+ * Resolve a SecureComs DM channel to its Hermes state.db session id via
5
+ * sessions.json, or null. DM keys only (group keys are out of scope this slice).
6
+ * Fail-safe: any read/parse error returns null.
7
+ */
8
+ export declare function resolveHermesSession(baseDir: string, conversationId: string): string | null;
@@ -0,0 +1,32 @@
1
+ export interface HermesTurn {
2
+ id: number;
3
+ role: "user" | "assistant";
4
+ content: string;
5
+ timestamp: number;
6
+ }
7
+ export declare class HermesStore {
8
+ private readonly dbPath;
9
+ constructor(baseDir: string);
10
+ /** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
11
+ readRecent(sessionId: string, limit: number): HermesTurn[];
12
+ private open;
13
+ /**
14
+ * Append a voice turn to a session in one synchronous BEGIN IMMEDIATE block
15
+ * (no await between statements → atomic against Hermes and our own retries).
16
+ * Guards: skip when the session is absent/archived/ended; in-transaction dedup
17
+ * on (session_id, platform_message_id). Writes only `messages`. Fail-safe.
18
+ */
19
+ writeVoiceTurn(input: {
20
+ sessionId: string;
21
+ role: "user" | "assistant";
22
+ content: string;
23
+ dedupKey: string | null;
24
+ timestamp: number;
25
+ }): "written" | "deduped" | "skipped" | "failed";
26
+ /**
27
+ * Startup write-capability probe: insert a sentinel against any existing
28
+ * session inside a rolled-back transaction. Throws if the write path is broken
29
+ * (e.g. a schema regression). No-op when the db has no sessions.
30
+ */
31
+ probeWrite(): void;
32
+ }
@@ -0,0 +1,5 @@
1
+ /** Build a state.db that mirrors the load-bearing parts of Hermes's schema. */
2
+ export declare function makeHermesFixture(base: string, sessionId: string, opts?: {
3
+ archived?: number;
4
+ endedAt?: number | null;
5
+ }): void;
@@ -0,0 +1,24 @@
1
+ import type { MemoryProvider } from "./provider.js";
2
+ import type { SqliteMlsStateStore, MemoryRecordInput, MemoryContextResult } from "@agentvault/connect-mls-store";
3
+ /**
4
+ * MemoryProvider for an OpenClaw-hosted agent (Cortina). context() merges
5
+ * OpenClaw's own text history (read-only, plain session .jsonl) with the
6
+ * SecureComs daemon-local voice turns, ordered on one wall-clock. record()
7
+ * stores voice turns in the SC store ([voice]-prefixed); text is a no-op
8
+ * (OpenClaw owns text). OpenClaw files are NEVER written. Fully fail-safe:
9
+ * any text-side failure degrades to voice-only.
10
+ */
11
+ export declare class OpenClawMemoryProvider implements MemoryProvider {
12
+ private readonly store;
13
+ private readonly baseDir;
14
+ private readonly agent;
15
+ private noSession;
16
+ private readFailed;
17
+ constructor(store: SqliteMlsStateStore, baseDir: string, agent: string);
18
+ record(input: MemoryRecordInput): Promise<{
19
+ seq: number;
20
+ deduped: boolean;
21
+ }>;
22
+ context(conversationId: string, limit?: number): Promise<MemoryContextResult>;
23
+ stats(): Record<string, number>;
24
+ }
@@ -0,0 +1,15 @@
1
+ export interface OpenClawTurn {
2
+ role: "peer" | "agent";
3
+ text: string;
4
+ ts: number;
5
+ }
6
+ /**
7
+ * Read the last K conversational turns from the agent's owner-DM session log,
8
+ * the CLEAN plain <baseDir>/agents/<agent>/sessions/<sessionId>.jsonl (NEVER the
9
+ * .trajectory.jsonl, a .reset.* snapshot, or the `main` log). Ordering uses the
10
+ * top-level ISO `timestamp`; the inner message.timestamp is unreliable and unused.
11
+ * Normalization is an allowlist gated on role: keep user (string) and assistant
12
+ * (joined `text` blocks only); skip every other role and empty (tool-only)
13
+ * assistant turns. Fail-safe: any read/parse error returns [].
14
+ */
15
+ export declare function readRecentTurns(baseDir: string, agent: string, sessionId: string, k: number): OpenClawTurn[];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolve the agent's single OpenClaw owner-DM session id from sessions.json.
3
+ * OpenClaw collapses every owner DM into one `…:direct:…:owner` session and
4
+ * discards the SecureComs channel id, so the SC conversationId is NOT used here.
5
+ * Fail-safe: any read/parse error, or no owner-direct key, returns null.
6
+ */
7
+ export declare function resolveOwnerDmSession(baseDir: string, agent: string): string | null;
@@ -0,0 +1,29 @@
1
+ import type { SqliteMlsStateStore, MemoryRecordInput, MemoryContextResult } from "@agentvault/connect-mls-store";
2
+ export interface MemoryProvider {
3
+ record(input: MemoryRecordInput): Promise<{
4
+ seq: number;
5
+ deduped: boolean;
6
+ }>;
7
+ context(conversationId: string, limit?: number): Promise<MemoryContextResult>;
8
+ /** Optional adapter counters surfaced on the daemon healthcheck. */
9
+ stats?(): Record<string, number>;
10
+ }
11
+ /** Default: the SecureComs-owned, daemon-local store (encrypted at rest). */
12
+ export declare class SecureComsLocalMemory implements MemoryProvider {
13
+ private readonly store;
14
+ constructor(store: SqliteMlsStateStore);
15
+ record(input: MemoryRecordInput): Promise<{
16
+ seq: number;
17
+ deduped: boolean;
18
+ }>;
19
+ context(conversationId: string, limit?: number): Promise<MemoryContextResult>;
20
+ }
21
+ /** BYO / disabled: the harness owns memory; SecureComs keeps none. */
22
+ export declare class NullMemory implements MemoryProvider {
23
+ record(): Promise<{
24
+ seq: number;
25
+ deduped: boolean;
26
+ }>;
27
+ context(): Promise<MemoryContextResult>;
28
+ }
29
+ export declare function createMemoryProvider(provider: "securecoms" | "none" | "hermes" | "openclaw", store: SqliteMlsStateStore, baseDir?: string, agent?: string): MemoryProvider;
@@ -0,0 +1,13 @@
1
+ import type { MemoryRecordInput } from "@agentvault/connect-mls-store";
2
+ interface MemorySink {
3
+ record(input: MemoryRecordInput): Promise<{
4
+ seq: number;
5
+ deduped: boolean;
6
+ }>;
7
+ }
8
+ /** Record an inbound (peer) text turn the agent actually consumed. Best-effort:
9
+ * never throws, so a memory failure can't break message delivery. */
10
+ export declare function recordInboundMemory(sink: MemorySink, channelId: string, senderId: string, text: string, messageId: string): Promise<void>;
11
+ /** Record the agent's own outbound text reply. Best-effort. */
12
+ export declare function recordOutboundMemory(sink: MemorySink, channelId: string, text: string, messageId: string): Promise<void>;
13
+ export {};
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/preflight-modern.cts
21
+ var preflight_modern_exports = {};
22
+ __export(preflight_modern_exports, {
23
+ passThrough: () => passThrough
24
+ });
25
+ module.exports = __toCommonJS(preflight_modern_exports);
26
+ var path = require("node:path");
27
+ function passThrough() {
28
+ const binPath = path.join(__dirname, "bin.js");
29
+ process.argv[1] = binPath;
30
+ return import(binPath);
31
+ }
32
+ // Annotate the CommonJS export names for ESM import in node:
33
+ 0 && (module.exports = {
34
+ passThrough
35
+ });
@@ -0,0 +1 @@
1
+ export declare function passThrough(): Promise<unknown>;
@@ -0,0 +1,30 @@
1
+ export declare function parseShasums(text: string, arch: string): {
2
+ filename: string;
3
+ sha256: string;
4
+ } | null;
5
+ export declare function sha256Hex(buf: Buffer): string;
6
+ export declare function promptYesNo(question: string, deps: {
7
+ isTTY: boolean;
8
+ ask: (q: string) => Promise<string>;
9
+ stderr: (s: string) => void;
10
+ }): Promise<boolean>;
11
+ export interface UpgradeDeps {
12
+ platform: string;
13
+ arch: string;
14
+ isTTY: boolean;
15
+ stderr: (s: string) => void;
16
+ ask: (q: string) => Promise<string>;
17
+ hasBrew: () => boolean;
18
+ runBrew: () => number;
19
+ fetchText: (url: string) => Promise<string>;
20
+ fetchBinary: (url: string, destPath: string) => Promise<void>;
21
+ fileSha256: (p: string) => string;
22
+ runSudoInstaller: (pkgPath: string) => number;
23
+ tmpdir: () => string;
24
+ cleanup: (p: string) => void;
25
+ resolveNewNode: () => string | null;
26
+ reexec: (nodePath: string) => boolean;
27
+ }
28
+ export declare function guidedUpgrade(deps: UpgradeDeps): Promise<boolean>;
29
+ export declare function resolveNewNodeFrom(probe: (p: string) => string | null, candidates?: string[]): string | null;
30
+ export declare function realUpgradeDeps(originalArgv: string[]): UpgradeDeps;
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
+
24
+ // src/preflight-upgrade.cts
25
+ var preflight_upgrade_exports = {};
26
+ __export(preflight_upgrade_exports, {
27
+ guidedUpgrade: () => guidedUpgrade,
28
+ parseShasums: () => parseShasums,
29
+ promptYesNo: () => promptYesNo,
30
+ realUpgradeDeps: () => realUpgradeDeps,
31
+ resolveNewNodeFrom: () => resolveNewNodeFrom,
32
+ sha256Hex: () => sha256Hex
33
+ });
34
+ function parseShasums(text, arch) {
35
+ const want = "-darwin-" + arch + ".pkg";
36
+ const lines = String(text).split("\n");
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const m = lines[i].trim().match(/^([0-9a-f]{6,64})\s+(\S+)$/);
39
+ if (m && m[2].indexOf(want) !== -1 && m[2].indexOf("node-v24.") === 0) {
40
+ return { filename: m[2], sha256: m[1] };
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function sha256Hex(buf) {
46
+ const crypto = require("crypto");
47
+ return crypto.createHash("sha256").update(buf).digest("hex");
48
+ }
49
+ async function promptYesNo(question, deps) {
50
+ if (!deps.isTTY) return false;
51
+ const a = (await deps.ask(question)).trim().toLowerCase();
52
+ return a === "y" || a === "yes";
53
+ }
54
+ async function guidedUpgrade(deps) {
55
+ if (deps.platform !== "darwin") return false;
56
+ try {
57
+ const viaBrew = deps.hasBrew();
58
+ const source = viaBrew ? "Homebrew (brew install node \u2014 user-space, no sudo)" : "the official installer from " + DIST_BASE + " (needs your admin password)";
59
+ const ok = await promptYesNo(
60
+ "[bridge] Node 24 LTS is required. Install it now via " + source + "? [y/N] ",
61
+ deps
62
+ );
63
+ if (!ok) return false;
64
+ if (viaBrew) {
65
+ deps.stderr("[bridge] running: brew install node\n");
66
+ if (deps.runBrew() !== 0) return false;
67
+ } else {
68
+ const shasums = await deps.fetchText(DIST_BASE + "/SHASUMS256.txt");
69
+ const entry = parseShasums(shasums, deps.arch);
70
+ if (!entry) return false;
71
+ const dest = deps.tmpdir() + "/" + entry.filename;
72
+ deps.stderr("[bridge] downloading " + DIST_BASE + "/" + entry.filename + "\n");
73
+ try {
74
+ await deps.fetchBinary(DIST_BASE + "/" + entry.filename, dest);
75
+ if (deps.fileSha256(dest) !== entry.sha256) {
76
+ deps.stderr("[bridge] checksum mismatch \u2014 aborting install (file discarded)\n");
77
+ return false;
78
+ }
79
+ deps.stderr("[bridge] running: sudo installer -pkg " + entry.filename + " -target /\n");
80
+ if (deps.runSudoInstaller(dest) !== 0) return false;
81
+ } finally {
82
+ deps.cleanup(dest);
83
+ }
84
+ }
85
+ const fresh = deps.resolveNewNode();
86
+ if (!fresh) return false;
87
+ deps.stderr("[bridge] Node 24 installed \u2014 continuing under " + fresh + "\n");
88
+ return deps.reexec(fresh);
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ function resolveNewNodeFrom(probe, candidates) {
94
+ const list = candidates || NODE_CANDIDATES;
95
+ for (let i = 0; i < list.length; i++) {
96
+ const v = probe(list[i]);
97
+ if (v && Number(String(v).replace(/^v/, "").split(".")[0]) >= 24) return list[i];
98
+ }
99
+ return null;
100
+ }
101
+ function realUpgradeDeps(originalArgv) {
102
+ const { spawnSync, execFileSync } = require("child_process");
103
+ const { createWriteStream, readFileSync, rmSync } = require("fs");
104
+ const os = require("os");
105
+ const https = require("https");
106
+ const readline = require("readline");
107
+ function fetch(url, redirects) {
108
+ return new Promise((resolve, reject) => {
109
+ https.get(url, (res) => {
110
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirects > 0) {
111
+ res.resume();
112
+ fetch(res.headers.location, redirects - 1).then(resolve, reject);
113
+ } else if (res.statusCode !== 200) {
114
+ res.resume();
115
+ reject(new Error("HTTP " + res.statusCode + " for " + url));
116
+ } else resolve(res);
117
+ }).on("error", reject);
118
+ });
119
+ }
120
+ return {
121
+ platform: process.platform,
122
+ arch: process.arch,
123
+ isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
124
+ stderr: (s) => process.stderr.write(s),
125
+ ask: (q) => new Promise((resolve) => {
126
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
127
+ rl.question(q, (a) => {
128
+ rl.close();
129
+ resolve(a);
130
+ });
131
+ }),
132
+ hasBrew: () => spawnSync("brew", ["--version"], { stdio: "ignore" }).status === 0,
133
+ runBrew: () => {
134
+ var _a;
135
+ return (_a = spawnSync("brew", ["install", "node"], { stdio: "inherit" }).status) != null ? _a : 1;
136
+ },
137
+ fetchText: async (url) => {
138
+ const res = await fetch(url, 3);
139
+ let out = "";
140
+ for await (const chunk of res) out += chunk;
141
+ return out;
142
+ },
143
+ fetchBinary: async (url, destPath) => {
144
+ const res = await fetch(url, 3);
145
+ await new Promise((resolve, reject) => {
146
+ const ws = createWriteStream(destPath);
147
+ res.pipe(ws);
148
+ ws.on("finish", resolve);
149
+ ws.on("error", reject);
150
+ res.on("error", reject);
151
+ });
152
+ },
153
+ fileSha256: (p) => sha256Hex(readFileSync(p)),
154
+ runSudoInstaller: (pkgPath) => {
155
+ var _a;
156
+ return (_a = spawnSync("sudo", ["installer", "-pkg", pkgPath, "-target", "/"], { stdio: "inherit" }).status) != null ? _a : 1;
157
+ },
158
+ tmpdir: () => os.tmpdir(),
159
+ cleanup: (p) => {
160
+ try {
161
+ rmSync(p, { force: true });
162
+ } catch {
163
+ }
164
+ },
165
+ resolveNewNode: () => resolveNewNodeFrom((p) => {
166
+ try {
167
+ return String(execFileSync(p, ["--version"], { encoding: "utf8" })).trim();
168
+ } catch {
169
+ return null;
170
+ }
171
+ }),
172
+ reexec: (nodePath) => {
173
+ const child = spawnSync(nodePath, originalArgv.slice(1), { stdio: "inherit" });
174
+ process.exit(child.status !== null && child.status !== void 0 ? child.status : child.signal ? 130 : 1);
175
+ }
176
+ };
177
+ }
178
+ var DIST_BASE, NODE_CANDIDATES;
179
+ var init_preflight_upgrade = __esm({
180
+ "src/preflight-upgrade.cts"() {
181
+ "use strict";
182
+ DIST_BASE = "https://nodejs.org/dist/latest-v24.x";
183
+ NODE_CANDIDATES = [
184
+ "/usr/local/bin/node",
185
+ // nodejs.org pkg (stable across upgrades)
186
+ "/opt/homebrew/bin/node",
187
+ // brew symlink (Apple Silicon)
188
+ "/opt/homebrew/opt/node/bin/node"
189
+ ];
190
+ }
191
+ });
192
+
193
+ // src/preflight.cts
194
+ var preflight_exports = {};
195
+ __export(preflight_exports, {
196
+ effectiveVersion: () => effectiveVersion,
197
+ instructionsBlock: () => instructionsBlock,
198
+ meetsFloor: () => meetsFloor,
199
+ parseNodeMajor: () => parseNodeMajor,
200
+ runPreflight: () => runPreflight
201
+ });
202
+ module.exports = __toCommonJS(preflight_exports);
203
+ var NODE_FLOOR = 24;
204
+ var FLOOR_REASON = "requires >= 24 (node:sqlite stable API)";
205
+ function parseNodeMajor(version) {
206
+ const n = Number(String(version).split(".")[0]);
207
+ return Number.isFinite(n) && n > 0 ? n : 0;
208
+ }
209
+ function meetsFloor(version) {
210
+ return parseNodeMajor(version) >= NODE_FLOOR;
211
+ }
212
+ function effectiveVersion(real, faked) {
213
+ if (!faked) return real;
214
+ const f = parseNodeMajor(faked);
215
+ if (f >= 1 && f < parseNodeMajor(real)) return faked;
216
+ return real;
217
+ }
218
+ function instructionsBlock(platform, current) {
219
+ const lines = [
220
+ "[bridge] Node " + current + " is too old \u2014 SecureComs " + FLOOR_REASON + ".",
221
+ "",
222
+ "How to get Node 24 LTS:",
223
+ " 1. Official installer (recommended): https://nodejs.org \u2014 download the",
224
+ " Node 24 LTS installer for your platform and run it."
225
+ ];
226
+ if (platform === "darwin") {
227
+ lines.push(" 2. Homebrew: brew install node");
228
+ }
229
+ lines.push(" " + (platform === "darwin" ? "3" : "2") + ". nvm: nvm install 24 && nvm use 24");
230
+ lines.push(
231
+ "",
232
+ "Then re-run the install command. (Always re-run the install command after",
233
+ "upgrading Node \u2014 the background service pins the Node path it was",
234
+ "installed with.)"
235
+ );
236
+ return lines.join("\n") + "\n";
237
+ }
238
+ async function runPreflight(deps) {
239
+ if (meetsFloor(deps.version)) {
240
+ await deps.delegate();
241
+ return 0;
242
+ }
243
+ if (deps.upgrade) {
244
+ if (await deps.upgrade()) return 0;
245
+ }
246
+ deps.stderr(instructionsBlock(deps.platform, deps.version));
247
+ return 1;
248
+ }
249
+ if (require.main === module) {
250
+ const version = effectiveVersion(
251
+ process.versions.node,
252
+ process.env.SECURECOMS_PREFLIGHT_FAKE_NODE_VERSION
253
+ );
254
+ runPreflight({
255
+ version,
256
+ platform: process.platform,
257
+ isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
258
+ stderr: (s) => process.stderr.write(s),
259
+ // The modern half is only ever require()d here, AFTER the gate passed —
260
+ // old Node never parses its dynamic-import syntax.
261
+ delegate: () => require("./preflight-modern.cjs").passThrough(),
262
+ upgrade: () => {
263
+ const up = (init_preflight_upgrade(), __toCommonJS(preflight_upgrade_exports));
264
+ return up.guidedUpgrade(up.realUpgradeDeps(process.argv));
265
+ }
266
+ }).then((code) => {
267
+ if (code !== 0) process.exit(code);
268
+ });
269
+ }
270
+ // Annotate the CommonJS export names for ESM import in node:
271
+ 0 && (module.exports = {
272
+ effectiveVersion,
273
+ instructionsBlock,
274
+ meetsFloor,
275
+ parseNodeMajor,
276
+ runPreflight
277
+ });
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ export declare function parseNodeMajor(version: string): number;
3
+ export declare function meetsFloor(version: string): boolean;
4
+ export declare function effectiveVersion(real: string, faked: string | undefined): string;
5
+ export declare function instructionsBlock(platform: string, current: string): string;
6
+ export interface PreflightDeps {
7
+ version: string;
8
+ platform: string;
9
+ isTTY: boolean;
10
+ stderr: (s: string) => void;
11
+ delegate: () => Promise<unknown>;
12
+ upgrade: (() => Promise<boolean>) | null;
13
+ }
14
+ export declare function runPreflight(deps: PreflightDeps): Promise<number>;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "bin": {
8
- "agent-bridge": "dist/bin.js"
8
+ "agent-bridge": "dist/preflight.cjs"
9
9
  },
10
10
  "exports": {
11
11
  ".": {
@@ -17,13 +17,11 @@
17
17
  "dist",
18
18
  "README.md"
19
19
  ],
20
- "engines": {
21
- "node": ">=24"
22
- },
23
20
  "publishConfig": {
24
21
  "access": "public"
25
22
  },
26
23
  "scripts": {
24
+ "prepack": "npm run build",
27
25
  "build": "tsc && node esbuild.config.mjs",
28
26
  "test": "vitest run",
29
27
  "test:watch": "vitest"
@@ -1,49 +0,0 @@
1
- import { basename } from "node:path";
2
- /** Reduce any name to a safe basename inside the target dir. Strips path
3
- * separators and `..`; falls back when nothing usable remains. */
4
- export function sanitizeFilename(name, fallback) {
5
- const base = basename(name ?? "");
6
- const cleaned = base.replace(/[/\\]/g, "").replace(/^\.+$/, "");
7
- return cleaned.length > 0 ? cleaned : fallback;
8
- }
9
- const MIME_BY_EXT = {
10
- png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif",
11
- webp: "image/webp", svg: "image/svg+xml", pdf: "application/pdf",
12
- txt: "text/plain", md: "text/markdown", json: "application/json",
13
- csv: "text/csv", html: "text/html", mp4: "video/mp4", mp3: "audio/mpeg",
14
- zip: "application/zip", wav: "audio/wav",
15
- };
16
- export function mimeForName(name) {
17
- const dot = name.lastIndexOf(".");
18
- if (dot < 0 || dot === name.length - 1)
19
- return "application/octet-stream";
20
- return MIME_BY_EXT[name.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
21
- }
22
- export async function prepareAttachment(filePath, caption, channelId, deps) {
23
- const bytes = await deps.readFile(filePath);
24
- const name = basename(filePath);
25
- const { encryptedData, fileKey, fileNonce, digest } = deps.encryptFile(bytes);
26
- const blobId = await deps.uploadBlob(encryptedData, channelId);
27
- const body = {
28
- v: 1, type: "artifact_share", name, mime: mimeForName(name), size: bytes.length,
29
- blobId, fileKey: deps.bytesToBase64(fileKey), fileNonce: deps.bytesToBase64(fileNonce), digest,
30
- };
31
- if (caption)
32
- body.text = caption;
33
- return body;
34
- }
35
- export async function materializeAttachment(body, ctx, deps) {
36
- const ciphertext = await deps.downloadBlob(body.blobId);
37
- // Verify digest on ciphertext BEFORE decrypting — tampered blobs fail here.
38
- const sha256 = deps.computeFileDigest(ciphertext);
39
- if (sha256 !== body.digest) {
40
- throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
41
- }
42
- const plain = deps.decryptFile(ciphertext, deps.base64ToBytes(body.fileKey), deps.base64ToBytes(body.fileNonce));
43
- const safeName = sanitizeFilename(body.name, ctx.messageId);
44
- const dir = `${ctx.attachmentsDir}/${ctx.channelId}`;
45
- const path = `${dir}/${ctx.messageId}-${safeName}`;
46
- await deps.mkdirp(dir);
47
- await deps.writeFile(path, plain);
48
- return { filename: safeName, mime: body.mime, size: plain.length, path, sha256 };
49
- }
package/dist/auth.js DELETED
@@ -1,56 +0,0 @@
1
- import { randomBytes, timingSafeEqual } from "node:crypto";
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
- import { isLoopback } from "./config.js";
4
- export function resolveToken(opts) {
5
- if (opts.token)
6
- return opts.token;
7
- const f = opts.tokenFile;
8
- if (f && existsSync(f))
9
- return readFileSync(f, "utf8").trim();
10
- const tok = randomBytes(32).toString("base64url");
11
- if (f)
12
- writeFileSync(f, tok, { mode: 0o600 });
13
- return tok;
14
- }
15
- export function tokenMatches(provided, expected) {
16
- if (!provided)
17
- return false;
18
- const a = Buffer.from(provided);
19
- const b = Buffer.from(expected);
20
- if (a.length !== b.length)
21
- return false;
22
- return timingSafeEqual(a, b);
23
- }
24
- export function bearerFrom(header) {
25
- if (!header)
26
- return undefined;
27
- const m = /^Bearer (.+)$/i.exec(header.trim());
28
- return m ? m[1] : undefined;
29
- }
30
- function hostnameOf(hostHeader) {
31
- if (!hostHeader)
32
- return undefined;
33
- // Strip an optional :port (but keep [::1] intact).
34
- if (hostHeader.startsWith("["))
35
- return hostHeader.slice(0, hostHeader.indexOf("]") + 1);
36
- const i = hostHeader.lastIndexOf(":");
37
- return i > 0 ? hostHeader.slice(0, i) : hostHeader;
38
- }
39
- export function originHostAllowed(headers, allowOrigins) {
40
- // Host must be a loopback name (blocks the DNS-rebind target Host: evil.com).
41
- const host = hostnameOf(headers.host);
42
- if (host && !isLoopback(host) && !isLoopback(`[${host}]`))
43
- return false;
44
- // Origin: absent → allow (non-browser). Present → must be loopback or allow-listed.
45
- const origin = headers.origin;
46
- if (!origin)
47
- return true;
48
- if (allowOrigins.includes(origin))
49
- return true;
50
- try {
51
- return isLoopback(new URL(origin).hostname);
52
- }
53
- catch {
54
- return false;
55
- }
56
- }