@tuent/sentinel 0.1.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.
@@ -0,0 +1,62 @@
1
+ // src/setup/pidManager.ts
2
+ import { readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
3
+ import { execSync } from "child_process";
4
+ import { join } from "path";
5
+ var PID_FILENAME = "sentinel-gateway.pid";
6
+ function readPidFile(home) {
7
+ try {
8
+ const raw = readFileSync(join(home, ".dahlia", PID_FILENAME), "utf-8").trim();
9
+ const pid = parseInt(raw, 10);
10
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function writePidFile(home, pid) {
16
+ const pidPath = join(home, ".dahlia", PID_FILENAME);
17
+ const tmpPath = pidPath + `.tmp.${process.pid}`;
18
+ writeFileSync(tmpPath, String(pid) + "\n", { mode: 384 });
19
+ renameSync(tmpPath, pidPath);
20
+ }
21
+ function removePidFile(home) {
22
+ try {
23
+ unlinkSync(join(home, ".dahlia", PID_FILENAME));
24
+ } catch {
25
+ }
26
+ }
27
+ function verifyPidIsGateway(pid) {
28
+ if (process.platform === "win32") {
29
+ throw new Error(
30
+ "PID validity check via 'ps' is not supported on Windows. macOS and Linux only for Sprint 5."
31
+ );
32
+ }
33
+ try {
34
+ const output = execSync(`ps -p ${pid} -o comm=`, {
35
+ stdio: "pipe",
36
+ encoding: "utf-8"
37
+ }).trim();
38
+ return output.includes("node");
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function acquireGatewayLock(home) {
44
+ const existingPid = readPidFile(home);
45
+ if (existingPid === null) {
46
+ return { reused: false, pid: null };
47
+ }
48
+ if (verifyPidIsGateway(existingPid)) {
49
+ return { reused: true, pid: existingPid };
50
+ }
51
+ removePidFile(home);
52
+ return { reused: false, pid: null };
53
+ }
54
+
55
+ export {
56
+ readPidFile,
57
+ writePidFile,
58
+ removePidFile,
59
+ verifyPidIsGateway,
60
+ acquireGatewayLock
61
+ };
62
+ //# sourceMappingURL=chunk-CUJKNIKT.js.map
@@ -0,0 +1,20 @@
1
+ // src/setup/policyDiscovery.ts
2
+ import { existsSync } from "fs";
3
+ import { resolve, join, dirname } from "path";
4
+ function discoverPolicy(startDir, home) {
5
+ let dir = resolve(startDir);
6
+ const homeAbs = resolve(home);
7
+ while (true) {
8
+ const candidate = join(dir, ".sentinel.yaml");
9
+ if (existsSync(candidate)) return candidate;
10
+ if (dir === homeAbs) return null;
11
+ const parent = dirname(dir);
12
+ if (parent === dir) return null;
13
+ dir = parent;
14
+ }
15
+ }
16
+
17
+ export {
18
+ discoverPolicy
19
+ };
20
+ //# sourceMappingURL=chunk-FMZWHT4M.js.map
@@ -0,0 +1,95 @@
1
+ // src/auditTrailKeys.ts
2
+ import {
3
+ createPublicKey,
4
+ generateKeyPairSync,
5
+ sign as cryptoSign,
6
+ verify as cryptoVerify
7
+ } from "crypto";
8
+ var ED25519_SPKI_HEADER = Buffer.from("302a300506032b6570032100", "hex");
9
+ function generateKeyPair() {
10
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
11
+ return { privateKey, publicKey };
12
+ }
13
+ async function saveKeyPair(privateKey, publicKey, keyDir) {
14
+ const { writeFile, mkdir, chmod } = await import("fs/promises");
15
+ await mkdir(keyDir, { recursive: true });
16
+ const privPath = `${keyDir}/signing.key`;
17
+ const pubPath = `${keyDir}/signing.pub`;
18
+ const privPem = privateKey.export({ type: "pkcs8", format: "pem" });
19
+ const pubPem = publicKey.export({ type: "spki", format: "pem" });
20
+ await writeFile(privPath, privPem, { mode: 384 });
21
+ await writeFile(pubPath, pubPem, { mode: 420 });
22
+ if (process.platform !== "win32") {
23
+ await chmod(privPath, 384);
24
+ }
25
+ }
26
+ async function loadKeyPair(keyDir) {
27
+ const { readFile } = await import("fs/promises");
28
+ const { createPrivateKey, createPublicKey: createPub } = await import("crypto");
29
+ const privPath = `${keyDir}/signing.key`;
30
+ const pubPath = `${keyDir}/signing.pub`;
31
+ let privPem;
32
+ let pubPem;
33
+ try {
34
+ privPem = await readFile(privPath, "utf-8");
35
+ pubPem = await readFile(pubPath, "utf-8");
36
+ } catch (err) {
37
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
38
+ return null;
39
+ }
40
+ throw new Error(`Failed to load signing keys from ${keyDir}: ${err}`, { cause: err });
41
+ }
42
+ try {
43
+ const privateKey = createPrivateKey({ key: privPem, format: "pem", type: "pkcs8" });
44
+ const publicKey = createPub({ key: pubPem, format: "pem", type: "spki" });
45
+ return { privateKey, publicKey };
46
+ } catch (err) {
47
+ throw new Error(`Corrupt signing keys in ${keyDir}: ${err}`, { cause: err });
48
+ }
49
+ }
50
+ async function getOrCreateKeyPair(keyDir) {
51
+ const existing = await loadKeyPair(keyDir);
52
+ if (existing) return existing;
53
+ const keyPair = generateKeyPair();
54
+ await saveKeyPair(keyPair.privateKey, keyPair.publicKey, keyDir);
55
+ return keyPair;
56
+ }
57
+ function publicKeyToBase64(publicKey) {
58
+ const spki = publicKey.export({ type: "spki", format: "der" });
59
+ const raw = spki.subarray(ED25519_SPKI_HEADER.length);
60
+ return raw.toString("base64");
61
+ }
62
+ function publicKeyFromBase64(b64) {
63
+ const raw = Buffer.from(b64, "base64");
64
+ const spki = Buffer.concat([ED25519_SPKI_HEADER, raw]);
65
+ return createPublicKey({ key: spki, format: "der", type: "spki" });
66
+ }
67
+ function signPayload(payload, privateKey) {
68
+ const signature = cryptoSign(null, Buffer.from(payload, "utf-8"), privateKey);
69
+ return signature.toString("base64");
70
+ }
71
+ function verifySignature(payload, signature, publicKeyB64) {
72
+ const publicKey = publicKeyFromBase64(publicKeyB64);
73
+ const sigBuf = Buffer.from(signature, "base64");
74
+ return cryptoVerify(null, Buffer.from(payload, "utf-8"), publicKey, sigBuf);
75
+ }
76
+ function reconstructSigningPayload(rawLine) {
77
+ const parsed = JSON.parse(rawLine);
78
+ delete parsed.signature;
79
+ delete parsed.signerPublicKey;
80
+ delete parsed.hash;
81
+ return JSON.stringify(parsed);
82
+ }
83
+
84
+ export {
85
+ generateKeyPair,
86
+ saveKeyPair,
87
+ loadKeyPair,
88
+ getOrCreateKeyPair,
89
+ publicKeyToBase64,
90
+ publicKeyFromBase64,
91
+ signPayload,
92
+ verifySignature,
93
+ reconstructSigningPayload
94
+ };
95
+ //# sourceMappingURL=chunk-NUXSUSYY.js.map
@@ -0,0 +1,238 @@
1
+ // src/adapters/logAdapter.ts
2
+ import { homedir } from "os";
3
+ function resolveHome(filePath) {
4
+ if (filePath.startsWith("~/")) {
5
+ return homedir() + filePath.slice(1);
6
+ }
7
+ return filePath;
8
+ }
9
+ var LogAdapter = class {
10
+ agentId;
11
+ logPath;
12
+ format;
13
+ fieldMapping;
14
+ pollIntervalMs;
15
+ lastReadPosition = 0;
16
+ pendingFragment = "";
17
+ running = false;
18
+ polling = false;
19
+ pollTimer = null;
20
+ onEvent = null;
21
+ readExisting;
22
+ stateDir;
23
+ constructor(agentId, logPath, options) {
24
+ this.agentId = agentId;
25
+ this.logPath = resolveHome(logPath);
26
+ this.format = options?.format ?? "json-lines";
27
+ this.fieldMapping = options?.fieldMapping;
28
+ this.pollIntervalMs = options?.pollIntervalMs ?? 2e3;
29
+ this.readExisting = options?.readExisting ?? false;
30
+ this.stateDir = options?.stateDir;
31
+ }
32
+ async start(onEvent) {
33
+ if (this.running) return;
34
+ this.onEvent = onEvent;
35
+ this.running = true;
36
+ const savedPosition = await this.loadCursor();
37
+ if (savedPosition !== null) {
38
+ this.lastReadPosition = savedPosition;
39
+ } else if (!this.readExisting) {
40
+ const { stat } = await import("fs/promises");
41
+ try {
42
+ const info = await stat(this.logPath);
43
+ this.lastReadPosition = info.size;
44
+ } catch {
45
+ this.lastReadPosition = 0;
46
+ }
47
+ } else {
48
+ this.lastReadPosition = 0;
49
+ }
50
+ this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
51
+ console.log(`LogAdapter watching ${this.logPath} for agent ${this.agentId}`);
52
+ }
53
+ async stop() {
54
+ if (this.pollTimer !== null) {
55
+ clearInterval(this.pollTimer);
56
+ this.pollTimer = null;
57
+ }
58
+ await this.saveCursor();
59
+ this.running = false;
60
+ this.onEvent = null;
61
+ console.log(`LogAdapter stopped for agent ${this.agentId}`);
62
+ }
63
+ async poll() {
64
+ if (!this.running || this.polling) return;
65
+ this.polling = true;
66
+ try {
67
+ const lines = await this.readNewLines();
68
+ for (const line of lines) {
69
+ if (!this.running) break;
70
+ const event = this.parseLine(line);
71
+ if (event) {
72
+ try {
73
+ this.onEvent?.(event);
74
+ } catch (err) {
75
+ console.warn(`Error in onEvent callback: ${err}`);
76
+ }
77
+ }
78
+ }
79
+ } catch (err) {
80
+ console.warn(`LogAdapter poll error: ${err}`);
81
+ } finally {
82
+ this.polling = false;
83
+ }
84
+ }
85
+ async readNewLines() {
86
+ const { stat, open } = await import("fs/promises");
87
+ let info;
88
+ try {
89
+ info = await stat(this.logPath);
90
+ } catch {
91
+ return [];
92
+ }
93
+ const size = info.size;
94
+ if (size < this.lastReadPosition) {
95
+ this.lastReadPosition = 0;
96
+ this.pendingFragment = "";
97
+ }
98
+ if (size === this.lastReadPosition) {
99
+ return [];
100
+ }
101
+ const MAX_READ_CHUNK = 5 * 1024 * 1024;
102
+ const bytesToRead = Math.min(size - this.lastReadPosition, MAX_READ_CHUNK);
103
+ const buffer = Buffer.alloc(bytesToRead);
104
+ let handle;
105
+ let bytesActuallyRead;
106
+ try {
107
+ handle = await open(this.logPath, "r");
108
+ const result = await handle.read(buffer, 0, bytesToRead, this.lastReadPosition);
109
+ bytesActuallyRead = result.bytesRead;
110
+ } catch {
111
+ return [];
112
+ } finally {
113
+ await handle?.close();
114
+ }
115
+ this.lastReadPosition += bytesActuallyRead;
116
+ const raw = this.pendingFragment + buffer.toString("utf-8", 0, bytesActuallyRead);
117
+ const segments = raw.split("\n");
118
+ this.pendingFragment = segments.pop() ?? "";
119
+ return segments.filter((l) => l.length > 0);
120
+ }
121
+ parseLine(line) {
122
+ if (this.format === "csv") {
123
+ return this.parseCsvLine(line);
124
+ }
125
+ return this.parseJsonLine(line);
126
+ }
127
+ parseJsonLine(line) {
128
+ let obj;
129
+ try {
130
+ obj = JSON.parse(line);
131
+ } catch {
132
+ console.warn(`Failed to parse JSON line: ${line}`);
133
+ return null;
134
+ }
135
+ if (this.fieldMapping) {
136
+ const remapped = { ...obj };
137
+ for (const [eventKey, sourceKey] of Object.entries(this.fieldMapping)) {
138
+ if (sourceKey in obj) {
139
+ remapped[eventKey] = obj[sourceKey];
140
+ }
141
+ }
142
+ obj = remapped;
143
+ }
144
+ obj.agentId = this.agentId;
145
+ if (!obj.agentName) obj.agentName = this.agentId;
146
+ if (!obj.agentRole) obj.agentRole = "unknown";
147
+ if (typeof obj.action !== "string" || typeof obj.timestamp !== "string") {
148
+ console.warn(`Missing required fields in log line: ${line}`);
149
+ return null;
150
+ }
151
+ let targets;
152
+ let primaryTarget;
153
+ if (Array.isArray(obj.targets) && obj.targets.length > 0) {
154
+ targets = obj.targets.map(String);
155
+ primaryTarget = targets[0];
156
+ } else if (typeof obj.target === "string") {
157
+ targets = [obj.target];
158
+ primaryTarget = obj.target;
159
+ } else {
160
+ console.warn(`Missing required fields in log line: ${line}`);
161
+ return null;
162
+ }
163
+ return {
164
+ ...obj,
165
+ agentId: String(obj.agentId),
166
+ agentName: String(obj.agentName),
167
+ agentRole: String(obj.agentRole),
168
+ action: obj.action,
169
+ targets,
170
+ primaryTarget,
171
+ schemaVersion: 2,
172
+ timestamp: obj.timestamp
173
+ };
174
+ }
175
+ get cursorPath() {
176
+ if (!this.stateDir) return null;
177
+ return `${this.stateDir}/log-cursor.json`;
178
+ }
179
+ async loadCursor() {
180
+ const path = this.cursorPath;
181
+ if (!path) return null;
182
+ try {
183
+ const { readFile } = await import("fs/promises");
184
+ const raw = await readFile(path, "utf-8");
185
+ const data = JSON.parse(raw);
186
+ if (data.logPath === this.logPath) return data.position;
187
+ } catch {
188
+ }
189
+ return null;
190
+ }
191
+ async saveCursor() {
192
+ const path = this.cursorPath;
193
+ if (!path) return;
194
+ try {
195
+ const { writeFile, mkdir } = await import("fs/promises");
196
+ await mkdir(this.stateDir, { recursive: true });
197
+ await writeFile(
198
+ path,
199
+ JSON.stringify({ position: this.lastReadPosition, logPath: this.logPath }) + "\n"
200
+ );
201
+ } catch (err) {
202
+ console.warn(`Failed to save log cursor: ${err}`);
203
+ }
204
+ }
205
+ parseCsvLine(line) {
206
+ const parts = line.split(",");
207
+ if (parts.length < 3) {
208
+ console.warn(`CSV line has too few columns: ${line}`);
209
+ return null;
210
+ }
211
+ const [timestamp, action, target, ...rest] = parts;
212
+ const metadataRaw = rest.join(",").trim();
213
+ let metadata;
214
+ if (metadataRaw) {
215
+ try {
216
+ metadata = JSON.parse(metadataRaw);
217
+ } catch {
218
+ }
219
+ }
220
+ const trimmedTarget = target.trim();
221
+ return {
222
+ agentId: this.agentId,
223
+ agentName: this.agentId,
224
+ agentRole: "unknown",
225
+ action: action.trim(),
226
+ targets: [trimmedTarget],
227
+ primaryTarget: trimmedTarget,
228
+ schemaVersion: 2,
229
+ timestamp: timestamp.trim(),
230
+ metadata
231
+ };
232
+ }
233
+ };
234
+
235
+ export {
236
+ LogAdapter
237
+ };
238
+ //# sourceMappingURL=chunk-PDWWRZXF.js.map