@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/dist/index.js CHANGED
@@ -11287,6 +11287,19 @@ function manifestHostReachable(target) {
11287
11287
  return null;
11288
11288
  return overrides.has(target);
11289
11289
  }
11290
+ function userFromSshAddress(address) {
11291
+ if (!address)
11292
+ return null;
11293
+ const at = address.indexOf("@");
11294
+ if (at <= 0)
11295
+ return null;
11296
+ return address.slice(0, at);
11297
+ }
11298
+ function commandTargetForRoute(target, user) {
11299
+ if (target.kind === "local" || target.target.includes("@") || !user)
11300
+ return target.target;
11301
+ return `${user}@${target.target}`;
11302
+ }
11290
11303
  function routeHints(input) {
11291
11304
  const hints = [];
11292
11305
  if (input.machineId === input.localMachineId) {
@@ -11337,6 +11350,7 @@ function buildEntry(input) {
11337
11350
  });
11338
11351
  const selectedRoute = selectRouteHint(hints);
11339
11352
  const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
11353
+ const routeUser = userFromSshAddress(manifest?.sshAddress) ?? (typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null);
11340
11354
  return {
11341
11355
  machine_id: input.machineId,
11342
11356
  hostname: manifest?.hostname ?? peer?.HostName ?? null,
@@ -11357,7 +11371,7 @@ function buildEntry(input) {
11357
11371
  ssh: {
11358
11372
  address: manifest?.sshAddress ?? null,
11359
11373
  route,
11360
- command_target: selectedRoute?.target ?? null
11374
+ command_target: selectedRoute ? commandTargetForRoute(selectedRoute, routeUser) : null
11361
11375
  },
11362
11376
  route_hints: hints,
11363
11377
  tags: manifest?.tags ?? [],
@@ -11596,6 +11610,7 @@ function resolveMachineRoute(machineId, options = {}) {
11596
11610
  const local = route === "local" || machine.machine_id === topology.local_machine_id;
11597
11611
  const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
11598
11612
  const ok = Boolean(selectedHint?.target);
11613
+ const commandTarget = selectedHint ? commandTargetForRoute(selectedHint, userFromSshAddress(machine.ssh.address) ?? machine.user) : null;
11599
11614
  return {
11600
11615
  schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
11601
11616
  package: topology.package,
@@ -11606,7 +11621,7 @@ function resolveMachineRoute(machineId, options = {}) {
11606
11621
  route,
11607
11622
  source: route,
11608
11623
  target: selectedHint?.target ?? null,
11609
- command_target: selectedHint?.target ?? null,
11624
+ command_target: commandTarget,
11610
11625
  confidence,
11611
11626
  local,
11612
11627
  evidence: {
@@ -12153,7 +12168,7 @@ function resolveSshTarget(machineId, options = {}) {
12153
12168
  }
12154
12169
  return {
12155
12170
  machineId: resolved.machine_id ?? machineId,
12156
- target: resolved.target,
12171
+ target: resolved.command_target ?? resolved.target,
12157
12172
  route: resolved.route,
12158
12173
  confidence: resolved.confidence,
12159
12174
  warnings: resolved.warnings
@@ -13440,528 +13455,7 @@ function listPorts(machineId) {
13440
13455
  // src/commands/runtime.ts
13441
13456
  import { spawnSync as spawnSync4 } from "child_process";
13442
13457
  import { setTimeout as sleep } from "timers/promises";
13443
-
13444
- // node_modules/@hasna/events/dist/index.js
13445
- import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
13446
- import { existsSync as existsSync7 } from "fs";
13447
- import { homedir as homedir4 } from "os";
13448
- import { join as join6 } from "path";
13449
- import { createHmac, timingSafeEqual } from "crypto";
13450
- import { randomUUID } from "crypto";
13451
- import { spawn } from "child_process";
13452
- import { randomUUID as randomUUID2 } from "crypto";
13453
- function getPathValue(input, path) {
13454
- return path.split(".").reduce((value, part) => {
13455
- if (value && typeof value === "object" && part in value) {
13456
- return value[part];
13457
- }
13458
- return;
13459
- }, input);
13460
- }
13461
- function wildcardToRegExp(pattern) {
13462
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
13463
- return new RegExp(`^${escaped}$`);
13464
- }
13465
- function matchString(value, matcher) {
13466
- if (matcher === undefined)
13467
- return true;
13468
- if (value === undefined)
13469
- return false;
13470
- const matchers = Array.isArray(matcher) ? matcher : [matcher];
13471
- return matchers.some((item) => wildcardToRegExp(item).test(value));
13472
- }
13473
- function matchRecord(input, matcher) {
13474
- if (!matcher)
13475
- return true;
13476
- return Object.entries(matcher).every(([path, expected]) => {
13477
- const actual = getPathValue(input, path);
13478
- if (typeof expected === "string" || Array.isArray(expected)) {
13479
- return matchString(actual === undefined ? undefined : String(actual), expected);
13480
- }
13481
- return actual === expected;
13482
- });
13483
- }
13484
- function eventMatchesFilter(event, filter) {
13485
- 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);
13486
- }
13487
- function channelMatchesEvent(channel, event) {
13488
- if (!channel.enabled)
13489
- return false;
13490
- if (!channel.filters || channel.filters.length === 0)
13491
- return true;
13492
- return channel.filters.some((filter) => eventMatchesFilter(event, filter));
13493
- }
13494
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
13495
- var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
13496
- function getEventsDataDir(override) {
13497
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
13498
- }
13499
-
13500
- class JsonEventsStore {
13501
- dataDir;
13502
- channelsPath;
13503
- eventsPath;
13504
- deliveriesPath;
13505
- constructor(dataDir = getEventsDataDir()) {
13506
- this.dataDir = dataDir;
13507
- this.channelsPath = join6(dataDir, "channels.json");
13508
- this.eventsPath = join6(dataDir, "events.json");
13509
- this.deliveriesPath = join6(dataDir, "deliveries.json");
13510
- }
13511
- async init() {
13512
- await mkdir(this.dataDir, { recursive: true, mode: 448 });
13513
- await chmod(this.dataDir, 448).catch(() => {
13514
- return;
13515
- });
13516
- await this.ensureArrayFile(this.channelsPath);
13517
- await this.ensureArrayFile(this.eventsPath);
13518
- await this.ensureArrayFile(this.deliveriesPath);
13519
- }
13520
- async addChannel(channel) {
13521
- await this.init();
13522
- const channels = await this.readJson(this.channelsPath, []);
13523
- const index = channels.findIndex((item) => item.id === channel.id);
13524
- if (index >= 0) {
13525
- channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
13526
- } else {
13527
- channels.push(channel);
13528
- }
13529
- await this.writeJson(this.channelsPath, channels);
13530
- return index >= 0 ? channels[index] : channel;
13531
- }
13532
- async listChannels() {
13533
- await this.init();
13534
- return this.readJson(this.channelsPath, []);
13535
- }
13536
- async getChannel(id) {
13537
- const channels = await this.listChannels();
13538
- return channels.find((channel) => channel.id === id);
13539
- }
13540
- async removeChannel(id) {
13541
- await this.init();
13542
- const channels = await this.readJson(this.channelsPath, []);
13543
- const next = channels.filter((channel) => channel.id !== id);
13544
- await this.writeJson(this.channelsPath, next);
13545
- return next.length !== channels.length;
13546
- }
13547
- async appendEvent(event) {
13548
- await this.init();
13549
- const events = await this.readJson(this.eventsPath, []);
13550
- events.push(event);
13551
- await this.writeJson(this.eventsPath, events);
13552
- return event;
13553
- }
13554
- async listEvents() {
13555
- await this.init();
13556
- return this.readJson(this.eventsPath, []);
13557
- }
13558
- async findEventByIdentity(identity) {
13559
- const events = await this.listEvents();
13560
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
13561
- }
13562
- async appendDelivery(result) {
13563
- await this.init();
13564
- const deliveries = await this.readJson(this.deliveriesPath, []);
13565
- deliveries.push(result);
13566
- await this.writeJson(this.deliveriesPath, deliveries);
13567
- return result;
13568
- }
13569
- async listDeliveries() {
13570
- await this.init();
13571
- return this.readJson(this.deliveriesPath, []);
13572
- }
13573
- async exportData() {
13574
- return {
13575
- channels: await this.listChannels(),
13576
- events: await this.listEvents(),
13577
- deliveries: await this.listDeliveries()
13578
- };
13579
- }
13580
- async ensureArrayFile(path) {
13581
- if (!existsSync7(path)) {
13582
- await writeFile(path, `[]
13583
- `, { encoding: "utf-8", mode: 384 });
13584
- }
13585
- await chmod(path, 384).catch(() => {
13586
- return;
13587
- });
13588
- }
13589
- async readJson(path, fallback) {
13590
- try {
13591
- const raw = await readFile(path, "utf-8");
13592
- if (!raw.trim())
13593
- return fallback;
13594
- return JSON.parse(raw);
13595
- } catch (error) {
13596
- if (error.code === "ENOENT")
13597
- return fallback;
13598
- throw error;
13599
- }
13600
- }
13601
- async writeJson(path, value) {
13602
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
13603
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
13604
- `, { encoding: "utf-8", mode: 384 });
13605
- await rename(tempPath, path);
13606
- await chmod(path, 384).catch(() => {
13607
- return;
13608
- });
13609
- }
13610
- }
13611
- var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
13612
- function buildSignatureBase(timestamp, body) {
13613
- return `${timestamp}.${body}`;
13614
- }
13615
- function signPayload(secret, timestamp, body) {
13616
- const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
13617
- return `sha256=${digest}`;
13618
- }
13619
- function now() {
13620
- return new Date().toISOString();
13621
- }
13622
- function truncate(value, max = 4096) {
13623
- return value.length > max ? `${value.slice(0, max)}...` : value;
13624
- }
13625
- function buildWebhookRequest(event, channel) {
13626
- if (!channel.webhook)
13627
- throw new Error(`Channel ${channel.id} has no webhook config`);
13628
- const body = JSON.stringify(event);
13629
- const timestamp = event.time;
13630
- const headers = {
13631
- "Content-Type": "application/json",
13632
- "User-Agent": "@hasna/events",
13633
- "X-Hasna-Event-Id": event.id,
13634
- "X-Hasna-Event-Type": event.type,
13635
- "X-Hasna-Timestamp": timestamp,
13636
- ...channel.webhook.headers
13637
- };
13638
- if (channel.webhook.secret) {
13639
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
13640
- }
13641
- return { body, headers };
13642
- }
13643
- async function dispatchWebhook2(event, channel, options = {}) {
13644
- if (!channel.webhook)
13645
- throw new Error(`Channel ${channel.id} has no webhook config`);
13646
- const startedAt = now();
13647
- const { body, headers } = buildWebhookRequest(event, channel);
13648
- const controller = new AbortController;
13649
- const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
13650
- try {
13651
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
13652
- method: "POST",
13653
- headers,
13654
- body,
13655
- signal: controller.signal
13656
- });
13657
- const responseBody = truncate(await response.text());
13658
- return {
13659
- attempt: 1,
13660
- status: response.ok ? "success" : "failed",
13661
- startedAt,
13662
- completedAt: now(),
13663
- responseStatus: response.status,
13664
- responseBody,
13665
- error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
13666
- };
13667
- } catch (error) {
13668
- return {
13669
- attempt: 1,
13670
- status: "failed",
13671
- startedAt,
13672
- completedAt: now(),
13673
- error: error instanceof Error ? error.message : String(error)
13674
- };
13675
- } finally {
13676
- clearTimeout(timeout);
13677
- }
13678
- }
13679
- async function dispatchCommand2(event, channel) {
13680
- if (!channel.command)
13681
- throw new Error(`Channel ${channel.id} has no command config`);
13682
- const startedAt = now();
13683
- const eventJson = JSON.stringify(event);
13684
- const env = {
13685
- ...process.env,
13686
- ...channel.command.env,
13687
- HASNA_CHANNEL_ID: channel.id,
13688
- HASNA_EVENT_ID: event.id,
13689
- HASNA_EVENT_TYPE: event.type,
13690
- HASNA_EVENT_SOURCE: event.source,
13691
- HASNA_EVENT_SUBJECT: event.subject ?? "",
13692
- HASNA_EVENT_SEVERITY: event.severity,
13693
- HASNA_EVENT_TIME: event.time,
13694
- HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
13695
- HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
13696
- HASNA_EVENT_JSON: eventJson
13697
- };
13698
- return new Promise((resolve2) => {
13699
- const child = spawn(channel.command.command, channel.command.args ?? [], {
13700
- cwd: channel.command.cwd,
13701
- env,
13702
- stdio: ["pipe", "pipe", "pipe"]
13703
- });
13704
- let stdout = "";
13705
- let stderr = "";
13706
- const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
13707
- child.stdin.end(eventJson);
13708
- child.stdout.on("data", (chunk) => {
13709
- stdout += chunk.toString();
13710
- });
13711
- child.stderr.on("data", (chunk) => {
13712
- stderr += chunk.toString();
13713
- });
13714
- child.on("error", (error) => {
13715
- clearTimeout(timeout);
13716
- resolve2({
13717
- attempt: 1,
13718
- status: "failed",
13719
- startedAt,
13720
- completedAt: now(),
13721
- stdout: truncate(stdout),
13722
- stderr: truncate(stderr),
13723
- error: error.message
13724
- });
13725
- });
13726
- child.on("close", (code, signal) => {
13727
- clearTimeout(timeout);
13728
- const success = code === 0;
13729
- resolve2({
13730
- attempt: 1,
13731
- status: success ? "success" : "failed",
13732
- startedAt,
13733
- completedAt: now(),
13734
- stdout: truncate(stdout),
13735
- stderr: truncate(stderr),
13736
- error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
13737
- });
13738
- });
13739
- });
13740
- }
13741
- async function dispatchChannel2(event, channel, options = {}) {
13742
- if (channel.transport === "webhook")
13743
- return dispatchWebhook2(event, channel, options);
13744
- if (channel.transport === "command")
13745
- return dispatchCommand2(event, channel);
13746
- return {
13747
- attempt: 1,
13748
- status: "skipped",
13749
- startedAt: now(),
13750
- completedAt: now(),
13751
- error: `Unsupported transport: ${channel.transport}`
13752
- };
13753
- }
13754
- function createDeliveryResult(event, channel, attempts) {
13755
- const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
13756
- return {
13757
- id: randomUUID(),
13758
- eventId: event.id,
13759
- channelId: channel.id,
13760
- transport: channel.transport,
13761
- status,
13762
- attempts,
13763
- createdAt: attempts[0]?.startedAt ?? now(),
13764
- completedAt: attempts.at(-1)?.completedAt ?? now()
13765
- };
13766
- }
13767
- function createEvent(input) {
13768
- return {
13769
- id: input.id ?? randomUUID2(),
13770
- source: input.source,
13771
- type: input.type,
13772
- time: normalizeTime(input.time),
13773
- subject: input.subject,
13774
- severity: input.severity ?? "info",
13775
- data: input.data ?? {},
13776
- message: input.message,
13777
- dedupeKey: input.dedupeKey,
13778
- schemaVersion: input.schemaVersion ?? "1.0",
13779
- metadata: input.metadata ?? {}
13780
- };
13781
- }
13782
-
13783
- class EventsClient {
13784
- store;
13785
- redactors;
13786
- transportOptions;
13787
- constructor(options = {}) {
13788
- this.store = options.store ?? new JsonEventsStore(options.dataDir);
13789
- this.redactors = options.redactors ?? [];
13790
- this.transportOptions = { fetchImpl: options.fetchImpl };
13791
- }
13792
- async addChannel(input) {
13793
- const timestamp = new Date().toISOString();
13794
- return this.store.addChannel({
13795
- ...input,
13796
- createdAt: input.createdAt ?? timestamp,
13797
- updatedAt: input.updatedAt ?? timestamp
13798
- });
13799
- }
13800
- async listChannels() {
13801
- return this.store.listChannels();
13802
- }
13803
- async removeChannel(id) {
13804
- return this.store.removeChannel(id);
13805
- }
13806
- async emit(input, options = {}) {
13807
- const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
13808
- if (options.dedupe !== false) {
13809
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
13810
- if (existing) {
13811
- return { event: existing, deliveries: [], deduped: true };
13812
- }
13813
- }
13814
- await this.store.appendEvent(event);
13815
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
13816
- return { event, deliveries, deduped: false };
13817
- }
13818
- async listEvents() {
13819
- return this.store.listEvents();
13820
- }
13821
- async listDeliveries() {
13822
- return this.store.listDeliveries();
13823
- }
13824
- async deliver(event) {
13825
- const channels = await this.store.listChannels();
13826
- const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
13827
- const deliveries = [];
13828
- for (const channel of selected) {
13829
- const eventForChannel = await this.applyRedaction(event, channel);
13830
- const result = await this.deliverWithRetry(eventForChannel, channel);
13831
- await this.store.appendDelivery(result);
13832
- deliveries.push(result);
13833
- }
13834
- return deliveries;
13835
- }
13836
- async testChannel(id, input = {}) {
13837
- const channel = await this.store.getChannel(id);
13838
- if (!channel)
13839
- throw new Error(`Channel not found: ${id}`);
13840
- const event = createEvent({
13841
- source: input.source ?? "hasna.events",
13842
- type: input.type ?? "events.test",
13843
- subject: input.subject ?? id,
13844
- severity: input.severity ?? "info",
13845
- data: input.data ?? { test: true },
13846
- message: input.message ?? "Hasna events test delivery",
13847
- dedupeKey: input.dedupeKey,
13848
- schemaVersion: input.schemaVersion,
13849
- metadata: input.metadata,
13850
- time: input.time,
13851
- id: input.id
13852
- });
13853
- const eventForChannel = await this.applyRedaction(event, channel);
13854
- const result = await this.deliverWithRetry(eventForChannel, channel);
13855
- await this.store.appendDelivery(result);
13856
- return result;
13857
- }
13858
- async replay(options = {}) {
13859
- const events = (await this.store.listEvents()).filter((event) => {
13860
- if (options.eventId && event.id !== options.eventId)
13861
- return false;
13862
- if (options.source && event.source !== options.source)
13863
- return false;
13864
- if (options.type && event.type !== options.type)
13865
- return false;
13866
- return true;
13867
- });
13868
- if (options.dryRun)
13869
- return { events, deliveries: [] };
13870
- const deliveries = [];
13871
- for (const event of events) {
13872
- deliveries.push(...await this.deliver(event));
13873
- }
13874
- return { events, deliveries };
13875
- }
13876
- async applyRedaction(event, channel) {
13877
- let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
13878
- for (const redactor of this.redactors) {
13879
- next = await redactor(next, channel);
13880
- }
13881
- return next;
13882
- }
13883
- async deliverWithRetry(event, channel) {
13884
- const policy = normalizeRetryPolicy(channel.retry);
13885
- const attempts = [];
13886
- for (let index = 0;index < policy.maxAttempts; index += 1) {
13887
- const attempt = await dispatchChannel2(event, channel, this.transportOptions);
13888
- attempt.attempt = index + 1;
13889
- if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
13890
- attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
13891
- }
13892
- attempts.push(attempt);
13893
- if (attempt.status !== "failed")
13894
- break;
13895
- if (attempt.nextBackoffMs)
13896
- await Bun.sleep(attempt.nextBackoffMs);
13897
- }
13898
- return createDeliveryResult(event, channel, attempts);
13899
- }
13900
- }
13901
- function redactPaths(event, paths, replacement = "[REDACTED]") {
13902
- if (paths.length === 0)
13903
- return event;
13904
- const copy = structuredClone(event);
13905
- for (const path of paths) {
13906
- setPath(copy, path, replacement);
13907
- }
13908
- return copy;
13909
- }
13910
- function sanitizeChannelForOutput(channel) {
13911
- const copy = structuredClone(channel);
13912
- if (copy.webhook?.secret)
13913
- copy.webhook.secret = "[REDACTED]";
13914
- if (copy.command?.env) {
13915
- copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
13916
- }
13917
- return copy;
13918
- }
13919
- function sanitizeChannelsForOutput(channels) {
13920
- return channels.map(sanitizeChannelForOutput);
13921
- }
13922
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
13923
- return redactValue(event, replacement);
13924
- }
13925
- function shouldRedactKey(key) {
13926
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
13927
- }
13928
- function redactValue(value, replacement) {
13929
- if (Array.isArray(value))
13930
- return value.map((item) => redactValue(item, replacement));
13931
- if (!value || typeof value !== "object")
13932
- return value;
13933
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
13934
- key,
13935
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
13936
- ]));
13937
- }
13938
- function setPath(input, path, replacement) {
13939
- const parts = path.split(".");
13940
- let cursor = input;
13941
- for (const part of parts.slice(0, -1)) {
13942
- const next = cursor[part];
13943
- if (!next || typeof next !== "object")
13944
- return;
13945
- cursor = next;
13946
- }
13947
- const last = parts.at(-1);
13948
- if (last && last in cursor)
13949
- cursor[last] = replacement;
13950
- }
13951
- function normalizeTime(value) {
13952
- if (!value)
13953
- return new Date().toISOString();
13954
- return value instanceof Date ? value.toISOString() : value;
13955
- }
13956
- function normalizeRetryPolicy(policy) {
13957
- return {
13958
- maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
13959
- backoffMs: Math.max(0, policy?.backoffMs ?? 250),
13960
- multiplier: Math.max(1, policy?.multiplier ?? 2)
13961
- };
13962
- }
13963
-
13964
- // src/commands/runtime.ts
13458
+ import { EventsClient } from "@hasna/events";
13965
13459
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
13966
13460
  const checkedAt = new Date().toISOString();
13967
13461
  const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
@@ -14046,6 +13540,9 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
14046
13540
  };
14047
13541
  return client.emit(input, { deliver });
14048
13542
  }
13543
+ // src/commands/serve.ts
13544
+ import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
13545
+
14049
13546
  // src/commands/status.ts
14050
13547
  function getStatus() {
14051
13548
  const manifest = readManifest();
@@ -14271,7 +13768,7 @@ function jsonError(message, status = 400) {
14271
13768
  }
14272
13769
  function startDashboardServer(options = {}) {
14273
13770
  const info = getServeInfo(options);
14274
- const events = new EventsClient;
13771
+ const events = new EventsClient2;
14275
13772
  return Bun.serve({
14276
13773
  hostname: info.host,
14277
13774
  port: info.port,
@@ -14425,7 +13922,7 @@ function runSelfTest() {
14425
13922
  };
14426
13923
  }
14427
13924
  // src/commands/setup.ts
14428
- import { homedir as homedir5 } from "os";
13925
+ import { homedir as homedir4 } from "os";
14429
13926
  function quote3(value) {
14430
13927
  return `'${value.replace(/'/g, `'\\''`)}'`;
14431
13928
  }
@@ -14498,7 +13995,7 @@ function buildSetupPlan(machineId) {
14498
13995
  const target = selected || {
14499
13996
  id: currentMachineId,
14500
13997
  platform: "linux",
14501
- workspacePath: `${homedir5()}/workspace`
13998
+ workspacePath: `${homedir4()}/workspace`
14502
13999
  };
14503
14000
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
14504
14001
  return {
@@ -14542,7 +14039,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
14542
14039
  return summary;
14543
14040
  }
14544
14041
  // src/commands/screen.ts
14545
- var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
14042
+ var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
14043
+ var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
14546
14044
  function shellQuote7(value) {
14547
14045
  return `'${value.replace(/'/g, "'\\''")}'`;
14548
14046
  }
@@ -14566,7 +14064,8 @@ function splitTarget(target) {
14566
14064
  return [target.slice(0, at), target.slice(at + 1)];
14567
14065
  }
14568
14066
  function defaultScreenPasswordSecretKey(machineId) {
14569
- return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
14067
+ const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
14068
+ return `${namespace}/screen-${machineId}-vnc-password`;
14570
14069
  }
14571
14070
  function resolveScreenTarget(machineId, options = {}) {
14572
14071
  const resolved = resolveMachineRoute(machineId, options);
@@ -14663,8 +14162,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
14663
14162
  };
14664
14163
  }
14665
14164
  // src/commands/sync.ts
14666
- import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
14667
- import { homedir as homedir6 } from "os";
14165
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
14166
+ import { homedir as homedir5 } from "os";
14668
14167
  function quote4(value) {
14669
14168
  return `'${value.replace(/'/g, `'\\''`)}'`;
14670
14169
  }
@@ -14716,8 +14215,8 @@ function detectFileActions(machine) {
14716
14215
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
14717
14216
  }
14718
14217
  return (machine.files || []).map((file, index) => {
14719
- const sourceExists = existsSync8(file.source);
14720
- const targetExists = existsSync8(file.target);
14218
+ const sourceExists = existsSync7(file.source);
14219
+ const targetExists = existsSync7(file.target);
14721
14220
  let status = "missing";
14722
14221
  if (sourceExists && targetExists) {
14723
14222
  if (file.mode === "symlink") {
@@ -14748,7 +14247,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
14748
14247
  const target = selected || {
14749
14248
  id: currentMachineId,
14750
14249
  platform: "linux",
14751
- workspacePath: `${homedir6()}/workspace`
14250
+ workspacePath: `${homedir5()}/workspace`
14752
14251
  };
14753
14252
  const actions = [
14754
14253
  ...detectPackageActions(target, runner),
@@ -14968,6 +14467,9 @@ function repairWorkspaceManifestMappings(options) {
14968
14467
  warnings
14969
14468
  };
14970
14469
  }
14470
+ // src/mcp/server.ts
14471
+ import { EventsClient as EventsClient3, sanitizeChannelForOutput, sanitizeChannelsForOutput as sanitizeChannelsForOutput2 } from "@hasna/events";
14472
+
14971
14473
  // node_modules/zod/v4/core/core.js
14972
14474
  var NEVER2 = Object.freeze({
14973
14475
  status: "aborted"
@@ -23940,7 +23442,7 @@ function buildServer(version2 = getPackageVersion()) {
23940
23442
  }
23941
23443
  function createMcpServer(version2) {
23942
23444
  const server = new McpServer({ name: "machines", version: version2 });
23943
- const events = new EventsClient;
23445
+ const events = new EventsClient3;
23944
23446
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
23945
23447
  content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
23946
23448
  }));
@@ -24094,7 +23596,7 @@ function createMcpServer(version2) {
24094
23596
  }));
24095
23597
  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) }] }));
24096
23598
  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) }] }));
24097
- server.tool("machines_webhooks_add", "Add or replace a shared Hasna event webhook channel.", {
23599
+ server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
24098
23600
  channel_id: exports_external.string().describe("Channel identifier"),
24099
23601
  url: exports_external.string().url().describe("Webhook URL"),
24100
23602
  event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
@@ -24102,26 +23604,26 @@ function createMcpServer(version2) {
24102
23604
  secret: exports_external.string().optional().describe("Optional HMAC secret"),
24103
23605
  enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
24104
23606
  }, async ({ channel_id, url, event_type, source, secret, enabled }) => {
24105
- const now2 = new Date().toISOString();
23607
+ const now = new Date().toISOString();
24106
23608
  const channel = await events.addChannel({
24107
23609
  id: channel_id,
24108
23610
  enabled: enabled ?? true,
24109
23611
  transport: "webhook",
24110
23612
  filters: event_type || source ? [{ type: event_type, source }] : undefined,
24111
23613
  webhook: { url, secret },
24112
- createdAt: now2,
24113
- updatedAt: now2
23614
+ createdAt: now,
23615
+ updatedAt: now
24114
23616
  });
24115
23617
  return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
24116
23618
  });
24117
- server.tool("machines_webhooks_list", "List shared Hasna event webhook channels.", {}, async () => ({
24118
- content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput(await events.listChannels()), null, 2) }]
23619
+ server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
23620
+ content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
24119
23621
  }));
24120
- server.tool("machines_webhooks_test", "Send a test event to one shared Hasna 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 }) => ({
23622
+ 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 }) => ({
24121
23623
  content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
24122
23624
  }));
24123
- server.tool("machines_webhooks_remove", "Remove a shared Hasna 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) }] }));
24124
- server.tool("machines_events_emit", "Emit a shared Hasna event from machines.", {
23625
+ 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) }] }));
23626
+ server.tool("machines_events_emit", "Emit a shared event from machines.", {
24125
23627
  event_type: exports_external.string().describe("Event type"),
24126
23628
  subject: exports_external.string().optional().describe("Event subject"),
24127
23629
  severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
@@ -24142,10 +23644,10 @@ function createMcpServer(version2) {
24142
23644
  dedupeKey: dedupe_key
24143
23645
  }, { deliver: deliver !== false }), null, 2) }]
24144
23646
  }));
24145
- server.tool("machines_events_list", "List shared Hasna events.", {}, async () => ({
23647
+ server.tool("machines_events_list", "List shared events.", {}, async () => ({
24146
23648
  content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
24147
23649
  }));
24148
- server.tool("machines_events_replay", "Replay shared Hasna 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 }) => ({
23650
+ 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 }) => ({
24149
23651
  content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
24150
23652
  }));
24151
23653
  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) }] }));
@@ -24276,6 +23778,7 @@ export {
24276
23778
  STORAGE_TABLES,
24277
23779
  STORAGE_MODE_ENV,
24278
23780
  STORAGE_DATABASE_ENV,
23781
+ SCREEN_SECRET_NAMESPACE_ENV,
24279
23782
  MACHINE_MCP_TOOL_NAMES,
24280
23783
  MACHINES_STORAGE_TABLES,
24281
23784
  MACHINES_STORAGE_MODE_FALLBACK_ENV,