@hasna/machines 0.0.32 → 0.0.34
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/README.md +48 -47
- package/dist/cli/index.js +154 -1323
- package/dist/commands/heal-daemon.d.ts.map +1 -1
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/consumer.js +18 -3
- package/dist/index.js +50 -547
- package/dist/mcp/index.js +103 -604
- package/dist/topology.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/mcp/index.js
CHANGED
|
@@ -40,527 +40,8 @@ function getPackageVersion() {
|
|
|
40
40
|
import { createServer } from "http";
|
|
41
41
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
42
42
|
|
|
43
|
-
// node_modules/@hasna/events/dist/index.js
|
|
44
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
45
|
-
import { existsSync as existsSync2 } from "fs";
|
|
46
|
-
import { homedir } from "os";
|
|
47
|
-
import { join as join2 } from "path";
|
|
48
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
49
|
-
import { randomUUID } from "crypto";
|
|
50
|
-
import { spawn } from "child_process";
|
|
51
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
52
|
-
function getPathValue(input, path) {
|
|
53
|
-
return path.split(".").reduce((value, part) => {
|
|
54
|
-
if (value && typeof value === "object" && part in value) {
|
|
55
|
-
return value[part];
|
|
56
|
-
}
|
|
57
|
-
return;
|
|
58
|
-
}, input);
|
|
59
|
-
}
|
|
60
|
-
function wildcardToRegExp(pattern) {
|
|
61
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
62
|
-
return new RegExp(`^${escaped}$`);
|
|
63
|
-
}
|
|
64
|
-
function matchString(value, matcher) {
|
|
65
|
-
if (matcher === undefined)
|
|
66
|
-
return true;
|
|
67
|
-
if (value === undefined)
|
|
68
|
-
return false;
|
|
69
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
70
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
71
|
-
}
|
|
72
|
-
function matchRecord(input, matcher) {
|
|
73
|
-
if (!matcher)
|
|
74
|
-
return true;
|
|
75
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
76
|
-
const actual = getPathValue(input, path);
|
|
77
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
78
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
79
|
-
}
|
|
80
|
-
return actual === expected;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
function eventMatchesFilter(event, filter) {
|
|
84
|
-
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
85
|
-
}
|
|
86
|
-
function channelMatchesEvent(channel, event) {
|
|
87
|
-
if (!channel.enabled)
|
|
88
|
-
return false;
|
|
89
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
90
|
-
return true;
|
|
91
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
92
|
-
}
|
|
93
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
94
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
95
|
-
function getEventsDataDir(override) {
|
|
96
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join2(homedir(), ".hasna", "events");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
class JsonEventsStore {
|
|
100
|
-
dataDir;
|
|
101
|
-
channelsPath;
|
|
102
|
-
eventsPath;
|
|
103
|
-
deliveriesPath;
|
|
104
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
105
|
-
this.dataDir = dataDir;
|
|
106
|
-
this.channelsPath = join2(dataDir, "channels.json");
|
|
107
|
-
this.eventsPath = join2(dataDir, "events.json");
|
|
108
|
-
this.deliveriesPath = join2(dataDir, "deliveries.json");
|
|
109
|
-
}
|
|
110
|
-
async init() {
|
|
111
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
112
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
113
|
-
return;
|
|
114
|
-
});
|
|
115
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
116
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
117
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
118
|
-
}
|
|
119
|
-
async addChannel(channel) {
|
|
120
|
-
await this.init();
|
|
121
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
122
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
123
|
-
if (index >= 0) {
|
|
124
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
125
|
-
} else {
|
|
126
|
-
channels.push(channel);
|
|
127
|
-
}
|
|
128
|
-
await this.writeJson(this.channelsPath, channels);
|
|
129
|
-
return index >= 0 ? channels[index] : channel;
|
|
130
|
-
}
|
|
131
|
-
async listChannels() {
|
|
132
|
-
await this.init();
|
|
133
|
-
return this.readJson(this.channelsPath, []);
|
|
134
|
-
}
|
|
135
|
-
async getChannel(id) {
|
|
136
|
-
const channels = await this.listChannels();
|
|
137
|
-
return channels.find((channel) => channel.id === id);
|
|
138
|
-
}
|
|
139
|
-
async removeChannel(id) {
|
|
140
|
-
await this.init();
|
|
141
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
142
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
143
|
-
await this.writeJson(this.channelsPath, next);
|
|
144
|
-
return next.length !== channels.length;
|
|
145
|
-
}
|
|
146
|
-
async appendEvent(event) {
|
|
147
|
-
await this.init();
|
|
148
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
149
|
-
events.push(event);
|
|
150
|
-
await this.writeJson(this.eventsPath, events);
|
|
151
|
-
return event;
|
|
152
|
-
}
|
|
153
|
-
async listEvents() {
|
|
154
|
-
await this.init();
|
|
155
|
-
return this.readJson(this.eventsPath, []);
|
|
156
|
-
}
|
|
157
|
-
async findEventByIdentity(identity) {
|
|
158
|
-
const events = await this.listEvents();
|
|
159
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
160
|
-
}
|
|
161
|
-
async appendDelivery(result) {
|
|
162
|
-
await this.init();
|
|
163
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
164
|
-
deliveries.push(result);
|
|
165
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
async listDeliveries() {
|
|
169
|
-
await this.init();
|
|
170
|
-
return this.readJson(this.deliveriesPath, []);
|
|
171
|
-
}
|
|
172
|
-
async exportData() {
|
|
173
|
-
return {
|
|
174
|
-
channels: await this.listChannels(),
|
|
175
|
-
events: await this.listEvents(),
|
|
176
|
-
deliveries: await this.listDeliveries()
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
async ensureArrayFile(path) {
|
|
180
|
-
if (!existsSync2(path)) {
|
|
181
|
-
await writeFile(path, `[]
|
|
182
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
183
|
-
}
|
|
184
|
-
await chmod(path, 384).catch(() => {
|
|
185
|
-
return;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
async readJson(path, fallback) {
|
|
189
|
-
try {
|
|
190
|
-
const raw = await readFile(path, "utf-8");
|
|
191
|
-
if (!raw.trim())
|
|
192
|
-
return fallback;
|
|
193
|
-
return JSON.parse(raw);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
if (error.code === "ENOENT")
|
|
196
|
-
return fallback;
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
async writeJson(path, value) {
|
|
201
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
202
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
203
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
204
|
-
await rename(tempPath, path);
|
|
205
|
-
await chmod(path, 384).catch(() => {
|
|
206
|
-
return;
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
211
|
-
function buildSignatureBase(timestamp, body) {
|
|
212
|
-
return `${timestamp}.${body}`;
|
|
213
|
-
}
|
|
214
|
-
function signPayload(secret, timestamp, body) {
|
|
215
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
216
|
-
return `sha256=${digest}`;
|
|
217
|
-
}
|
|
218
|
-
function now() {
|
|
219
|
-
return new Date().toISOString();
|
|
220
|
-
}
|
|
221
|
-
function truncate(value, max = 4096) {
|
|
222
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
223
|
-
}
|
|
224
|
-
function buildWebhookRequest(event, channel) {
|
|
225
|
-
if (!channel.webhook)
|
|
226
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
227
|
-
const body = JSON.stringify(event);
|
|
228
|
-
const timestamp = event.time;
|
|
229
|
-
const headers = {
|
|
230
|
-
"Content-Type": "application/json",
|
|
231
|
-
"User-Agent": "@hasna/events",
|
|
232
|
-
"X-Hasna-Event-Id": event.id,
|
|
233
|
-
"X-Hasna-Event-Type": event.type,
|
|
234
|
-
"X-Hasna-Timestamp": timestamp,
|
|
235
|
-
...channel.webhook.headers
|
|
236
|
-
};
|
|
237
|
-
if (channel.webhook.secret) {
|
|
238
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
239
|
-
}
|
|
240
|
-
return { body, headers };
|
|
241
|
-
}
|
|
242
|
-
async function dispatchWebhook(event, channel, options = {}) {
|
|
243
|
-
if (!channel.webhook)
|
|
244
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
245
|
-
const startedAt = now();
|
|
246
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
247
|
-
const controller = new AbortController;
|
|
248
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
249
|
-
try {
|
|
250
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
251
|
-
method: "POST",
|
|
252
|
-
headers,
|
|
253
|
-
body,
|
|
254
|
-
signal: controller.signal
|
|
255
|
-
});
|
|
256
|
-
const responseBody = truncate(await response.text());
|
|
257
|
-
return {
|
|
258
|
-
attempt: 1,
|
|
259
|
-
status: response.ok ? "success" : "failed",
|
|
260
|
-
startedAt,
|
|
261
|
-
completedAt: now(),
|
|
262
|
-
responseStatus: response.status,
|
|
263
|
-
responseBody,
|
|
264
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
265
|
-
};
|
|
266
|
-
} catch (error) {
|
|
267
|
-
return {
|
|
268
|
-
attempt: 1,
|
|
269
|
-
status: "failed",
|
|
270
|
-
startedAt,
|
|
271
|
-
completedAt: now(),
|
|
272
|
-
error: error instanceof Error ? error.message : String(error)
|
|
273
|
-
};
|
|
274
|
-
} finally {
|
|
275
|
-
clearTimeout(timeout);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
async function dispatchCommand(event, channel) {
|
|
279
|
-
if (!channel.command)
|
|
280
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
281
|
-
const startedAt = now();
|
|
282
|
-
const eventJson = JSON.stringify(event);
|
|
283
|
-
const env = {
|
|
284
|
-
...process.env,
|
|
285
|
-
...channel.command.env,
|
|
286
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
287
|
-
HASNA_EVENT_ID: event.id,
|
|
288
|
-
HASNA_EVENT_TYPE: event.type,
|
|
289
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
290
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
291
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
292
|
-
HASNA_EVENT_TIME: event.time,
|
|
293
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
294
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
295
|
-
HASNA_EVENT_JSON: eventJson
|
|
296
|
-
};
|
|
297
|
-
return new Promise((resolve) => {
|
|
298
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
299
|
-
cwd: channel.command.cwd,
|
|
300
|
-
env,
|
|
301
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
302
|
-
});
|
|
303
|
-
let stdout = "";
|
|
304
|
-
let stderr = "";
|
|
305
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
306
|
-
child.stdin.end(eventJson);
|
|
307
|
-
child.stdout.on("data", (chunk) => {
|
|
308
|
-
stdout += chunk.toString();
|
|
309
|
-
});
|
|
310
|
-
child.stderr.on("data", (chunk) => {
|
|
311
|
-
stderr += chunk.toString();
|
|
312
|
-
});
|
|
313
|
-
child.on("error", (error) => {
|
|
314
|
-
clearTimeout(timeout);
|
|
315
|
-
resolve({
|
|
316
|
-
attempt: 1,
|
|
317
|
-
status: "failed",
|
|
318
|
-
startedAt,
|
|
319
|
-
completedAt: now(),
|
|
320
|
-
stdout: truncate(stdout),
|
|
321
|
-
stderr: truncate(stderr),
|
|
322
|
-
error: error.message
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
child.on("close", (code, signal) => {
|
|
326
|
-
clearTimeout(timeout);
|
|
327
|
-
const success = code === 0;
|
|
328
|
-
resolve({
|
|
329
|
-
attempt: 1,
|
|
330
|
-
status: success ? "success" : "failed",
|
|
331
|
-
startedAt,
|
|
332
|
-
completedAt: now(),
|
|
333
|
-
stdout: truncate(stdout),
|
|
334
|
-
stderr: truncate(stderr),
|
|
335
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
async function dispatchChannel(event, channel, options = {}) {
|
|
341
|
-
if (channel.transport === "webhook")
|
|
342
|
-
return dispatchWebhook(event, channel, options);
|
|
343
|
-
if (channel.transport === "command")
|
|
344
|
-
return dispatchCommand(event, channel);
|
|
345
|
-
return {
|
|
346
|
-
attempt: 1,
|
|
347
|
-
status: "skipped",
|
|
348
|
-
startedAt: now(),
|
|
349
|
-
completedAt: now(),
|
|
350
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
354
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
355
|
-
return {
|
|
356
|
-
id: randomUUID(),
|
|
357
|
-
eventId: event.id,
|
|
358
|
-
channelId: channel.id,
|
|
359
|
-
transport: channel.transport,
|
|
360
|
-
status,
|
|
361
|
-
attempts,
|
|
362
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
363
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
function createEvent(input) {
|
|
367
|
-
return {
|
|
368
|
-
id: input.id ?? randomUUID2(),
|
|
369
|
-
source: input.source,
|
|
370
|
-
type: input.type,
|
|
371
|
-
time: normalizeTime(input.time),
|
|
372
|
-
subject: input.subject,
|
|
373
|
-
severity: input.severity ?? "info",
|
|
374
|
-
data: input.data ?? {},
|
|
375
|
-
message: input.message,
|
|
376
|
-
dedupeKey: input.dedupeKey,
|
|
377
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
378
|
-
metadata: input.metadata ?? {}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
class EventsClient {
|
|
383
|
-
store;
|
|
384
|
-
redactors;
|
|
385
|
-
transportOptions;
|
|
386
|
-
constructor(options = {}) {
|
|
387
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
388
|
-
this.redactors = options.redactors ?? [];
|
|
389
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
390
|
-
}
|
|
391
|
-
async addChannel(input) {
|
|
392
|
-
const timestamp = new Date().toISOString();
|
|
393
|
-
return this.store.addChannel({
|
|
394
|
-
...input,
|
|
395
|
-
createdAt: input.createdAt ?? timestamp,
|
|
396
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
async listChannels() {
|
|
400
|
-
return this.store.listChannels();
|
|
401
|
-
}
|
|
402
|
-
async removeChannel(id) {
|
|
403
|
-
return this.store.removeChannel(id);
|
|
404
|
-
}
|
|
405
|
-
async emit(input, options = {}) {
|
|
406
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
407
|
-
if (options.dedupe !== false) {
|
|
408
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
409
|
-
if (existing) {
|
|
410
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
await this.store.appendEvent(event);
|
|
414
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
415
|
-
return { event, deliveries, deduped: false };
|
|
416
|
-
}
|
|
417
|
-
async listEvents() {
|
|
418
|
-
return this.store.listEvents();
|
|
419
|
-
}
|
|
420
|
-
async listDeliveries() {
|
|
421
|
-
return this.store.listDeliveries();
|
|
422
|
-
}
|
|
423
|
-
async deliver(event) {
|
|
424
|
-
const channels = await this.store.listChannels();
|
|
425
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
426
|
-
const deliveries = [];
|
|
427
|
-
for (const channel of selected) {
|
|
428
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
429
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
430
|
-
await this.store.appendDelivery(result);
|
|
431
|
-
deliveries.push(result);
|
|
432
|
-
}
|
|
433
|
-
return deliveries;
|
|
434
|
-
}
|
|
435
|
-
async testChannel(id, input = {}) {
|
|
436
|
-
const channel = await this.store.getChannel(id);
|
|
437
|
-
if (!channel)
|
|
438
|
-
throw new Error(`Channel not found: ${id}`);
|
|
439
|
-
const event = createEvent({
|
|
440
|
-
source: input.source ?? "hasna.events",
|
|
441
|
-
type: input.type ?? "events.test",
|
|
442
|
-
subject: input.subject ?? id,
|
|
443
|
-
severity: input.severity ?? "info",
|
|
444
|
-
data: input.data ?? { test: true },
|
|
445
|
-
message: input.message ?? "Hasna events test delivery",
|
|
446
|
-
dedupeKey: input.dedupeKey,
|
|
447
|
-
schemaVersion: input.schemaVersion,
|
|
448
|
-
metadata: input.metadata,
|
|
449
|
-
time: input.time,
|
|
450
|
-
id: input.id
|
|
451
|
-
});
|
|
452
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
453
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
454
|
-
await this.store.appendDelivery(result);
|
|
455
|
-
return result;
|
|
456
|
-
}
|
|
457
|
-
async replay(options = {}) {
|
|
458
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
459
|
-
if (options.eventId && event.id !== options.eventId)
|
|
460
|
-
return false;
|
|
461
|
-
if (options.source && event.source !== options.source)
|
|
462
|
-
return false;
|
|
463
|
-
if (options.type && event.type !== options.type)
|
|
464
|
-
return false;
|
|
465
|
-
return true;
|
|
466
|
-
});
|
|
467
|
-
if (options.dryRun)
|
|
468
|
-
return { events, deliveries: [] };
|
|
469
|
-
const deliveries = [];
|
|
470
|
-
for (const event of events) {
|
|
471
|
-
deliveries.push(...await this.deliver(event));
|
|
472
|
-
}
|
|
473
|
-
return { events, deliveries };
|
|
474
|
-
}
|
|
475
|
-
async applyRedaction(event, channel) {
|
|
476
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
477
|
-
for (const redactor of this.redactors) {
|
|
478
|
-
next = await redactor(next, channel);
|
|
479
|
-
}
|
|
480
|
-
return next;
|
|
481
|
-
}
|
|
482
|
-
async deliverWithRetry(event, channel) {
|
|
483
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
484
|
-
const attempts = [];
|
|
485
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
486
|
-
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
487
|
-
attempt.attempt = index + 1;
|
|
488
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
489
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
490
|
-
}
|
|
491
|
-
attempts.push(attempt);
|
|
492
|
-
if (attempt.status !== "failed")
|
|
493
|
-
break;
|
|
494
|
-
if (attempt.nextBackoffMs)
|
|
495
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
496
|
-
}
|
|
497
|
-
return createDeliveryResult(event, channel, attempts);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
501
|
-
if (paths.length === 0)
|
|
502
|
-
return event;
|
|
503
|
-
const copy = structuredClone(event);
|
|
504
|
-
for (const path of paths) {
|
|
505
|
-
setPath(copy, path, replacement);
|
|
506
|
-
}
|
|
507
|
-
return copy;
|
|
508
|
-
}
|
|
509
|
-
function sanitizeChannelForOutput(channel) {
|
|
510
|
-
const copy = structuredClone(channel);
|
|
511
|
-
if (copy.webhook?.secret)
|
|
512
|
-
copy.webhook.secret = "[REDACTED]";
|
|
513
|
-
if (copy.command?.env) {
|
|
514
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
515
|
-
}
|
|
516
|
-
return copy;
|
|
517
|
-
}
|
|
518
|
-
function sanitizeChannelsForOutput(channels) {
|
|
519
|
-
return channels.map(sanitizeChannelForOutput);
|
|
520
|
-
}
|
|
521
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
522
|
-
return redactValue(event, replacement);
|
|
523
|
-
}
|
|
524
|
-
function shouldRedactKey(key) {
|
|
525
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
526
|
-
}
|
|
527
|
-
function redactValue(value, replacement) {
|
|
528
|
-
if (Array.isArray(value))
|
|
529
|
-
return value.map((item) => redactValue(item, replacement));
|
|
530
|
-
if (!value || typeof value !== "object")
|
|
531
|
-
return value;
|
|
532
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
533
|
-
key,
|
|
534
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
535
|
-
]));
|
|
536
|
-
}
|
|
537
|
-
function setPath(input, path, replacement) {
|
|
538
|
-
const parts = path.split(".");
|
|
539
|
-
let cursor = input;
|
|
540
|
-
for (const part of parts.slice(0, -1)) {
|
|
541
|
-
const next = cursor[part];
|
|
542
|
-
if (!next || typeof next !== "object")
|
|
543
|
-
return;
|
|
544
|
-
cursor = next;
|
|
545
|
-
}
|
|
546
|
-
const last = parts.at(-1);
|
|
547
|
-
if (last && last in cursor)
|
|
548
|
-
cursor[last] = replacement;
|
|
549
|
-
}
|
|
550
|
-
function normalizeTime(value) {
|
|
551
|
-
if (!value)
|
|
552
|
-
return new Date().toISOString();
|
|
553
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
554
|
-
}
|
|
555
|
-
function normalizeRetryPolicy(policy) {
|
|
556
|
-
return {
|
|
557
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
558
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
559
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
43
|
// src/mcp/server.ts
|
|
44
|
+
import { EventsClient as EventsClient2, sanitizeChannelForOutput, sanitizeChannelsForOutput as sanitizeChannelsForOutput2 } from "@hasna/events";
|
|
564
45
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
565
46
|
|
|
566
47
|
// node_modules/zod/v3/external.js
|
|
@@ -4537,8 +4018,8 @@ var coerce = {
|
|
|
4537
4018
|
};
|
|
4538
4019
|
var NEVER = INVALID;
|
|
4539
4020
|
// src/commands/backup.ts
|
|
4540
|
-
import { homedir
|
|
4541
|
-
import { join as
|
|
4021
|
+
import { homedir, hostname } from "os";
|
|
4022
|
+
import { join as join2 } from "path";
|
|
4542
4023
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
4543
4024
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
4544
4025
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -4586,16 +4067,16 @@ function resolveBackupTarget(options = {}) {
|
|
|
4586
4067
|
};
|
|
4587
4068
|
}
|
|
4588
4069
|
function defaultBackupSources() {
|
|
4589
|
-
const home =
|
|
4070
|
+
const home = homedir();
|
|
4590
4071
|
return [
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4072
|
+
join2(home, ".hasna"),
|
|
4073
|
+
join2(home, ".ssh"),
|
|
4074
|
+
join2(home, ".secrets")
|
|
4594
4075
|
];
|
|
4595
4076
|
}
|
|
4596
4077
|
function buildBackupPlan(bucket, prefix) {
|
|
4597
4078
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
4598
|
-
const archivePath =
|
|
4079
|
+
const archivePath = join2(homedir(), ".hasna", "machines", "backup.tgz");
|
|
4599
4080
|
const sources = defaultBackupSources();
|
|
4600
4081
|
const steps = [
|
|
4601
4082
|
{
|
|
@@ -4646,33 +4127,33 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
4646
4127
|
}
|
|
4647
4128
|
|
|
4648
4129
|
// src/manifests.ts
|
|
4649
|
-
import { existsSync as
|
|
4650
|
-
import { arch, homedir as
|
|
4130
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4131
|
+
import { arch, homedir as homedir2, hostname as hostname2, platform, userInfo } from "os";
|
|
4651
4132
|
import { dirname as dirname3 } from "path";
|
|
4652
4133
|
|
|
4653
4134
|
// src/paths.ts
|
|
4654
|
-
import { existsSync as
|
|
4655
|
-
import { dirname as dirname2, join as
|
|
4135
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
4136
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
4656
4137
|
function homeDir() {
|
|
4657
4138
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4658
4139
|
}
|
|
4659
4140
|
function getDataDir() {
|
|
4660
|
-
return process.env["HASNA_MACHINES_DIR"] ||
|
|
4141
|
+
return process.env["HASNA_MACHINES_DIR"] || join3(homeDir(), ".hasna", "machines");
|
|
4661
4142
|
}
|
|
4662
4143
|
function getDbPath() {
|
|
4663
|
-
return process.env["HASNA_MACHINES_DB_PATH"] ||
|
|
4144
|
+
return process.env["HASNA_MACHINES_DB_PATH"] || join3(getDataDir(), "machines.db");
|
|
4664
4145
|
}
|
|
4665
4146
|
function getManifestPath() {
|
|
4666
|
-
return process.env["HASNA_MACHINES_MANIFEST_PATH"] ||
|
|
4147
|
+
return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join3(getDataDir(), "machines.json");
|
|
4667
4148
|
}
|
|
4668
4149
|
function getNotificationsPath() {
|
|
4669
|
-
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] ||
|
|
4150
|
+
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join3(getDataDir(), "notifications.json");
|
|
4670
4151
|
}
|
|
4671
4152
|
function ensureParentDir(filePath) {
|
|
4672
4153
|
if (filePath === ":memory:")
|
|
4673
4154
|
return;
|
|
4674
4155
|
const dir = dirname2(resolve(filePath));
|
|
4675
|
-
if (!
|
|
4156
|
+
if (!existsSync2(dir)) {
|
|
4676
4157
|
mkdirSync(dir, { recursive: true });
|
|
4677
4158
|
}
|
|
4678
4159
|
}
|
|
@@ -4714,7 +4195,7 @@ var fleetSchema = exports_external.object({
|
|
|
4714
4195
|
machines: exports_external.array(machineSchema)
|
|
4715
4196
|
});
|
|
4716
4197
|
function detectWorkspacePath() {
|
|
4717
|
-
const home =
|
|
4198
|
+
const home = homedir2();
|
|
4718
4199
|
if (platform() === "darwin") {
|
|
4719
4200
|
return `${home}/Workspace`;
|
|
4720
4201
|
}
|
|
@@ -4738,7 +4219,7 @@ function getDefaultManifest() {
|
|
|
4738
4219
|
};
|
|
4739
4220
|
}
|
|
4740
4221
|
function readManifest(path = getManifestPath()) {
|
|
4741
|
-
if (!
|
|
4222
|
+
if (!existsSync3(path)) {
|
|
4742
4223
|
return getDefaultManifest();
|
|
4743
4224
|
}
|
|
4744
4225
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -4874,19 +4355,19 @@ function countRuns(table) {
|
|
|
4874
4355
|
}
|
|
4875
4356
|
function recordSetupRun(machineId, status, details) {
|
|
4876
4357
|
const db = getDb();
|
|
4877
|
-
const
|
|
4358
|
+
const now = new Date().toISOString();
|
|
4878
4359
|
db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
|
|
4879
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details),
|
|
4360
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
|
|
4880
4361
|
}
|
|
4881
4362
|
function recordSyncRun(machineId, status, actions) {
|
|
4882
4363
|
const db = getDb();
|
|
4883
|
-
const
|
|
4364
|
+
const now = new Date().toISOString();
|
|
4884
4365
|
db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
|
|
4885
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions),
|
|
4366
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
4886
4367
|
}
|
|
4887
4368
|
|
|
4888
4369
|
// src/topology.ts
|
|
4889
|
-
import { existsSync as
|
|
4370
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4890
4371
|
import { arch as arch2, hostname as hostname4, platform as platform2, userInfo as userInfo2 } from "os";
|
|
4891
4372
|
import { spawnSync } from "child_process";
|
|
4892
4373
|
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
@@ -4998,6 +4479,19 @@ function manifestHostReachable(target) {
|
|
|
4998
4479
|
return null;
|
|
4999
4480
|
return overrides.has(target);
|
|
5000
4481
|
}
|
|
4482
|
+
function userFromSshAddress(address) {
|
|
4483
|
+
if (!address)
|
|
4484
|
+
return null;
|
|
4485
|
+
const at = address.indexOf("@");
|
|
4486
|
+
if (at <= 0)
|
|
4487
|
+
return null;
|
|
4488
|
+
return address.slice(0, at);
|
|
4489
|
+
}
|
|
4490
|
+
function commandTargetForRoute(target, user) {
|
|
4491
|
+
if (target.kind === "local" || target.target.includes("@") || !user)
|
|
4492
|
+
return target.target;
|
|
4493
|
+
return `${user}@${target.target}`;
|
|
4494
|
+
}
|
|
5001
4495
|
function routeHints(input) {
|
|
5002
4496
|
const hints = [];
|
|
5003
4497
|
if (input.machineId === input.localMachineId) {
|
|
@@ -5048,6 +4542,7 @@ function buildEntry(input) {
|
|
|
5048
4542
|
});
|
|
5049
4543
|
const selectedRoute = selectRouteHint(hints);
|
|
5050
4544
|
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
4545
|
+
const routeUser = userFromSshAddress(manifest?.sshAddress) ?? (typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null);
|
|
5051
4546
|
return {
|
|
5052
4547
|
machine_id: input.machineId,
|
|
5053
4548
|
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
@@ -5068,7 +4563,7 @@ function buildEntry(input) {
|
|
|
5068
4563
|
ssh: {
|
|
5069
4564
|
address: manifest?.sshAddress ?? null,
|
|
5070
4565
|
route,
|
|
5071
|
-
command_target: selectedRoute
|
|
4566
|
+
command_target: selectedRoute ? commandTargetForRoute(selectedRoute, routeUser) : null
|
|
5072
4567
|
},
|
|
5073
4568
|
route_hints: hints,
|
|
5074
4569
|
tags: manifest?.tags ?? [],
|
|
@@ -5076,7 +4571,7 @@ function buildEntry(input) {
|
|
|
5076
4571
|
};
|
|
5077
4572
|
}
|
|
5078
4573
|
function discoverMachineTopology(options = {}) {
|
|
5079
|
-
const
|
|
4574
|
+
const now = options.now ?? new Date;
|
|
5080
4575
|
const runner = options.runner ?? defaultRunner;
|
|
5081
4576
|
const warnings = [];
|
|
5082
4577
|
const manifest = readManifest();
|
|
@@ -5108,11 +4603,11 @@ function discoverMachineTopology(options = {}) {
|
|
|
5108
4603
|
version: getPackageVersion()
|
|
5109
4604
|
},
|
|
5110
4605
|
capabilities: getMachinesConsumerCapabilities(),
|
|
5111
|
-
generated_at:
|
|
4606
|
+
generated_at: now.toISOString(),
|
|
5112
4607
|
local_machine_id: localMachineId,
|
|
5113
4608
|
local_hostname: hostname4(),
|
|
5114
4609
|
current_platform: normalizePlatform2(),
|
|
5115
|
-
manifest_path_known:
|
|
4610
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
5116
4611
|
machines,
|
|
5117
4612
|
warnings
|
|
5118
4613
|
};
|
|
@@ -5231,11 +4726,11 @@ function cacheability(input) {
|
|
|
5231
4726
|
};
|
|
5232
4727
|
}
|
|
5233
4728
|
function resolveMachineRoute(machineId, options = {}) {
|
|
5234
|
-
const
|
|
4729
|
+
const now = options.now ?? new Date;
|
|
5235
4730
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
5236
4731
|
const warnings = [...topology.warnings];
|
|
5237
4732
|
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
5238
|
-
const generatedAt =
|
|
4733
|
+
const generatedAt = now.toISOString();
|
|
5239
4734
|
if (!machine) {
|
|
5240
4735
|
warnings.push(`machine_not_found:${machineId}`);
|
|
5241
4736
|
return {
|
|
@@ -5261,8 +4756,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
5261
4756
|
},
|
|
5262
4757
|
cacheability: cacheability({
|
|
5263
4758
|
ok: false,
|
|
5264
|
-
observedAt:
|
|
5265
|
-
now
|
|
4759
|
+
observedAt: now,
|
|
4760
|
+
now,
|
|
5266
4761
|
ttlMs: options.resolverTtlMs,
|
|
5267
4762
|
authority: "unresolved",
|
|
5268
4763
|
confidence: "none",
|
|
@@ -5276,6 +4771,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
5276
4771
|
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
5277
4772
|
const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
|
|
5278
4773
|
const ok = Boolean(selectedHint?.target);
|
|
4774
|
+
const commandTarget = selectedHint ? commandTargetForRoute(selectedHint, userFromSshAddress(machine.ssh.address) ?? machine.user) : null;
|
|
5279
4775
|
return {
|
|
5280
4776
|
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
5281
4777
|
package: topology.package,
|
|
@@ -5286,7 +4782,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
5286
4782
|
route,
|
|
5287
4783
|
source: route,
|
|
5288
4784
|
target: selectedHint?.target ?? null,
|
|
5289
|
-
command_target:
|
|
4785
|
+
command_target: commandTarget,
|
|
5290
4786
|
confidence,
|
|
5291
4787
|
local,
|
|
5292
4788
|
evidence: {
|
|
@@ -5299,8 +4795,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
5299
4795
|
},
|
|
5300
4796
|
cacheability: cacheability({
|
|
5301
4797
|
ok,
|
|
5302
|
-
observedAt:
|
|
5303
|
-
now
|
|
4798
|
+
observedAt: now,
|
|
4799
|
+
now,
|
|
5304
4800
|
ttlMs: options.resolverTtlMs,
|
|
5305
4801
|
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
5306
4802
|
confidence,
|
|
@@ -5387,7 +4883,7 @@ function canCheckPathForMachine(machine, localMachineId) {
|
|
|
5387
4883
|
function checkedPathExists(path, check) {
|
|
5388
4884
|
if (!path || !check)
|
|
5389
4885
|
return null;
|
|
5390
|
-
return
|
|
4886
|
+
return existsSync4(path);
|
|
5391
4887
|
}
|
|
5392
4888
|
function repairHint(input) {
|
|
5393
4889
|
const command = [
|
|
@@ -5602,11 +5098,11 @@ function metadataKeysForDiagnostics(metadata) {
|
|
|
5602
5098
|
return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
|
|
5603
5099
|
}
|
|
5604
5100
|
function resolveMachineWorkspace(options) {
|
|
5605
|
-
const
|
|
5101
|
+
const now = options.now ?? new Date;
|
|
5606
5102
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
5607
5103
|
const warnings = [...topology.warnings];
|
|
5608
5104
|
const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
|
|
5609
|
-
const generatedAt =
|
|
5105
|
+
const generatedAt = now.toISOString();
|
|
5610
5106
|
const repoName = options.repoName ?? options.projectId;
|
|
5611
5107
|
const openFilesRepoName = options.openFilesRepoName ?? "open-files";
|
|
5612
5108
|
if (!machine) {
|
|
@@ -5635,8 +5131,8 @@ function resolveMachineWorkspace(options) {
|
|
|
5635
5131
|
},
|
|
5636
5132
|
cacheability: cacheability({
|
|
5637
5133
|
ok: false,
|
|
5638
|
-
observedAt:
|
|
5639
|
-
now
|
|
5134
|
+
observedAt: now,
|
|
5135
|
+
now,
|
|
5640
5136
|
ttlMs: options.resolverTtlMs,
|
|
5641
5137
|
authority: "unresolved",
|
|
5642
5138
|
confidence: "none",
|
|
@@ -5708,8 +5204,8 @@ function resolveMachineWorkspace(options) {
|
|
|
5708
5204
|
},
|
|
5709
5205
|
cacheability: cacheability({
|
|
5710
5206
|
ok: workspaceOk,
|
|
5711
|
-
observedAt:
|
|
5712
|
-
now
|
|
5207
|
+
observedAt: now,
|
|
5208
|
+
now,
|
|
5713
5209
|
ttlMs: options.resolverTtlMs,
|
|
5714
5210
|
authority: workspaceAuthority(workspacePaths),
|
|
5715
5211
|
confidence: workspaceOk ? "medium" : "none",
|
|
@@ -5744,7 +5240,7 @@ function resolveSshTarget(machineId, options = {}) {
|
|
|
5744
5240
|
}
|
|
5745
5241
|
return {
|
|
5746
5242
|
machineId: resolved.machine_id ?? machineId,
|
|
5747
|
-
target: resolved.target,
|
|
5243
|
+
target: resolved.command_target ?? resolved.target,
|
|
5748
5244
|
route: resolved.route,
|
|
5749
5245
|
confidence: resolved.confidence,
|
|
5750
5246
|
warnings: resolved.warnings
|
|
@@ -5946,21 +5442,21 @@ function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
|
5946
5442
|
}
|
|
5947
5443
|
|
|
5948
5444
|
// src/commands/cert.ts
|
|
5949
|
-
import { homedir as
|
|
5950
|
-
import { join as
|
|
5445
|
+
import { homedir as homedir3, platform as platform3 } from "os";
|
|
5446
|
+
import { join as join4 } from "path";
|
|
5951
5447
|
function quote2(value) {
|
|
5952
5448
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5953
5449
|
}
|
|
5954
5450
|
function certDir() {
|
|
5955
|
-
return
|
|
5451
|
+
return join4(homedir3(), ".hasna", "machines", "certs");
|
|
5956
5452
|
}
|
|
5957
5453
|
function buildCertPlan(domains) {
|
|
5958
5454
|
if (domains.length === 0) {
|
|
5959
5455
|
throw new Error("At least one domain is required.");
|
|
5960
5456
|
}
|
|
5961
5457
|
const primary = domains[0];
|
|
5962
|
-
const certPath =
|
|
5963
|
-
const keyPath =
|
|
5458
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
5459
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
5964
5460
|
const steps = [];
|
|
5965
5461
|
if (platform3() === "darwin") {
|
|
5966
5462
|
steps.push({
|
|
@@ -6024,14 +5520,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
6024
5520
|
}
|
|
6025
5521
|
|
|
6026
5522
|
// src/commands/dns.ts
|
|
6027
|
-
import { existsSync as
|
|
6028
|
-
import { join as
|
|
5523
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5524
|
+
import { join as join5 } from "path";
|
|
6029
5525
|
function getDnsPath() {
|
|
6030
|
-
return
|
|
5526
|
+
return join5(getDataDir(), "dns.json");
|
|
6031
5527
|
}
|
|
6032
5528
|
function readMappings() {
|
|
6033
5529
|
const path = getDnsPath();
|
|
6034
|
-
if (!
|
|
5530
|
+
if (!existsSync5(path))
|
|
6035
5531
|
return [];
|
|
6036
5532
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
6037
5533
|
}
|
|
@@ -6060,10 +5556,10 @@ function renderDomainMapping(domain) {
|
|
|
6060
5556
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
6061
5557
|
caddySnippet: `${entry.domain} {
|
|
6062
5558
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
6063
|
-
tls ${
|
|
5559
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
6064
5560
|
}`,
|
|
6065
|
-
certPath:
|
|
6066
|
-
keyPath:
|
|
5561
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
5562
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
6067
5563
|
};
|
|
6068
5564
|
}
|
|
6069
5565
|
|
|
@@ -6333,7 +5829,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
6333
5829
|
}
|
|
6334
5830
|
|
|
6335
5831
|
// src/commands/notifications.ts
|
|
6336
|
-
import { existsSync as
|
|
5832
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
6337
5833
|
var notificationChannelSchema = exports_external.object({
|
|
6338
5834
|
id: exports_external.string(),
|
|
6339
5835
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -6415,7 +5911,7 @@ ${message}
|
|
|
6415
5911
|
}
|
|
6416
5912
|
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
6417
5913
|
}
|
|
6418
|
-
async function
|
|
5914
|
+
async function dispatchWebhook(channel, event, message) {
|
|
6419
5915
|
const response = await fetch(channel.target, {
|
|
6420
5916
|
method: "POST",
|
|
6421
5917
|
headers: {
|
|
@@ -6440,7 +5936,7 @@ async function dispatchWebhook2(channel, event, message) {
|
|
|
6440
5936
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
6441
5937
|
};
|
|
6442
5938
|
}
|
|
6443
|
-
async function
|
|
5939
|
+
async function dispatchCommand(channel, event, message) {
|
|
6444
5940
|
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
6445
5941
|
stdout: "pipe",
|
|
6446
5942
|
stderr: "pipe",
|
|
@@ -6463,7 +5959,7 @@ async function dispatchCommand2(channel, event, message) {
|
|
|
6463
5959
|
detail: stdout || "Command completed successfully"
|
|
6464
5960
|
};
|
|
6465
5961
|
}
|
|
6466
|
-
async function
|
|
5962
|
+
async function dispatchChannel(channel, event, message) {
|
|
6467
5963
|
if (!channel.enabled) {
|
|
6468
5964
|
return {
|
|
6469
5965
|
channelId: channel.id,
|
|
@@ -6477,9 +5973,9 @@ async function dispatchChannel2(channel, event, message) {
|
|
|
6477
5973
|
return dispatchEmail(channel, event, message);
|
|
6478
5974
|
}
|
|
6479
5975
|
if (channel.type === "webhook") {
|
|
6480
|
-
return
|
|
5976
|
+
return dispatchWebhook(channel, event, message);
|
|
6481
5977
|
}
|
|
6482
|
-
return
|
|
5978
|
+
return dispatchCommand(channel, event, message);
|
|
6483
5979
|
}
|
|
6484
5980
|
function getDefaultNotificationConfig() {
|
|
6485
5981
|
return {
|
|
@@ -6489,7 +5985,7 @@ function getDefaultNotificationConfig() {
|
|
|
6489
5985
|
};
|
|
6490
5986
|
}
|
|
6491
5987
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
6492
|
-
if (!
|
|
5988
|
+
if (!existsSync6(path)) {
|
|
6493
5989
|
return getDefaultNotificationConfig();
|
|
6494
5990
|
}
|
|
6495
5991
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -6534,7 +6030,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
6534
6030
|
const deliveries = [];
|
|
6535
6031
|
for (const channel of channels) {
|
|
6536
6032
|
try {
|
|
6537
|
-
deliveries.push(await
|
|
6033
|
+
deliveries.push(await dispatchChannel(channel, event, message));
|
|
6538
6034
|
} catch (error) {
|
|
6539
6035
|
deliveries.push({
|
|
6540
6036
|
channelId: channel.id,
|
|
@@ -6569,7 +6065,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
6569
6065
|
if (!options.yes) {
|
|
6570
6066
|
throw new Error("Notification test execution requires --yes.");
|
|
6571
6067
|
}
|
|
6572
|
-
const delivery = await
|
|
6068
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
6573
6069
|
return {
|
|
6574
6070
|
channelId,
|
|
6575
6071
|
mode: "apply",
|
|
@@ -6633,6 +6129,9 @@ function listPorts(machineId) {
|
|
|
6633
6129
|
};
|
|
6634
6130
|
}
|
|
6635
6131
|
|
|
6132
|
+
// src/commands/serve.ts
|
|
6133
|
+
import { EventsClient, sanitizeChannelsForOutput } from "@hasna/events";
|
|
6134
|
+
|
|
6636
6135
|
// src/commands/manifest.ts
|
|
6637
6136
|
function manifestList() {
|
|
6638
6137
|
return readManifest();
|
|
@@ -6910,7 +6409,7 @@ function renderDashboardHtml() {
|
|
|
6910
6409
|
}
|
|
6911
6410
|
|
|
6912
6411
|
// src/commands/setup.ts
|
|
6913
|
-
import { homedir as
|
|
6412
|
+
import { homedir as homedir4 } from "os";
|
|
6914
6413
|
function quote3(value) {
|
|
6915
6414
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6916
6415
|
}
|
|
@@ -6983,7 +6482,7 @@ function buildSetupPlan(machineId) {
|
|
|
6983
6482
|
const target = selected || {
|
|
6984
6483
|
id: currentMachineId,
|
|
6985
6484
|
platform: "linux",
|
|
6986
|
-
workspacePath: `${
|
|
6485
|
+
workspacePath: `${homedir4()}/workspace`
|
|
6987
6486
|
};
|
|
6988
6487
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
6989
6488
|
return {
|
|
@@ -7028,8 +6527,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
7028
6527
|
}
|
|
7029
6528
|
|
|
7030
6529
|
// src/commands/sync.ts
|
|
7031
|
-
import { existsSync as
|
|
7032
|
-
import { homedir as
|
|
6530
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
6531
|
+
import { homedir as homedir5 } from "os";
|
|
7033
6532
|
function quote4(value) {
|
|
7034
6533
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7035
6534
|
}
|
|
@@ -7081,8 +6580,8 @@ function detectFileActions(machine) {
|
|
|
7081
6580
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
7082
6581
|
}
|
|
7083
6582
|
return (machine.files || []).map((file, index) => {
|
|
7084
|
-
const sourceExists =
|
|
7085
|
-
const targetExists =
|
|
6583
|
+
const sourceExists = existsSync7(file.source);
|
|
6584
|
+
const targetExists = existsSync7(file.target);
|
|
7086
6585
|
let status = "missing";
|
|
7087
6586
|
if (sourceExists && targetExists) {
|
|
7088
6587
|
if (file.mode === "symlink") {
|
|
@@ -7113,7 +6612,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
7113
6612
|
const target = selected || {
|
|
7114
6613
|
id: currentMachineId,
|
|
7115
6614
|
platform: "linux",
|
|
7116
|
-
workspacePath: `${
|
|
6615
|
+
workspacePath: `${homedir5()}/workspace`
|
|
7117
6616
|
};
|
|
7118
6617
|
const actions = [
|
|
7119
6618
|
...detectPackageActions(target, runner),
|
|
@@ -7704,7 +7203,7 @@ function upsertSqlite(db, table, columns, rows) {
|
|
|
7704
7203
|
}
|
|
7705
7204
|
function recordSyncMeta(db, direction, results) {
|
|
7706
7205
|
ensureSyncMetaTable(db);
|
|
7707
|
-
const
|
|
7206
|
+
const now = new Date().toISOString();
|
|
7708
7207
|
const statement = db.query(`
|
|
7709
7208
|
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
7710
7209
|
VALUES (?, ?, ?)
|
|
@@ -7713,7 +7212,7 @@ function recordSyncMeta(db, direction, results) {
|
|
|
7713
7212
|
for (const result of results) {
|
|
7714
7213
|
if (result.errors.length > 0)
|
|
7715
7214
|
continue;
|
|
7716
|
-
statement.run(result.table,
|
|
7215
|
+
statement.run(result.table, now, direction);
|
|
7717
7216
|
}
|
|
7718
7217
|
}
|
|
7719
7218
|
function ensureSyncMetaTable(db) {
|
|
@@ -7763,7 +7262,7 @@ function buildServer(version = getPackageVersion()) {
|
|
|
7763
7262
|
}
|
|
7764
7263
|
function createMcpServer(version) {
|
|
7765
7264
|
const server = new McpServer({ name: "machines", version });
|
|
7766
|
-
const events = new
|
|
7265
|
+
const events = new EventsClient2;
|
|
7767
7266
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
7768
7267
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
7769
7268
|
}));
|
|
@@ -7917,7 +7416,7 @@ function createMcpServer(version) {
|
|
|
7917
7416
|
}));
|
|
7918
7417
|
server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel") }, async ({ event, message, channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id }), null, 2) }] }));
|
|
7919
7418
|
server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] }));
|
|
7920
|
-
server.tool("machines_webhooks_add", "Add or replace a shared
|
|
7419
|
+
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
7921
7420
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
7922
7421
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
7923
7422
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
@@ -7925,26 +7424,26 @@ function createMcpServer(version) {
|
|
|
7925
7424
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
7926
7425
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
7927
7426
|
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
7928
|
-
const
|
|
7427
|
+
const now = new Date().toISOString();
|
|
7929
7428
|
const channel = await events.addChannel({
|
|
7930
7429
|
id: channel_id,
|
|
7931
7430
|
enabled: enabled ?? true,
|
|
7932
7431
|
transport: "webhook",
|
|
7933
7432
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
7934
7433
|
webhook: { url, secret },
|
|
7935
|
-
createdAt:
|
|
7936
|
-
updatedAt:
|
|
7434
|
+
createdAt: now,
|
|
7435
|
+
updatedAt: now
|
|
7937
7436
|
});
|
|
7938
7437
|
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
7939
7438
|
});
|
|
7940
|
-
server.tool("machines_webhooks_list", "List shared
|
|
7941
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
7439
|
+
server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
|
|
7440
|
+
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
|
|
7942
7441
|
}));
|
|
7943
|
-
server.tool("machines_webhooks_test", "Send a test event to one shared
|
|
7442
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) => ({
|
|
7944
7443
|
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
7945
7444
|
}));
|
|
7946
|
-
server.tool("machines_webhooks_remove", "Remove a shared
|
|
7947
|
-
server.tool("machines_events_emit", "Emit a shared
|
|
7445
|
+
server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] }));
|
|
7446
|
+
server.tool("machines_events_emit", "Emit a shared event from machines.", {
|
|
7948
7447
|
event_type: exports_external.string().describe("Event type"),
|
|
7949
7448
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
7950
7449
|
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
@@ -7965,10 +7464,10 @@ function createMcpServer(version) {
|
|
|
7965
7464
|
dedupeKey: dedupe_key
|
|
7966
7465
|
}, { deliver: deliver !== false }), null, 2) }]
|
|
7967
7466
|
}));
|
|
7968
|
-
server.tool("machines_events_list", "List shared
|
|
7467
|
+
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
7969
7468
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
7970
7469
|
}));
|
|
7971
|
-
server.tool("machines_events_replay", "Replay shared
|
|
7470
|
+
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) => ({
|
|
7972
7471
|
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
7973
7472
|
}));
|
|
7974
7473
|
server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
|