@hasna/machines 0.0.32 → 0.0.33

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/cli/index.js CHANGED
@@ -2082,34 +2082,34 @@ var require_commander = __commonJS((exports) => {
2082
2082
  });
2083
2083
 
2084
2084
  // src/paths.ts
2085
- import { existsSync as existsSync3, mkdirSync } from "fs";
2086
- import { dirname as dirname2, join as join3, resolve } from "path";
2085
+ import { existsSync as existsSync2, mkdirSync } from "fs";
2086
+ import { dirname as dirname2, join as join2, resolve } from "path";
2087
2087
  function homeDir() {
2088
2088
  return process.env["HOME"] || process.env["USERPROFILE"] || "~";
2089
2089
  }
2090
2090
  function getDataDir() {
2091
- return process.env["HASNA_MACHINES_DIR"] || join3(homeDir(), ".hasna", "machines");
2091
+ return process.env["HASNA_MACHINES_DIR"] || join2(homeDir(), ".hasna", "machines");
2092
2092
  }
2093
2093
  function getDbPath() {
2094
- return process.env["HASNA_MACHINES_DB_PATH"] || join3(getDataDir(), "machines.db");
2094
+ return process.env["HASNA_MACHINES_DB_PATH"] || join2(getDataDir(), "machines.db");
2095
2095
  }
2096
2096
  function getManifestPath() {
2097
- return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join3(getDataDir(), "machines.json");
2097
+ return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join2(getDataDir(), "machines.json");
2098
2098
  }
2099
2099
  function getNotificationsPath() {
2100
- return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join3(getDataDir(), "notifications.json");
2100
+ return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join2(getDataDir(), "notifications.json");
2101
2101
  }
2102
2102
  function getClipboardKeyPath() {
2103
- return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join3(getDataDir(), "clipboard.key");
2103
+ return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join2(getDataDir(), "clipboard.key");
2104
2104
  }
2105
2105
  function getClipboardHistoryPath() {
2106
- return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join3(getDataDir(), "clipboard-history.json");
2106
+ return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join2(getDataDir(), "clipboard-history.json");
2107
2107
  }
2108
2108
  function ensureParentDir(filePath) {
2109
2109
  if (filePath === ":memory:")
2110
2110
  return;
2111
2111
  const dir = dirname2(resolve(filePath));
2112
- if (!existsSync3(dir)) {
2112
+ if (!existsSync2(dir)) {
2113
2113
  mkdirSync(dir, { recursive: true });
2114
2114
  }
2115
2115
  }
@@ -2203,15 +2203,15 @@ function countRuns(table) {
2203
2203
  }
2204
2204
  function recordSetupRun(machineId, status, details) {
2205
2205
  const db = getDb();
2206
- const now2 = new Date().toISOString();
2206
+ const now = new Date().toISOString();
2207
2207
  db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
2208
- VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now2, now2);
2208
+ VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now, now);
2209
2209
  }
2210
2210
  function recordSyncRun(machineId, status, actions) {
2211
2211
  const db = getDb();
2212
- const now2 = new Date().toISOString();
2212
+ const now = new Date().toISOString();
2213
2213
  db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
2214
- VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now2, now2);
2214
+ VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
2215
2215
  }
2216
2216
  var adapter = null;
2217
2217
  var init_db = __esm(() => {
@@ -2479,7 +2479,7 @@ function upsertSqlite(db, table, columns, rows) {
2479
2479
  }
2480
2480
  function recordSyncMeta(db, direction, results) {
2481
2481
  ensureSyncMetaTable(db);
2482
- const now3 = new Date().toISOString();
2482
+ const now = new Date().toISOString();
2483
2483
  const statement = db.query(`
2484
2484
  INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
2485
2485
  VALUES (?, ?, ?)
@@ -2488,7 +2488,7 @@ function recordSyncMeta(db, direction, results) {
2488
2488
  for (const result of results) {
2489
2489
  if (result.errors.length > 0)
2490
2490
  continue;
2491
- statement.run(result.table, now3, direction);
2491
+ statement.run(result.table, now, direction);
2492
2492
  }
2493
2493
  }
2494
2494
  function ensureSyncMetaTable(db) {
@@ -2601,685 +2601,8 @@ var {
2601
2601
  Help
2602
2602
  } = import__.default;
2603
2603
 
2604
- // node_modules/@hasna/events/dist/commander.js
2605
- import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
2606
- import { existsSync } from "fs";
2607
- import { homedir } from "os";
2608
- import { join } from "path";
2609
- import { createHmac, timingSafeEqual } from "crypto";
2610
- import { randomUUID } from "crypto";
2611
- import { spawn } from "child_process";
2612
- import { randomUUID as randomUUID2 } from "crypto";
2613
- function getPathValue(input, path) {
2614
- return path.split(".").reduce((value, part) => {
2615
- if (value && typeof value === "object" && part in value) {
2616
- return value[part];
2617
- }
2618
- return;
2619
- }, input);
2620
- }
2621
- function wildcardToRegExp(pattern) {
2622
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
2623
- return new RegExp(`^${escaped}$`);
2624
- }
2625
- function matchString(value, matcher) {
2626
- if (matcher === undefined)
2627
- return true;
2628
- if (value === undefined)
2629
- return false;
2630
- const matchers = Array.isArray(matcher) ? matcher : [matcher];
2631
- return matchers.some((item) => wildcardToRegExp(item).test(value));
2632
- }
2633
- function matchRecord(input, matcher) {
2634
- if (!matcher)
2635
- return true;
2636
- return Object.entries(matcher).every(([path, expected]) => {
2637
- const actual = getPathValue(input, path);
2638
- if (typeof expected === "string" || Array.isArray(expected)) {
2639
- return matchString(actual === undefined ? undefined : String(actual), expected);
2640
- }
2641
- return actual === expected;
2642
- });
2643
- }
2644
- function eventMatchesFilter(event, filter) {
2645
- 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);
2646
- }
2647
- function channelMatchesEvent(channel, event) {
2648
- if (!channel.enabled)
2649
- return false;
2650
- if (!channel.filters || channel.filters.length === 0)
2651
- return true;
2652
- return channel.filters.some((filter) => eventMatchesFilter(event, filter));
2653
- }
2654
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
2655
- var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
2656
- function getEventsDataDir(override) {
2657
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
2658
- }
2659
-
2660
- class JsonEventsStore {
2661
- dataDir;
2662
- channelsPath;
2663
- eventsPath;
2664
- deliveriesPath;
2665
- constructor(dataDir = getEventsDataDir()) {
2666
- this.dataDir = dataDir;
2667
- this.channelsPath = join(dataDir, "channels.json");
2668
- this.eventsPath = join(dataDir, "events.json");
2669
- this.deliveriesPath = join(dataDir, "deliveries.json");
2670
- }
2671
- async init() {
2672
- await mkdir(this.dataDir, { recursive: true, mode: 448 });
2673
- await chmod(this.dataDir, 448).catch(() => {
2674
- return;
2675
- });
2676
- await this.ensureArrayFile(this.channelsPath);
2677
- await this.ensureArrayFile(this.eventsPath);
2678
- await this.ensureArrayFile(this.deliveriesPath);
2679
- }
2680
- async addChannel(channel) {
2681
- await this.init();
2682
- const channels = await this.readJson(this.channelsPath, []);
2683
- const index = channels.findIndex((item) => item.id === channel.id);
2684
- if (index >= 0) {
2685
- channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
2686
- } else {
2687
- channels.push(channel);
2688
- }
2689
- await this.writeJson(this.channelsPath, channels);
2690
- return index >= 0 ? channels[index] : channel;
2691
- }
2692
- async listChannels() {
2693
- await this.init();
2694
- return this.readJson(this.channelsPath, []);
2695
- }
2696
- async getChannel(id) {
2697
- const channels = await this.listChannels();
2698
- return channels.find((channel) => channel.id === id);
2699
- }
2700
- async removeChannel(id) {
2701
- await this.init();
2702
- const channels = await this.readJson(this.channelsPath, []);
2703
- const next = channels.filter((channel) => channel.id !== id);
2704
- await this.writeJson(this.channelsPath, next);
2705
- return next.length !== channels.length;
2706
- }
2707
- async appendEvent(event) {
2708
- await this.init();
2709
- const events = await this.readJson(this.eventsPath, []);
2710
- events.push(event);
2711
- await this.writeJson(this.eventsPath, events);
2712
- return event;
2713
- }
2714
- async listEvents() {
2715
- await this.init();
2716
- return this.readJson(this.eventsPath, []);
2717
- }
2718
- async findEventByIdentity(identity) {
2719
- const events = await this.listEvents();
2720
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
2721
- }
2722
- async appendDelivery(result) {
2723
- await this.init();
2724
- const deliveries = await this.readJson(this.deliveriesPath, []);
2725
- deliveries.push(result);
2726
- await this.writeJson(this.deliveriesPath, deliveries);
2727
- return result;
2728
- }
2729
- async listDeliveries() {
2730
- await this.init();
2731
- return this.readJson(this.deliveriesPath, []);
2732
- }
2733
- async exportData() {
2734
- return {
2735
- channels: await this.listChannels(),
2736
- events: await this.listEvents(),
2737
- deliveries: await this.listDeliveries()
2738
- };
2739
- }
2740
- async ensureArrayFile(path) {
2741
- if (!existsSync(path)) {
2742
- await writeFile(path, `[]
2743
- `, { encoding: "utf-8", mode: 384 });
2744
- }
2745
- await chmod(path, 384).catch(() => {
2746
- return;
2747
- });
2748
- }
2749
- async readJson(path, fallback) {
2750
- try {
2751
- const raw = await readFile(path, "utf-8");
2752
- if (!raw.trim())
2753
- return fallback;
2754
- return JSON.parse(raw);
2755
- } catch (error) {
2756
- if (error.code === "ENOENT")
2757
- return fallback;
2758
- throw error;
2759
- }
2760
- }
2761
- async writeJson(path, value) {
2762
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
2763
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
2764
- `, { encoding: "utf-8", mode: 384 });
2765
- await rename(tempPath, path);
2766
- await chmod(path, 384).catch(() => {
2767
- return;
2768
- });
2769
- }
2770
- }
2771
- var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
2772
- function buildSignatureBase(timestamp, body) {
2773
- return `${timestamp}.${body}`;
2774
- }
2775
- function signPayload(secret, timestamp, body) {
2776
- const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
2777
- return `sha256=${digest}`;
2778
- }
2779
- function now() {
2780
- return new Date().toISOString();
2781
- }
2782
- function truncate(value, max = 4096) {
2783
- return value.length > max ? `${value.slice(0, max)}...` : value;
2784
- }
2785
- function buildWebhookRequest(event, channel) {
2786
- if (!channel.webhook)
2787
- throw new Error(`Channel ${channel.id} has no webhook config`);
2788
- const body = JSON.stringify(event);
2789
- const timestamp = event.time;
2790
- const headers = {
2791
- "Content-Type": "application/json",
2792
- "User-Agent": "@hasna/events",
2793
- "X-Hasna-Event-Id": event.id,
2794
- "X-Hasna-Event-Type": event.type,
2795
- "X-Hasna-Timestamp": timestamp,
2796
- ...channel.webhook.headers
2797
- };
2798
- if (channel.webhook.secret) {
2799
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
2800
- }
2801
- return { body, headers };
2802
- }
2803
- async function dispatchWebhook(event, channel, options = {}) {
2804
- if (!channel.webhook)
2805
- throw new Error(`Channel ${channel.id} has no webhook config`);
2806
- const startedAt = now();
2807
- const { body, headers } = buildWebhookRequest(event, channel);
2808
- const controller = new AbortController;
2809
- const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
2810
- try {
2811
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
2812
- method: "POST",
2813
- headers,
2814
- body,
2815
- signal: controller.signal
2816
- });
2817
- const responseBody = truncate(await response.text());
2818
- return {
2819
- attempt: 1,
2820
- status: response.ok ? "success" : "failed",
2821
- startedAt,
2822
- completedAt: now(),
2823
- responseStatus: response.status,
2824
- responseBody,
2825
- error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
2826
- };
2827
- } catch (error) {
2828
- return {
2829
- attempt: 1,
2830
- status: "failed",
2831
- startedAt,
2832
- completedAt: now(),
2833
- error: error instanceof Error ? error.message : String(error)
2834
- };
2835
- } finally {
2836
- clearTimeout(timeout);
2837
- }
2838
- }
2839
- async function dispatchCommand(event, channel) {
2840
- if (!channel.command)
2841
- throw new Error(`Channel ${channel.id} has no command config`);
2842
- const startedAt = now();
2843
- const eventJson = JSON.stringify(event);
2844
- const env = {
2845
- ...process.env,
2846
- ...channel.command.env,
2847
- HASNA_CHANNEL_ID: channel.id,
2848
- HASNA_EVENT_ID: event.id,
2849
- HASNA_EVENT_TYPE: event.type,
2850
- HASNA_EVENT_SOURCE: event.source,
2851
- HASNA_EVENT_SUBJECT: event.subject ?? "",
2852
- HASNA_EVENT_SEVERITY: event.severity,
2853
- HASNA_EVENT_TIME: event.time,
2854
- HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
2855
- HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
2856
- HASNA_EVENT_JSON: eventJson
2857
- };
2858
- return new Promise((resolve) => {
2859
- const child = spawn(channel.command.command, channel.command.args ?? [], {
2860
- cwd: channel.command.cwd,
2861
- env,
2862
- stdio: ["pipe", "pipe", "pipe"]
2863
- });
2864
- let stdout = "";
2865
- let stderr = "";
2866
- const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
2867
- child.stdin.end(eventJson);
2868
- child.stdout.on("data", (chunk) => {
2869
- stdout += chunk.toString();
2870
- });
2871
- child.stderr.on("data", (chunk) => {
2872
- stderr += chunk.toString();
2873
- });
2874
- child.on("error", (error) => {
2875
- clearTimeout(timeout);
2876
- resolve({
2877
- attempt: 1,
2878
- status: "failed",
2879
- startedAt,
2880
- completedAt: now(),
2881
- stdout: truncate(stdout),
2882
- stderr: truncate(stderr),
2883
- error: error.message
2884
- });
2885
- });
2886
- child.on("close", (code, signal) => {
2887
- clearTimeout(timeout);
2888
- const success = code === 0;
2889
- resolve({
2890
- attempt: 1,
2891
- status: success ? "success" : "failed",
2892
- startedAt,
2893
- completedAt: now(),
2894
- stdout: truncate(stdout),
2895
- stderr: truncate(stderr),
2896
- error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
2897
- });
2898
- });
2899
- });
2900
- }
2901
- async function dispatchChannel(event, channel, options = {}) {
2902
- if (channel.transport === "webhook")
2903
- return dispatchWebhook(event, channel, options);
2904
- if (channel.transport === "command")
2905
- return dispatchCommand(event, channel);
2906
- return {
2907
- attempt: 1,
2908
- status: "skipped",
2909
- startedAt: now(),
2910
- completedAt: now(),
2911
- error: `Unsupported transport: ${channel.transport}`
2912
- };
2913
- }
2914
- function createDeliveryResult(event, channel, attempts) {
2915
- const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
2916
- return {
2917
- id: randomUUID(),
2918
- eventId: event.id,
2919
- channelId: channel.id,
2920
- transport: channel.transport,
2921
- status,
2922
- attempts,
2923
- createdAt: attempts[0]?.startedAt ?? now(),
2924
- completedAt: attempts.at(-1)?.completedAt ?? now()
2925
- };
2926
- }
2927
- function createEvent(input) {
2928
- return {
2929
- id: input.id ?? randomUUID2(),
2930
- source: input.source,
2931
- type: input.type,
2932
- time: normalizeTime(input.time),
2933
- subject: input.subject,
2934
- severity: input.severity ?? "info",
2935
- data: input.data ?? {},
2936
- message: input.message,
2937
- dedupeKey: input.dedupeKey,
2938
- schemaVersion: input.schemaVersion ?? "1.0",
2939
- metadata: input.metadata ?? {}
2940
- };
2941
- }
2942
-
2943
- class EventsClient {
2944
- store;
2945
- redactors;
2946
- transportOptions;
2947
- constructor(options = {}) {
2948
- this.store = options.store ?? new JsonEventsStore(options.dataDir);
2949
- this.redactors = options.redactors ?? [];
2950
- this.transportOptions = { fetchImpl: options.fetchImpl };
2951
- }
2952
- async addChannel(input) {
2953
- const timestamp = new Date().toISOString();
2954
- return this.store.addChannel({
2955
- ...input,
2956
- createdAt: input.createdAt ?? timestamp,
2957
- updatedAt: input.updatedAt ?? timestamp
2958
- });
2959
- }
2960
- async listChannels() {
2961
- return this.store.listChannels();
2962
- }
2963
- async removeChannel(id) {
2964
- return this.store.removeChannel(id);
2965
- }
2966
- async emit(input, options = {}) {
2967
- const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
2968
- if (options.dedupe !== false) {
2969
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
2970
- if (existing) {
2971
- return { event: existing, deliveries: [], deduped: true };
2972
- }
2973
- }
2974
- await this.store.appendEvent(event);
2975
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
2976
- return { event, deliveries, deduped: false };
2977
- }
2978
- async listEvents() {
2979
- return this.store.listEvents();
2980
- }
2981
- async listDeliveries() {
2982
- return this.store.listDeliveries();
2983
- }
2984
- async deliver(event) {
2985
- const channels = await this.store.listChannels();
2986
- const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
2987
- const deliveries = [];
2988
- for (const channel of selected) {
2989
- const eventForChannel = await this.applyRedaction(event, channel);
2990
- const result = await this.deliverWithRetry(eventForChannel, channel);
2991
- await this.store.appendDelivery(result);
2992
- deliveries.push(result);
2993
- }
2994
- return deliveries;
2995
- }
2996
- async testChannel(id, input = {}) {
2997
- const channel = await this.store.getChannel(id);
2998
- if (!channel)
2999
- throw new Error(`Channel not found: ${id}`);
3000
- const event = createEvent({
3001
- source: input.source ?? "hasna.events",
3002
- type: input.type ?? "events.test",
3003
- subject: input.subject ?? id,
3004
- severity: input.severity ?? "info",
3005
- data: input.data ?? { test: true },
3006
- message: input.message ?? "Hasna events test delivery",
3007
- dedupeKey: input.dedupeKey,
3008
- schemaVersion: input.schemaVersion,
3009
- metadata: input.metadata,
3010
- time: input.time,
3011
- id: input.id
3012
- });
3013
- const eventForChannel = await this.applyRedaction(event, channel);
3014
- const result = await this.deliverWithRetry(eventForChannel, channel);
3015
- await this.store.appendDelivery(result);
3016
- return result;
3017
- }
3018
- async replay(options = {}) {
3019
- const events = (await this.store.listEvents()).filter((event) => {
3020
- if (options.eventId && event.id !== options.eventId)
3021
- return false;
3022
- if (options.source && event.source !== options.source)
3023
- return false;
3024
- if (options.type && event.type !== options.type)
3025
- return false;
3026
- return true;
3027
- });
3028
- if (options.dryRun)
3029
- return { events, deliveries: [] };
3030
- const deliveries = [];
3031
- for (const event of events) {
3032
- deliveries.push(...await this.deliver(event));
3033
- }
3034
- return { events, deliveries };
3035
- }
3036
- async applyRedaction(event, channel) {
3037
- let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
3038
- for (const redactor of this.redactors) {
3039
- next = await redactor(next, channel);
3040
- }
3041
- return next;
3042
- }
3043
- async deliverWithRetry(event, channel) {
3044
- const policy = normalizeRetryPolicy(channel.retry);
3045
- const attempts = [];
3046
- for (let index = 0;index < policy.maxAttempts; index += 1) {
3047
- const attempt = await dispatchChannel(event, channel, this.transportOptions);
3048
- attempt.attempt = index + 1;
3049
- if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
3050
- attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
3051
- }
3052
- attempts.push(attempt);
3053
- if (attempt.status !== "failed")
3054
- break;
3055
- if (attempt.nextBackoffMs)
3056
- await Bun.sleep(attempt.nextBackoffMs);
3057
- }
3058
- return createDeliveryResult(event, channel, attempts);
3059
- }
3060
- }
3061
- function redactPaths(event, paths, replacement = "[REDACTED]") {
3062
- if (paths.length === 0)
3063
- return event;
3064
- const copy = structuredClone(event);
3065
- for (const path of paths) {
3066
- setPath(copy, path, replacement);
3067
- }
3068
- return copy;
3069
- }
3070
- function sanitizeChannelForOutput(channel) {
3071
- const copy = structuredClone(channel);
3072
- if (copy.webhook?.secret)
3073
- copy.webhook.secret = "[REDACTED]";
3074
- if (copy.command?.env) {
3075
- copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
3076
- }
3077
- return copy;
3078
- }
3079
- function sanitizeChannelsForOutput(channels) {
3080
- return channels.map(sanitizeChannelForOutput);
3081
- }
3082
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
3083
- return redactValue(event, replacement);
3084
- }
3085
- function shouldRedactKey(key) {
3086
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
3087
- }
3088
- function redactValue(value, replacement) {
3089
- if (Array.isArray(value))
3090
- return value.map((item) => redactValue(item, replacement));
3091
- if (!value || typeof value !== "object")
3092
- return value;
3093
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
3094
- key,
3095
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
3096
- ]));
3097
- }
3098
- function setPath(input, path, replacement) {
3099
- const parts = path.split(".");
3100
- let cursor = input;
3101
- for (const part of parts.slice(0, -1)) {
3102
- const next = cursor[part];
3103
- if (!next || typeof next !== "object")
3104
- return;
3105
- cursor = next;
3106
- }
3107
- const last = parts.at(-1);
3108
- if (last && last in cursor)
3109
- cursor[last] = replacement;
3110
- }
3111
- function normalizeTime(value) {
3112
- if (!value)
3113
- return new Date().toISOString();
3114
- return value instanceof Date ? value.toISOString() : value;
3115
- }
3116
- function normalizeRetryPolicy(policy) {
3117
- return {
3118
- maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
3119
- backoffMs: Math.max(0, policy?.backoffMs ?? 250),
3120
- multiplier: Math.max(1, policy?.multiplier ?? 2)
3121
- };
3122
- }
3123
- function parseJsonObject(value, fallback) {
3124
- if (!value)
3125
- return fallback;
3126
- const parsed = JSON.parse(value);
3127
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3128
- throw new Error("Expected a JSON object");
3129
- }
3130
- return parsed;
3131
- }
3132
- function parseHeaders(values) {
3133
- if (!values?.length)
3134
- return;
3135
- const headers = {};
3136
- for (const value of values) {
3137
- const separator = value.indexOf("=");
3138
- if (separator === -1)
3139
- throw new Error(`Invalid header, expected name=value: ${value}`);
3140
- headers[value.slice(0, separator)] = value.slice(separator + 1);
3141
- }
3142
- return headers;
3143
- }
3144
- function parseFilter(options) {
3145
- const filter2 = {};
3146
- if (options.source)
3147
- filter2.source = options.source;
3148
- if (options.type)
3149
- filter2.type = options.type;
3150
- if (options.subject)
3151
- filter2.subject = options.subject;
3152
- if (options.severity)
3153
- filter2.severity = options.severity;
3154
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
3155
- }
3156
- function createClient(options) {
3157
- if (options.createClient)
3158
- return options.createClient();
3159
- return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
3160
- }
3161
- function print(value, json, text) {
3162
- if (json)
3163
- console.log(JSON.stringify(value, null, 2));
3164
- else
3165
- console.log(text);
3166
- }
3167
- function registerWebhookCommands(program2, options) {
3168
- const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
3169
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions) => {
3170
- const timestamp = new Date().toISOString();
3171
- const channel = {
3172
- id: actionOptions.id,
3173
- name: actionOptions.name,
3174
- enabled: !actionOptions.disabled,
3175
- transport: actionOptions.transport,
3176
- filters: parseFilter(actionOptions),
3177
- retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
3178
- redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
3179
- createdAt: timestamp,
3180
- updatedAt: timestamp
3181
- };
3182
- if (actionOptions.transport === "webhook") {
3183
- channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
3184
- } else if (actionOptions.transport === "command") {
3185
- channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
3186
- } else {
3187
- throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
3188
- }
3189
- const saved = await createClient(options).addChannel(channel);
3190
- print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
3191
- });
3192
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
3193
- const channels = await createClient(options).listChannels();
3194
- if (actionOptions.json) {
3195
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
3196
- return;
3197
- }
3198
- if (!channels.length) {
3199
- console.log("No channels configured.");
3200
- return;
3201
- }
3202
- for (const channel of channels) {
3203
- console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
3204
- }
3205
- });
3206
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
3207
- const removed = await createClient(options).removeChannel(id);
3208
- print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
3209
- });
3210
- webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
3211
- const result = await createClient(options).testChannel(id, {
3212
- source: options.source,
3213
- type: actionOptions.type,
3214
- subject: actionOptions.subject ?? id,
3215
- message: actionOptions.message,
3216
- data: parseJsonObject(actionOptions.data, { test: true })
3217
- });
3218
- print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
3219
- });
3220
- return webhooks;
3221
- }
3222
- function registerEventCommands(program2, options) {
3223
- const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
3224
- events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
3225
- const result = await createClient(options).emit({
3226
- source: actionOptions.source ?? options.source,
3227
- type,
3228
- subject: actionOptions.subject,
3229
- severity: actionOptions.severity,
3230
- message: actionOptions.message,
3231
- dedupeKey: actionOptions.dedupeKey,
3232
- data: parseJsonObject(actionOptions.data, {}),
3233
- metadata: parseJsonObject(actionOptions.metadata, {})
3234
- }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
3235
- print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
3236
- });
3237
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
3238
- let rows = await createClient(options).listEvents();
3239
- if (actionOptions.source)
3240
- rows = rows.filter((event) => event.source === actionOptions.source);
3241
- if (actionOptions.type)
3242
- rows = rows.filter((event) => event.type === actionOptions.type);
3243
- if (actionOptions.limit)
3244
- rows = rows.slice(-actionOptions.limit);
3245
- if (actionOptions.json) {
3246
- console.log(JSON.stringify(rows, null, 2));
3247
- return;
3248
- }
3249
- if (!rows.length) {
3250
- console.log("No events recorded.");
3251
- return;
3252
- }
3253
- for (const event of rows)
3254
- console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
3255
- });
3256
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
3257
- const result = await createClient(options).replay({
3258
- eventId: actionOptions.id,
3259
- source: actionOptions.source,
3260
- type: actionOptions.type,
3261
- dryRun: actionOptions.dryRun
3262
- });
3263
- print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
3264
- });
3265
- return events;
3266
- }
3267
- function registerEventsCommands(program2, options) {
3268
- registerWebhookCommands(program2, options);
3269
- registerEventCommands(program2, options);
3270
- }
3271
- function parseNumber(value) {
3272
- const parsed = Number(value);
3273
- if (!Number.isFinite(parsed))
3274
- throw new Error(`Expected a number, got ${value}`);
3275
- return parsed;
3276
- }
3277
- function collectValues(value, previous) {
3278
- previous.push(value);
3279
- return previous;
3280
- }
3281
-
3282
2604
  // src/cli/index.ts
2605
+ import { registerEventCommands, registerWebhookCommands } from "@hasna/events/commander";
3283
2606
  import { execFileSync } from "child_process";
3284
2607
 
3285
2608
  // node_modules/chalk/source/vendor/ansi-styles/index.js
@@ -3772,14 +3095,14 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
3772
3095
  var source_default = chalk;
3773
3096
 
3774
3097
  // src/version.ts
3775
- import { existsSync as existsSync2, readFileSync } from "fs";
3776
- import { dirname, join as join2 } from "path";
3098
+ import { existsSync, readFileSync } from "fs";
3099
+ import { dirname, join } from "path";
3777
3100
  import { fileURLToPath } from "url";
3778
3101
  function getPackageVersion() {
3779
3102
  try {
3780
3103
  const here = dirname(fileURLToPath(import.meta.url));
3781
- const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
3782
- const pkgPath = candidates.find((candidate) => existsSync2(candidate));
3104
+ const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
3105
+ const pkgPath = candidates.find((candidate) => existsSync(candidate));
3783
3106
  if (!pkgPath) {
3784
3107
  return "0.0.0";
3785
3108
  }
@@ -3790,8 +3113,8 @@ function getPackageVersion() {
3790
3113
  }
3791
3114
 
3792
3115
  // src/manifests.ts
3793
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
3794
- import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
3116
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
3117
+ import { arch, homedir, hostname, platform, userInfo } from "os";
3795
3118
  import { dirname as dirname3 } from "path";
3796
3119
 
3797
3120
  // node_modules/zod/v3/external.js
@@ -7805,7 +7128,7 @@ var fleetSchema = exports_external.object({
7805
7128
  machines: exports_external.array(machineSchema)
7806
7129
  });
7807
7130
  function detectWorkspacePath() {
7808
- const home = homedir2();
7131
+ const home = homedir();
7809
7132
  if (platform() === "darwin") {
7810
7133
  return `${home}/Workspace`;
7811
7134
  }
@@ -7829,7 +7152,7 @@ function getDefaultManifest() {
7829
7152
  };
7830
7153
  }
7831
7154
  function readManifest(path = getManifestPath()) {
7832
- if (!existsSync4(path)) {
7155
+ if (!existsSync3(path)) {
7833
7156
  return getDefaultManifest();
7834
7157
  }
7835
7158
  const raw = JSON.parse(readFileSync2(path, "utf8"));
@@ -7910,7 +7233,7 @@ function manifestValidate() {
7910
7233
  }
7911
7234
 
7912
7235
  // src/commands/setup.ts
7913
- import { homedir as homedir3 } from "os";
7236
+ import { homedir as homedir2 } from "os";
7914
7237
  init_db();
7915
7238
 
7916
7239
  // src/remote.ts
@@ -7920,7 +7243,7 @@ import { hostname as hostname4 } from "os";
7920
7243
 
7921
7244
  // src/topology.ts
7922
7245
  init_db();
7923
- import { existsSync as existsSync5 } from "fs";
7246
+ import { existsSync as existsSync4 } from "fs";
7924
7247
  import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
7925
7248
  import { spawnSync } from "child_process";
7926
7249
  init_paths();
@@ -8111,7 +7434,7 @@ function buildEntry(input) {
8111
7434
  };
8112
7435
  }
8113
7436
  function discoverMachineTopology(options = {}) {
8114
- const now2 = options.now ?? new Date;
7437
+ const now = options.now ?? new Date;
8115
7438
  const runner = options.runner ?? defaultRunner;
8116
7439
  const warnings = [];
8117
7440
  const manifest = readManifest();
@@ -8143,11 +7466,11 @@ function discoverMachineTopology(options = {}) {
8143
7466
  version: getPackageVersion()
8144
7467
  },
8145
7468
  capabilities: getMachinesConsumerCapabilities(),
8146
- generated_at: now2.toISOString(),
7469
+ generated_at: now.toISOString(),
8147
7470
  local_machine_id: localMachineId,
8148
7471
  local_hostname: hostname3(),
8149
7472
  current_platform: normalizePlatform2(),
8150
- manifest_path_known: existsSync5(getManifestPath()),
7473
+ manifest_path_known: existsSync4(getManifestPath()),
8151
7474
  machines,
8152
7475
  warnings
8153
7476
  };
@@ -8266,11 +7589,11 @@ function cacheability(input) {
8266
7589
  };
8267
7590
  }
8268
7591
  function resolveMachineRoute(machineId, options = {}) {
8269
- const now2 = options.now ?? new Date;
7592
+ const now = options.now ?? new Date;
8270
7593
  const topology = options.topology ?? discoverMachineTopology(options);
8271
7594
  const warnings = [...topology.warnings];
8272
7595
  const { machine, matchedBy } = findRouteMachine(topology, machineId);
8273
- const generatedAt = now2.toISOString();
7596
+ const generatedAt = now.toISOString();
8274
7597
  if (!machine) {
8275
7598
  warnings.push(`machine_not_found:${machineId}`);
8276
7599
  return {
@@ -8296,8 +7619,8 @@ function resolveMachineRoute(machineId, options = {}) {
8296
7619
  },
8297
7620
  cacheability: cacheability({
8298
7621
  ok: false,
8299
- observedAt: now2,
8300
- now: now2,
7622
+ observedAt: now,
7623
+ now,
8301
7624
  ttlMs: options.resolverTtlMs,
8302
7625
  authority: "unresolved",
8303
7626
  confidence: "none",
@@ -8334,8 +7657,8 @@ function resolveMachineRoute(machineId, options = {}) {
8334
7657
  },
8335
7658
  cacheability: cacheability({
8336
7659
  ok,
8337
- observedAt: now2,
8338
- now: now2,
7660
+ observedAt: now,
7661
+ now,
8339
7662
  ttlMs: options.resolverTtlMs,
8340
7663
  authority: routeAuthority({ machine, selectedHint, matchedBy }),
8341
7664
  confidence,
@@ -8422,7 +7745,7 @@ function canCheckPathForMachine(machine, localMachineId) {
8422
7745
  function checkedPathExists(path, check) {
8423
7746
  if (!path || !check)
8424
7747
  return null;
8425
- return existsSync5(path);
7748
+ return existsSync4(path);
8426
7749
  }
8427
7750
  function repairHint(input) {
8428
7751
  const command = [
@@ -8637,11 +7960,11 @@ function metadataKeysForDiagnostics(metadata) {
8637
7960
  return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
8638
7961
  }
8639
7962
  function resolveMachineWorkspace(options) {
8640
- const now2 = options.now ?? new Date;
7963
+ const now = options.now ?? new Date;
8641
7964
  const topology = options.topology ?? discoverMachineTopology(options);
8642
7965
  const warnings = [...topology.warnings];
8643
7966
  const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
8644
- const generatedAt = now2.toISOString();
7967
+ const generatedAt = now.toISOString();
8645
7968
  const repoName = options.repoName ?? options.projectId;
8646
7969
  const openFilesRepoName = options.openFilesRepoName ?? "open-files";
8647
7970
  if (!machine) {
@@ -8670,8 +7993,8 @@ function resolveMachineWorkspace(options) {
8670
7993
  },
8671
7994
  cacheability: cacheability({
8672
7995
  ok: false,
8673
- observedAt: now2,
8674
- now: now2,
7996
+ observedAt: now,
7997
+ now,
8675
7998
  ttlMs: options.resolverTtlMs,
8676
7999
  authority: "unresolved",
8677
8000
  confidence: "none",
@@ -8743,8 +8066,8 @@ function resolveMachineWorkspace(options) {
8743
8066
  },
8744
8067
  cacheability: cacheability({
8745
8068
  ok: workspaceOk,
8746
- observedAt: now2,
8747
- now: now2,
8069
+ observedAt: now,
8070
+ now,
8748
8071
  ttlMs: options.resolverTtlMs,
8749
8072
  authority: workspaceAuthority(workspacePaths),
8750
8073
  confidence: workspaceOk ? "medium" : "none",
@@ -8913,7 +8236,7 @@ function buildSetupPlan(machineId) {
8913
8236
  const target = selected || {
8914
8237
  id: currentMachineId,
8915
8238
  platform: "linux",
8916
- workspacePath: `${homedir3()}/workspace`
8239
+ workspacePath: `${homedir2()}/workspace`
8917
8240
  };
8918
8241
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
8919
8242
  return {
@@ -8958,8 +8281,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
8958
8281
  }
8959
8282
 
8960
8283
  // src/commands/backup.ts
8961
- import { homedir as homedir4, hostname as hostname5 } from "os";
8962
- import { join as join4 } from "path";
8284
+ import { homedir as homedir3, hostname as hostname5 } from "os";
8285
+ import { join as join3 } from "path";
8963
8286
  var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
8964
8287
  var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
8965
8288
  var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
@@ -9007,16 +8330,16 @@ function resolveBackupTarget(options = {}) {
9007
8330
  };
9008
8331
  }
9009
8332
  function defaultBackupSources() {
9010
- const home = homedir4();
8333
+ const home = homedir3();
9011
8334
  return [
9012
- join4(home, ".hasna"),
9013
- join4(home, ".ssh"),
9014
- join4(home, ".secrets")
8335
+ join3(home, ".hasna"),
8336
+ join3(home, ".ssh"),
8337
+ join3(home, ".secrets")
9015
8338
  ];
9016
8339
  }
9017
8340
  function buildBackupPlan(bucket, prefix) {
9018
8341
  const target = resolveBackupTarget({ bucket, prefix });
9019
- const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
8342
+ const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
9020
8343
  const sources = defaultBackupSources();
9021
8344
  const steps = [
9022
8345
  {
@@ -9067,21 +8390,21 @@ function runBackup(bucket, prefix, options = {}) {
9067
8390
  }
9068
8391
 
9069
8392
  // src/commands/cert.ts
9070
- import { homedir as homedir5, platform as platform3 } from "os";
9071
- import { join as join5 } from "path";
8393
+ import { homedir as homedir4, platform as platform3 } from "os";
8394
+ import { join as join4 } from "path";
9072
8395
  function quote3(value) {
9073
8396
  return `'${value.replace(/'/g, `'\\''`)}'`;
9074
8397
  }
9075
8398
  function certDir() {
9076
- return join5(homedir5(), ".hasna", "machines", "certs");
8399
+ return join4(homedir4(), ".hasna", "machines", "certs");
9077
8400
  }
9078
8401
  function buildCertPlan(domains) {
9079
8402
  if (domains.length === 0) {
9080
8403
  throw new Error("At least one domain is required.");
9081
8404
  }
9082
8405
  const primary = domains[0];
9083
- const certPath = join5(certDir(), `${primary}.pem`);
9084
- const keyPath = join5(certDir(), `${primary}-key.pem`);
8406
+ const certPath = join4(certDir(), `${primary}.pem`);
8407
+ const keyPath = join4(certDir(), `${primary}-key.pem`);
9085
8408
  const steps = [];
9086
8409
  if (platform3() === "darwin") {
9087
8410
  steps.push({
@@ -9146,14 +8469,14 @@ function runCertPlan(domains, options = {}) {
9146
8469
 
9147
8470
  // src/commands/dns.ts
9148
8471
  init_paths();
9149
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
9150
- import { join as join6 } from "path";
8472
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8473
+ import { join as join5 } from "path";
9151
8474
  function getDnsPath() {
9152
- return join6(getDataDir(), "dns.json");
8475
+ return join5(getDataDir(), "dns.json");
9153
8476
  }
9154
8477
  function readMappings() {
9155
8478
  const path = getDnsPath();
9156
- if (!existsSync6(path))
8479
+ if (!existsSync5(path))
9157
8480
  return [];
9158
8481
  return JSON.parse(readFileSync3(path, "utf8"));
9159
8482
  }
@@ -9182,10 +8505,10 @@ function renderDomainMapping(domain) {
9182
8505
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
9183
8506
  caddySnippet: `${entry.domain} {
9184
8507
  reverse_proxy 127.0.0.1:${entry.port}
9185
- tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
8508
+ tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
9186
8509
  }`,
9187
- certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
9188
- keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
8510
+ certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
8511
+ keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
9189
8512
  };
9190
8513
  }
9191
8514
 
@@ -9542,7 +8865,7 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
9542
8865
  }
9543
8866
 
9544
8867
  // src/commands/notifications.ts
9545
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
8868
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
9546
8869
  init_paths();
9547
8870
  var notificationChannelSchema = exports_external.object({
9548
8871
  id: exports_external.string(),
@@ -9625,7 +8948,7 @@ ${message}
9625
8948
  }
9626
8949
  throw new Error("No local email transport available. Install sendmail or mail.");
9627
8950
  }
9628
- async function dispatchWebhook2(channel, event, message) {
8951
+ async function dispatchWebhook(channel, event, message) {
9629
8952
  const response = await fetch(channel.target, {
9630
8953
  method: "POST",
9631
8954
  headers: {
@@ -9650,7 +8973,7 @@ async function dispatchWebhook2(channel, event, message) {
9650
8973
  detail: `Webhook accepted with HTTP ${response.status}`
9651
8974
  };
9652
8975
  }
9653
- async function dispatchCommand2(channel, event, message) {
8976
+ async function dispatchCommand(channel, event, message) {
9654
8977
  const result = Bun.spawnSync(["bash", "-lc", channel.target], {
9655
8978
  stdout: "pipe",
9656
8979
  stderr: "pipe",
@@ -9673,7 +8996,7 @@ async function dispatchCommand2(channel, event, message) {
9673
8996
  detail: stdout || "Command completed successfully"
9674
8997
  };
9675
8998
  }
9676
- async function dispatchChannel2(channel, event, message) {
8999
+ async function dispatchChannel(channel, event, message) {
9677
9000
  if (!channel.enabled) {
9678
9001
  return {
9679
9002
  channelId: channel.id,
@@ -9687,9 +9010,9 @@ async function dispatchChannel2(channel, event, message) {
9687
9010
  return dispatchEmail(channel, event, message);
9688
9011
  }
9689
9012
  if (channel.type === "webhook") {
9690
- return dispatchWebhook2(channel, event, message);
9013
+ return dispatchWebhook(channel, event, message);
9691
9014
  }
9692
- return dispatchCommand2(channel, event, message);
9015
+ return dispatchCommand(channel, event, message);
9693
9016
  }
9694
9017
  function getDefaultNotificationConfig() {
9695
9018
  return {
@@ -9699,7 +9022,7 @@ function getDefaultNotificationConfig() {
9699
9022
  };
9700
9023
  }
9701
9024
  function readNotificationConfig(path = getNotificationsPath()) {
9702
- if (!existsSync7(path)) {
9025
+ if (!existsSync6(path)) {
9703
9026
  return getDefaultNotificationConfig();
9704
9027
  }
9705
9028
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -9744,7 +9067,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
9744
9067
  const deliveries = [];
9745
9068
  for (const channel of channels) {
9746
9069
  try {
9747
- deliveries.push(await dispatchChannel2(channel, event, message));
9070
+ deliveries.push(await dispatchChannel(channel, event, message));
9748
9071
  } catch (error) {
9749
9072
  deliveries.push({
9750
9073
  channelId: channel.id,
@@ -9779,7 +9102,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
9779
9102
  if (!options.yes) {
9780
9103
  throw new Error("Notification test execution requires --yes.");
9781
9104
  }
9782
- const delivery = await dispatchChannel2(channel, event, message);
9105
+ const delivery = await dispatchChannel(channel, event, message);
9783
9106
  return {
9784
9107
  channelId,
9785
9108
  mode: "apply",
@@ -9847,528 +9170,7 @@ function listPorts(machineId) {
9847
9170
  // src/commands/runtime.ts
9848
9171
  import { spawnSync as spawnSync4 } from "child_process";
9849
9172
  import { setTimeout as sleep } from "timers/promises";
9850
-
9851
- // node_modules/@hasna/events/dist/index.js
9852
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
9853
- import { existsSync as existsSync8 } from "fs";
9854
- import { homedir as homedir6 } from "os";
9855
- import { join as join7 } from "path";
9856
- import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
9857
- import { randomUUID as randomUUID3 } from "crypto";
9858
- import { spawn as spawn2 } from "child_process";
9859
- import { randomUUID as randomUUID22 } from "crypto";
9860
- function getPathValue2(input, path) {
9861
- return path.split(".").reduce((value, part) => {
9862
- if (value && typeof value === "object" && part in value) {
9863
- return value[part];
9864
- }
9865
- return;
9866
- }, input);
9867
- }
9868
- function wildcardToRegExp2(pattern) {
9869
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
9870
- return new RegExp(`^${escaped}$`);
9871
- }
9872
- function matchString2(value, matcher) {
9873
- if (matcher === undefined)
9874
- return true;
9875
- if (value === undefined)
9876
- return false;
9877
- const matchers = Array.isArray(matcher) ? matcher : [matcher];
9878
- return matchers.some((item) => wildcardToRegExp2(item).test(value));
9879
- }
9880
- function matchRecord2(input, matcher) {
9881
- if (!matcher)
9882
- return true;
9883
- return Object.entries(matcher).every(([path, expected]) => {
9884
- const actual = getPathValue2(input, path);
9885
- if (typeof expected === "string" || Array.isArray(expected)) {
9886
- return matchString2(actual === undefined ? undefined : String(actual), expected);
9887
- }
9888
- return actual === expected;
9889
- });
9890
- }
9891
- function eventMatchesFilter2(event, filter) {
9892
- return matchString2(event.source, filter.source) && matchString2(event.type, filter.type) && matchString2(event.subject, filter.subject) && matchString2(event.severity, filter.severity) && matchRecord2(event.data, filter.data) && matchRecord2(event.metadata, filter.metadata);
9893
- }
9894
- function channelMatchesEvent2(channel, event) {
9895
- if (!channel.enabled)
9896
- return false;
9897
- if (!channel.filters || channel.filters.length === 0)
9898
- return true;
9899
- return channel.filters.some((filter) => eventMatchesFilter2(event, filter));
9900
- }
9901
- var HASNA_EVENTS_DIR_ENV2 = "HASNA_EVENTS_DIR";
9902
- var HASNA_EVENTS_HOME_ENV2 = "HASNA_EVENTS_HOME";
9903
- function getEventsDataDir2(override) {
9904
- return override || process.env[HASNA_EVENTS_DIR_ENV2] || process.env[HASNA_EVENTS_HOME_ENV2] || join7(homedir6(), ".hasna", "events");
9905
- }
9906
-
9907
- class JsonEventsStore2 {
9908
- dataDir;
9909
- channelsPath;
9910
- eventsPath;
9911
- deliveriesPath;
9912
- constructor(dataDir = getEventsDataDir2()) {
9913
- this.dataDir = dataDir;
9914
- this.channelsPath = join7(dataDir, "channels.json");
9915
- this.eventsPath = join7(dataDir, "events.json");
9916
- this.deliveriesPath = join7(dataDir, "deliveries.json");
9917
- }
9918
- async init() {
9919
- await mkdir2(this.dataDir, { recursive: true, mode: 448 });
9920
- await chmod2(this.dataDir, 448).catch(() => {
9921
- return;
9922
- });
9923
- await this.ensureArrayFile(this.channelsPath);
9924
- await this.ensureArrayFile(this.eventsPath);
9925
- await this.ensureArrayFile(this.deliveriesPath);
9926
- }
9927
- async addChannel(channel) {
9928
- await this.init();
9929
- const channels = await this.readJson(this.channelsPath, []);
9930
- const index = channels.findIndex((item) => item.id === channel.id);
9931
- if (index >= 0) {
9932
- channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
9933
- } else {
9934
- channels.push(channel);
9935
- }
9936
- await this.writeJson(this.channelsPath, channels);
9937
- return index >= 0 ? channels[index] : channel;
9938
- }
9939
- async listChannels() {
9940
- await this.init();
9941
- return this.readJson(this.channelsPath, []);
9942
- }
9943
- async getChannel(id) {
9944
- const channels = await this.listChannels();
9945
- return channels.find((channel) => channel.id === id);
9946
- }
9947
- async removeChannel(id) {
9948
- await this.init();
9949
- const channels = await this.readJson(this.channelsPath, []);
9950
- const next = channels.filter((channel) => channel.id !== id);
9951
- await this.writeJson(this.channelsPath, next);
9952
- return next.length !== channels.length;
9953
- }
9954
- async appendEvent(event) {
9955
- await this.init();
9956
- const events = await this.readJson(this.eventsPath, []);
9957
- events.push(event);
9958
- await this.writeJson(this.eventsPath, events);
9959
- return event;
9960
- }
9961
- async listEvents() {
9962
- await this.init();
9963
- return this.readJson(this.eventsPath, []);
9964
- }
9965
- async findEventByIdentity(identity) {
9966
- const events = await this.listEvents();
9967
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
9968
- }
9969
- async appendDelivery(result) {
9970
- await this.init();
9971
- const deliveries = await this.readJson(this.deliveriesPath, []);
9972
- deliveries.push(result);
9973
- await this.writeJson(this.deliveriesPath, deliveries);
9974
- return result;
9975
- }
9976
- async listDeliveries() {
9977
- await this.init();
9978
- return this.readJson(this.deliveriesPath, []);
9979
- }
9980
- async exportData() {
9981
- return {
9982
- channels: await this.listChannels(),
9983
- events: await this.listEvents(),
9984
- deliveries: await this.listDeliveries()
9985
- };
9986
- }
9987
- async ensureArrayFile(path) {
9988
- if (!existsSync8(path)) {
9989
- await writeFile2(path, `[]
9990
- `, { encoding: "utf-8", mode: 384 });
9991
- }
9992
- await chmod2(path, 384).catch(() => {
9993
- return;
9994
- });
9995
- }
9996
- async readJson(path, fallback) {
9997
- try {
9998
- const raw = await readFile2(path, "utf-8");
9999
- if (!raw.trim())
10000
- return fallback;
10001
- return JSON.parse(raw);
10002
- } catch (error) {
10003
- if (error.code === "ENOENT")
10004
- return fallback;
10005
- throw error;
10006
- }
10007
- }
10008
- async writeJson(path, value) {
10009
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
10010
- await writeFile2(tempPath, `${JSON.stringify(value, null, 2)}
10011
- `, { encoding: "utf-8", mode: 384 });
10012
- await rename2(tempPath, path);
10013
- await chmod2(path, 384).catch(() => {
10014
- return;
10015
- });
10016
- }
10017
- }
10018
- var DEFAULT_SIGNATURE_TOLERANCE_MS2 = 5 * 60 * 1000;
10019
- function buildSignatureBase2(timestamp, body) {
10020
- return `${timestamp}.${body}`;
10021
- }
10022
- function signPayload2(secret, timestamp, body) {
10023
- const digest = createHmac2("sha256", secret).update(buildSignatureBase2(timestamp, body)).digest("hex");
10024
- return `sha256=${digest}`;
10025
- }
10026
- function now2() {
10027
- return new Date().toISOString();
10028
- }
10029
- function truncate2(value, max = 4096) {
10030
- return value.length > max ? `${value.slice(0, max)}...` : value;
10031
- }
10032
- function buildWebhookRequest2(event, channel) {
10033
- if (!channel.webhook)
10034
- throw new Error(`Channel ${channel.id} has no webhook config`);
10035
- const body = JSON.stringify(event);
10036
- const timestamp = event.time;
10037
- const headers = {
10038
- "Content-Type": "application/json",
10039
- "User-Agent": "@hasna/events",
10040
- "X-Hasna-Event-Id": event.id,
10041
- "X-Hasna-Event-Type": event.type,
10042
- "X-Hasna-Timestamp": timestamp,
10043
- ...channel.webhook.headers
10044
- };
10045
- if (channel.webhook.secret) {
10046
- headers["X-Hasna-Signature"] = signPayload2(channel.webhook.secret, timestamp, body);
10047
- }
10048
- return { body, headers };
10049
- }
10050
- async function dispatchWebhook3(event, channel, options = {}) {
10051
- if (!channel.webhook)
10052
- throw new Error(`Channel ${channel.id} has no webhook config`);
10053
- const startedAt = now2();
10054
- const { body, headers } = buildWebhookRequest2(event, channel);
10055
- const controller = new AbortController;
10056
- const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
10057
- try {
10058
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
10059
- method: "POST",
10060
- headers,
10061
- body,
10062
- signal: controller.signal
10063
- });
10064
- const responseBody = truncate2(await response.text());
10065
- return {
10066
- attempt: 1,
10067
- status: response.ok ? "success" : "failed",
10068
- startedAt,
10069
- completedAt: now2(),
10070
- responseStatus: response.status,
10071
- responseBody,
10072
- error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
10073
- };
10074
- } catch (error) {
10075
- return {
10076
- attempt: 1,
10077
- status: "failed",
10078
- startedAt,
10079
- completedAt: now2(),
10080
- error: error instanceof Error ? error.message : String(error)
10081
- };
10082
- } finally {
10083
- clearTimeout(timeout);
10084
- }
10085
- }
10086
- async function dispatchCommand3(event, channel) {
10087
- if (!channel.command)
10088
- throw new Error(`Channel ${channel.id} has no command config`);
10089
- const startedAt = now2();
10090
- const eventJson = JSON.stringify(event);
10091
- const env2 = {
10092
- ...process.env,
10093
- ...channel.command.env,
10094
- HASNA_CHANNEL_ID: channel.id,
10095
- HASNA_EVENT_ID: event.id,
10096
- HASNA_EVENT_TYPE: event.type,
10097
- HASNA_EVENT_SOURCE: event.source,
10098
- HASNA_EVENT_SUBJECT: event.subject ?? "",
10099
- HASNA_EVENT_SEVERITY: event.severity,
10100
- HASNA_EVENT_TIME: event.time,
10101
- HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
10102
- HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
10103
- HASNA_EVENT_JSON: eventJson
10104
- };
10105
- return new Promise((resolve2) => {
10106
- const child = spawn2(channel.command.command, channel.command.args ?? [], {
10107
- cwd: channel.command.cwd,
10108
- env: env2,
10109
- stdio: ["pipe", "pipe", "pipe"]
10110
- });
10111
- let stdout = "";
10112
- let stderr = "";
10113
- const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
10114
- child.stdin.end(eventJson);
10115
- child.stdout.on("data", (chunk) => {
10116
- stdout += chunk.toString();
10117
- });
10118
- child.stderr.on("data", (chunk) => {
10119
- stderr += chunk.toString();
10120
- });
10121
- child.on("error", (error) => {
10122
- clearTimeout(timeout);
10123
- resolve2({
10124
- attempt: 1,
10125
- status: "failed",
10126
- startedAt,
10127
- completedAt: now2(),
10128
- stdout: truncate2(stdout),
10129
- stderr: truncate2(stderr),
10130
- error: error.message
10131
- });
10132
- });
10133
- child.on("close", (code, signal) => {
10134
- clearTimeout(timeout);
10135
- const success = code === 0;
10136
- resolve2({
10137
- attempt: 1,
10138
- status: success ? "success" : "failed",
10139
- startedAt,
10140
- completedAt: now2(),
10141
- stdout: truncate2(stdout),
10142
- stderr: truncate2(stderr),
10143
- error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
10144
- });
10145
- });
10146
- });
10147
- }
10148
- async function dispatchChannel3(event, channel, options = {}) {
10149
- if (channel.transport === "webhook")
10150
- return dispatchWebhook3(event, channel, options);
10151
- if (channel.transport === "command")
10152
- return dispatchCommand3(event, channel);
10153
- return {
10154
- attempt: 1,
10155
- status: "skipped",
10156
- startedAt: now2(),
10157
- completedAt: now2(),
10158
- error: `Unsupported transport: ${channel.transport}`
10159
- };
10160
- }
10161
- function createDeliveryResult2(event, channel, attempts) {
10162
- const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
10163
- return {
10164
- id: randomUUID3(),
10165
- eventId: event.id,
10166
- channelId: channel.id,
10167
- transport: channel.transport,
10168
- status,
10169
- attempts,
10170
- createdAt: attempts[0]?.startedAt ?? now2(),
10171
- completedAt: attempts.at(-1)?.completedAt ?? now2()
10172
- };
10173
- }
10174
- function createEvent2(input) {
10175
- return {
10176
- id: input.id ?? randomUUID22(),
10177
- source: input.source,
10178
- type: input.type,
10179
- time: normalizeTime2(input.time),
10180
- subject: input.subject,
10181
- severity: input.severity ?? "info",
10182
- data: input.data ?? {},
10183
- message: input.message,
10184
- dedupeKey: input.dedupeKey,
10185
- schemaVersion: input.schemaVersion ?? "1.0",
10186
- metadata: input.metadata ?? {}
10187
- };
10188
- }
10189
-
10190
- class EventsClient2 {
10191
- store;
10192
- redactors;
10193
- transportOptions;
10194
- constructor(options = {}) {
10195
- this.store = options.store ?? new JsonEventsStore2(options.dataDir);
10196
- this.redactors = options.redactors ?? [];
10197
- this.transportOptions = { fetchImpl: options.fetchImpl };
10198
- }
10199
- async addChannel(input) {
10200
- const timestamp = new Date().toISOString();
10201
- return this.store.addChannel({
10202
- ...input,
10203
- createdAt: input.createdAt ?? timestamp,
10204
- updatedAt: input.updatedAt ?? timestamp
10205
- });
10206
- }
10207
- async listChannels() {
10208
- return this.store.listChannels();
10209
- }
10210
- async removeChannel(id) {
10211
- return this.store.removeChannel(id);
10212
- }
10213
- async emit(input, options = {}) {
10214
- const event = options.redactSensitiveData === false ? createEvent2(input) : redactSensitiveKeys2(createEvent2(input));
10215
- if (options.dedupe !== false) {
10216
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
10217
- if (existing) {
10218
- return { event: existing, deliveries: [], deduped: true };
10219
- }
10220
- }
10221
- await this.store.appendEvent(event);
10222
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
10223
- return { event, deliveries, deduped: false };
10224
- }
10225
- async listEvents() {
10226
- return this.store.listEvents();
10227
- }
10228
- async listDeliveries() {
10229
- return this.store.listDeliveries();
10230
- }
10231
- async deliver(event) {
10232
- const channels = await this.store.listChannels();
10233
- const selected = channels.filter((channel) => channelMatchesEvent2(channel, event));
10234
- const deliveries = [];
10235
- for (const channel of selected) {
10236
- const eventForChannel = await this.applyRedaction(event, channel);
10237
- const result = await this.deliverWithRetry(eventForChannel, channel);
10238
- await this.store.appendDelivery(result);
10239
- deliveries.push(result);
10240
- }
10241
- return deliveries;
10242
- }
10243
- async testChannel(id, input = {}) {
10244
- const channel = await this.store.getChannel(id);
10245
- if (!channel)
10246
- throw new Error(`Channel not found: ${id}`);
10247
- const event = createEvent2({
10248
- source: input.source ?? "hasna.events",
10249
- type: input.type ?? "events.test",
10250
- subject: input.subject ?? id,
10251
- severity: input.severity ?? "info",
10252
- data: input.data ?? { test: true },
10253
- message: input.message ?? "Hasna events test delivery",
10254
- dedupeKey: input.dedupeKey,
10255
- schemaVersion: input.schemaVersion,
10256
- metadata: input.metadata,
10257
- time: input.time,
10258
- id: input.id
10259
- });
10260
- const eventForChannel = await this.applyRedaction(event, channel);
10261
- const result = await this.deliverWithRetry(eventForChannel, channel);
10262
- await this.store.appendDelivery(result);
10263
- return result;
10264
- }
10265
- async replay(options = {}) {
10266
- const events = (await this.store.listEvents()).filter((event) => {
10267
- if (options.eventId && event.id !== options.eventId)
10268
- return false;
10269
- if (options.source && event.source !== options.source)
10270
- return false;
10271
- if (options.type && event.type !== options.type)
10272
- return false;
10273
- return true;
10274
- });
10275
- if (options.dryRun)
10276
- return { events, deliveries: [] };
10277
- const deliveries = [];
10278
- for (const event of events) {
10279
- deliveries.push(...await this.deliver(event));
10280
- }
10281
- return { events, deliveries };
10282
- }
10283
- async applyRedaction(event, channel) {
10284
- let next = redactPaths2(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
10285
- for (const redactor of this.redactors) {
10286
- next = await redactor(next, channel);
10287
- }
10288
- return next;
10289
- }
10290
- async deliverWithRetry(event, channel) {
10291
- const policy = normalizeRetryPolicy2(channel.retry);
10292
- const attempts = [];
10293
- for (let index = 0;index < policy.maxAttempts; index += 1) {
10294
- const attempt = await dispatchChannel3(event, channel, this.transportOptions);
10295
- attempt.attempt = index + 1;
10296
- if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
10297
- attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
10298
- }
10299
- attempts.push(attempt);
10300
- if (attempt.status !== "failed")
10301
- break;
10302
- if (attempt.nextBackoffMs)
10303
- await Bun.sleep(attempt.nextBackoffMs);
10304
- }
10305
- return createDeliveryResult2(event, channel, attempts);
10306
- }
10307
- }
10308
- function redactPaths2(event, paths, replacement = "[REDACTED]") {
10309
- if (paths.length === 0)
10310
- return event;
10311
- const copy = structuredClone(event);
10312
- for (const path of paths) {
10313
- setPath2(copy, path, replacement);
10314
- }
10315
- return copy;
10316
- }
10317
- function sanitizeChannelForOutput2(channel) {
10318
- const copy = structuredClone(channel);
10319
- if (copy.webhook?.secret)
10320
- copy.webhook.secret = "[REDACTED]";
10321
- if (copy.command?.env) {
10322
- copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey2(key) ? "[REDACTED]" : value]));
10323
- }
10324
- return copy;
10325
- }
10326
- function sanitizeChannelsForOutput2(channels) {
10327
- return channels.map(sanitizeChannelForOutput2);
10328
- }
10329
- function redactSensitiveKeys2(event, replacement = "[REDACTED]") {
10330
- return redactValue2(event, replacement);
10331
- }
10332
- function shouldRedactKey2(key) {
10333
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
10334
- }
10335
- function redactValue2(value, replacement) {
10336
- if (Array.isArray(value))
10337
- return value.map((item) => redactValue2(item, replacement));
10338
- if (!value || typeof value !== "object")
10339
- return value;
10340
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
10341
- key,
10342
- shouldRedactKey2(key) ? replacement : redactValue2(item, replacement)
10343
- ]));
10344
- }
10345
- function setPath2(input, path, replacement) {
10346
- const parts = path.split(".");
10347
- let cursor = input;
10348
- for (const part of parts.slice(0, -1)) {
10349
- const next = cursor[part];
10350
- if (!next || typeof next !== "object")
10351
- return;
10352
- cursor = next;
10353
- }
10354
- const last = parts.at(-1);
10355
- if (last && last in cursor)
10356
- cursor[last] = replacement;
10357
- }
10358
- function normalizeTime2(value) {
10359
- if (!value)
10360
- return new Date().toISOString();
10361
- return value instanceof Date ? value.toISOString() : value;
10362
- }
10363
- function normalizeRetryPolicy2(policy) {
10364
- return {
10365
- maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
10366
- backoffMs: Math.max(0, policy?.backoffMs ?? 250),
10367
- multiplier: Math.max(1, policy?.multiplier ?? 2)
10368
- };
10369
- }
10370
-
10371
- // src/commands/runtime.ts
9173
+ import { EventsClient } from "@hasna/events";
10372
9174
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
10373
9175
  const checkedAt = new Date().toISOString();
10374
9176
  const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
@@ -10394,7 +9196,7 @@ async function watchTmuxPane(options) {
10394
9196
  throw new Error("tmux pane target is required");
10395
9197
  const intervalMs = Math.max(0, options.intervalMs ?? 5000);
10396
9198
  const maxChecks = options.maxChecks ?? Number.POSITIVE_INFINITY;
10397
- const client = options.client ?? new EventsClient2;
9199
+ const client = options.client ?? new EventsClient;
10398
9200
  const probe = options.probe ?? ((paneTarget) => probeTmuxPane(paneTarget, options.tmuxCommand));
10399
9201
  const wait = options.sleep ?? sleep;
10400
9202
  let lastPresent;
@@ -10455,7 +9257,8 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
10455
9257
  }
10456
9258
 
10457
9259
  // src/commands/screen.ts
10458
- var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
9260
+ var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
9261
+ var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
10459
9262
  function shellQuote6(value) {
10460
9263
  return `'${value.replace(/'/g, "'\\''")}'`;
10461
9264
  }
@@ -10479,7 +9282,8 @@ function splitTarget(target) {
10479
9282
  return [target.slice(0, at), target.slice(at + 1)];
10480
9283
  }
10481
9284
  function defaultScreenPasswordSecretKey(machineId) {
10482
- return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
9285
+ const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
9286
+ return `${namespace}/screen-${machineId}-vnc-password`;
10483
9287
  }
10484
9288
  function resolveScreenTarget(machineId, options = {}) {
10485
9289
  const resolved = resolveMachineRoute(machineId, options);
@@ -10563,8 +9367,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
10563
9367
  }
10564
9368
 
10565
9369
  // src/commands/sync.ts
10566
- import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
10567
- import { homedir as homedir7 } from "os";
9370
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
9371
+ import { homedir as homedir5 } from "os";
10568
9372
  init_paths();
10569
9373
  init_db();
10570
9374
  function quote4(value) {
@@ -10618,8 +9422,8 @@ function detectFileActions(machine) {
10618
9422
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
10619
9423
  }
10620
9424
  return (machine.files || []).map((file, index) => {
10621
- const sourceExists = existsSync9(file.source);
10622
- const targetExists = existsSync9(file.target);
9425
+ const sourceExists = existsSync7(file.source);
9426
+ const targetExists = existsSync7(file.target);
10623
9427
  let status = "missing";
10624
9428
  if (sourceExists && targetExists) {
10625
9429
  if (file.mode === "symlink") {
@@ -10650,7 +9454,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
10650
9454
  const target = selected || {
10651
9455
  id: currentMachineId,
10652
9456
  platform: "linux",
10653
- workspacePath: `${homedir7()}/workspace`
9457
+ workspacePath: `${homedir5()}/workspace`
10654
9458
  };
10655
9459
  const actions = [
10656
9460
  ...detectPackageActions(target, runner),
@@ -11207,6 +10011,7 @@ function runDoctor(machineId = getLocalMachineId()) {
11207
10011
  init_db();
11208
10012
 
11209
10013
  // src/commands/serve.ts
10014
+ import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
11210
10015
  function escapeHtml(value) {
11211
10016
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11212
10017
  }
@@ -11423,7 +10228,7 @@ function startDashboardServer(options = {}) {
11423
10228
  if (request.method !== "GET") {
11424
10229
  return jsonError("Use GET for webhook channel listing.", 405);
11425
10230
  }
11426
- return Response.json(sanitizeChannelsForOutput2(await events.listChannels()));
10231
+ return Response.json(sanitizeChannelsForOutput(await events.listChannels()));
11427
10232
  }
11428
10233
  if (url.pathname === "/api/events") {
11429
10234
  if (request.method === "GET") {
@@ -11556,8 +10361,8 @@ function runSelfTest() {
11556
10361
  // src/commands/clipboard.ts
11557
10362
  init_paths();
11558
10363
  import { createHash } from "crypto";
11559
- import { existsSync as existsSync10, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
11560
- import { join as join8 } from "path";
10364
+ import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
10365
+ import { join as join6 } from "path";
11561
10366
  var DEFAULT_CONFIG = {
11562
10367
  version: 1,
11563
10368
  enabled: true,
@@ -11575,7 +10380,7 @@ var DEFAULT_CONFIG = {
11575
10380
  function resolveConfigPath(configPath) {
11576
10381
  if (configPath)
11577
10382
  return configPath;
11578
- return join8(getDataDir(), "clipboard-config.json");
10383
+ return join6(getDataDir(), "clipboard-config.json");
11579
10384
  }
11580
10385
  function resolveHistoryPath(historyPath) {
11581
10386
  if (historyPath)
@@ -11587,7 +10392,7 @@ function getDefaultConfig() {
11587
10392
  }
11588
10393
  function readConfig(configPath) {
11589
10394
  const path = resolveConfigPath(configPath);
11590
- if (!existsSync10(path)) {
10395
+ if (!existsSync8(path)) {
11591
10396
  return getDefaultConfig();
11592
10397
  }
11593
10398
  const parsed = JSON.parse(readFileSync6(path, "utf8"));
@@ -11601,7 +10406,7 @@ function writeConfig(config, configPath) {
11601
10406
  }
11602
10407
  function readHistory(historyPath) {
11603
10408
  const path = resolveHistoryPath(historyPath);
11604
- if (!existsSync10(path)) {
10409
+ if (!existsSync8(path)) {
11605
10410
  return [];
11606
10411
  }
11607
10412
  try {
@@ -11634,7 +10439,7 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
11634
10439
  }
11635
10440
  function getOrCreateClipboardKey() {
11636
10441
  const keyPath = getClipboardKeyPath();
11637
- if (existsSync10(keyPath)) {
10442
+ if (existsSync8(keyPath)) {
11638
10443
  return readFileSync6(keyPath, "utf8").trim();
11639
10444
  }
11640
10445
  const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
@@ -11674,7 +10479,7 @@ function addClipboardEntry(entry, historyPath) {
11674
10479
  }
11675
10480
  function clearClipboardHistory(historyPath) {
11676
10481
  const path = resolveHistoryPath(historyPath);
11677
- if (existsSync10(path)) {
10482
+ if (existsSync8(path)) {
11678
10483
  rmSync(path);
11679
10484
  }
11680
10485
  }
@@ -11691,7 +10496,7 @@ function getClipboardStatus(historyPath) {
11691
10496
  // src/commands/clipboard-daemon.ts
11692
10497
  init_paths();
11693
10498
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
11694
- import { join as join9 } from "path";
10499
+ import { join as join7 } from "path";
11695
10500
  import { createHash as createHash3 } from "crypto";
11696
10501
 
11697
10502
  // src/commands/clipboard-server.ts
@@ -11849,7 +10654,7 @@ function handleGetClipboard(response, config) {
11849
10654
  }
11850
10655
 
11851
10656
  // src/commands/clipboard-daemon.ts
11852
- var DAEMON_PID_PATH = join9(getDataDir(), "clipboard-daemon.pid");
10657
+ var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
11853
10658
  function readLocalClipboardSync2() {
11854
10659
  const platform4 = process.platform;
11855
10660
  if (platform4 === "darwin") {
@@ -12023,8 +10828,8 @@ async function discoverPeers() {
12023
10828
 
12024
10829
  // src/commands/heal.ts
12025
10830
  init_paths();
12026
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
12027
- import { join as join10 } from "path";
10831
+ import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
10832
+ import { join as join8 } from "path";
12028
10833
  var DEFAULT_THRESHOLDS = {
12029
10834
  reconnect: 3,
12030
10835
  nmRestart: 7,
@@ -12068,14 +10873,14 @@ function defaultHealState() {
12068
10873
  };
12069
10874
  }
12070
10875
  function getHealConfigPath() {
12071
- return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join10(getDataDir(), "heal-config.json");
10876
+ return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
12072
10877
  }
12073
10878
  function getHealStatePath() {
12074
- return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join10(getDataDir(), "heal-state.json");
10879
+ return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
12075
10880
  }
12076
10881
  function readHealConfig(path) {
12077
10882
  const p = path || getHealConfigPath();
12078
- if (!existsSync11(p))
10883
+ if (!existsSync9(p))
12079
10884
  return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
12080
10885
  const parsed = JSON.parse(readFileSync9(p, "utf8"));
12081
10886
  return {
@@ -12093,7 +10898,7 @@ function writeHealConfig(config, path) {
12093
10898
  }
12094
10899
  function readHealState(path) {
12095
10900
  const p = path || getHealStatePath();
12096
- if (!existsSync11(p))
10901
+ if (!existsSync9(p))
12097
10902
  return defaultHealState();
12098
10903
  try {
12099
10904
  return { ...defaultHealState(), ...JSON.parse(readFileSync9(p, "utf8")) };
@@ -12133,7 +10938,7 @@ function evaluateHealth(probe, config, state) {
12133
10938
  return { healthy: localOk && quorumOk, remoteScore, reasons };
12134
10939
  }
12135
10940
  function decideAction(input) {
12136
- const { healthy, now: now3, gpuBusy, config, currentBootId } = input;
10941
+ const { healthy, now, gpuBusy, config, currentBootId } = input;
12137
10942
  const s = { ...input.state };
12138
10943
  const t = config.thresholds;
12139
10944
  if (s.bootId !== currentBootId) {
@@ -12144,13 +10949,13 @@ function decideAction(input) {
12144
10949
  if (healthy) {
12145
10950
  s.failCount = 0;
12146
10951
  if (s.bootHealthySince === null)
12147
- s.bootHealthySince = now3;
12148
- if (now3 - s.bootHealthySince >= config.healthyWindowSec) {
10952
+ s.bootHealthySince = now;
10953
+ if (now - s.bootHealthySince >= config.healthyWindowSec) {
12149
10954
  s.failedBootRecoveries = 0;
12150
10955
  s.rebootSuppressUntil = 0;
12151
10956
  s.pendingRebootRecovery = false;
12152
10957
  }
12153
- if (s.degradedUntil > 0 && now3 >= s.degradedUntil) {
10958
+ if (s.degradedUntil > 0 && now >= s.degradedUntil) {
12154
10959
  s.degradedUntil = 0;
12155
10960
  return { action: "restore_preferred", state: s };
12156
10961
  }
@@ -12168,8 +10973,8 @@ function decideAction(input) {
12168
10973
  else if (s.failCount >= t.reconnect)
12169
10974
  tier = "reconnect";
12170
10975
  const tryReconnect = (reason) => {
12171
- if (now3 - s.lastReconnect >= config.reconnectMinIntervalSec) {
12172
- s.lastReconnect = now3;
10976
+ if (now - s.lastReconnect >= config.reconnectMinIntervalSec) {
10977
+ s.lastReconnect = now;
12173
10978
  return { action: "reconnect_wifi", suppressedReason: reason, state: s };
12174
10979
  }
12175
10980
  return { action: "none", suppressedReason: reason, state: s };
@@ -12178,15 +10983,15 @@ function decideAction(input) {
12178
10983
  case "reconnect":
12179
10984
  return tryReconnect();
12180
10985
  case "nmRestart":
12181
- if (now3 - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
12182
- s.lastNmRestart = now3;
10986
+ if (now - s.lastNmRestart >= config.nmRestartMinIntervalSec) {
10987
+ s.lastNmRestart = now;
12183
10988
  return { action: "restart_nm", state: s };
12184
10989
  }
12185
10990
  return tryReconnect();
12186
10991
  case "fallback":
12187
- if (now3 - s.lastFallback >= config.fallbackWindowSec) {
12188
- s.lastFallback = now3;
12189
- s.degradedUntil = now3 + config.fallbackWindowSec;
10992
+ if (now - s.lastFallback >= config.fallbackWindowSec) {
10993
+ s.lastFallback = now;
10994
+ s.degradedUntil = now + config.fallbackWindowSec;
12190
10995
  return { action: "fallback_ssid", state: s };
12191
10996
  }
12192
10997
  return tryReconnect();
@@ -12194,22 +10999,22 @@ function decideAction(input) {
12194
10999
  let reason = null;
12195
11000
  if (!config.allowReboot)
12196
11001
  reason = "disabled";
12197
- else if (now3 < s.rebootSuppressUntil)
11002
+ else if (now < s.rebootSuppressUntil)
12198
11003
  reason = "loop";
12199
11004
  else if (config.gpuJobGuard && gpuBusy)
12200
11005
  reason = "gpu";
12201
- else if (now3 - s.lastRebootAttempt < config.rebootMinIntervalSec)
11006
+ else if (now - s.lastRebootAttempt < config.rebootMinIntervalSec)
12202
11007
  reason = "rate";
12203
11008
  if (reason)
12204
11009
  return tryReconnect(reason);
12205
11010
  if (s.pendingRebootRecovery) {
12206
11011
  s.failedBootRecoveries += 1;
12207
11012
  if (s.failedBootRecoveries >= config.maxFailedBootRecoveries) {
12208
- s.rebootSuppressUntil = now3 + config.bootBackoffSec;
11013
+ s.rebootSuppressUntil = now + config.bootBackoffSec;
12209
11014
  return tryReconnect("loop");
12210
11015
  }
12211
11016
  }
12212
- s.lastRebootAttempt = now3;
11017
+ s.lastRebootAttempt = now;
12213
11018
  s.pendingRebootRecovery = true;
12214
11019
  return { action: "reboot", state: s };
12215
11020
  }
@@ -12309,9 +11114,9 @@ function executeAction(action, config) {
12309
11114
 
12310
11115
  // src/commands/heal-daemon.ts
12311
11116
  init_paths();
12312
- import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
12313
- import { join as join11 } from "path";
12314
- var DAEMON_PID_PATH2 = join11(getDataDir(), "heal-daemon.pid");
11117
+ import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
11118
+ import { join as join9 } from "path";
11119
+ var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
12315
11120
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
12316
11121
  var SYSTEM_CONF = "/etc/systemd/system.conf";
12317
11122
  function log(msg) {
@@ -12429,7 +11234,7 @@ function applyDeterminism(config) {
12429
11234
  }
12430
11235
  function enableHardwareWatchdog() {
12431
11236
  const log2 = [];
12432
- if (!existsSync12(SYSTEM_CONF))
11237
+ if (!existsSync10(SYSTEM_CONF))
12433
11238
  return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
12434
11239
  let conf = readFileSync10(SYSTEM_CONF, "utf8");
12435
11240
  const set = (key, value) => {
@@ -12459,7 +11264,7 @@ function binPath() {
12459
11264
  const home = process.env["HOME"] || "/home/hasna";
12460
11265
  candidates.push(`${home}/.bun/bin/machines`, "/home/hasna/.bun/bin/machines", "/root/.bun/bin/machines", "/usr/local/bin/machines");
12461
11266
  for (const c of candidates) {
12462
- if (c && existsSync12(c))
11267
+ if (c && existsSync10(c))
12463
11268
  return c;
12464
11269
  }
12465
11270
  return "machines";
@@ -12495,7 +11300,7 @@ WantedBy=multi-user.target
12495
11300
  function uninstallHealService() {
12496
11301
  const log2 = [];
12497
11302
  sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
12498
- if (existsSync12(SERVICE_PATH)) {
11303
+ if (existsSync10(SERVICE_PATH)) {
12499
11304
  sh2(`rm -f ${SERVICE_PATH}`);
12500
11305
  sh2("systemctl daemon-reload");
12501
11306
  log2.push(`removed ${SERVICE_PATH}`);
@@ -12506,7 +11311,7 @@ function uninstallHealService() {
12506
11311
  }
12507
11312
  function healServiceStatus() {
12508
11313
  return {
12509
- installed: existsSync12(SERVICE_PATH),
11314
+ installed: existsSync10(SERVICE_PATH),
12510
11315
  active: sh2("systemctl is-active machines-heal.service").out === "active",
12511
11316
  enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
12512
11317
  };
@@ -12777,8 +11582,17 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
12777
11582
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
12778
11583
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
12779
11584
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
12780
- registerEventsCommands(program2, { source: "machines" });
12781
- var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit Hasna events");
11585
+ var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
11586
+ eventWebhooksCommand.description("Manage shared event webhook subscriptions");
11587
+ var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
11588
+ var webhookOptions = webhookTestCommand?.options ?? [];
11589
+ var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
11590
+ if (webhookMessageOption) {
11591
+ webhookMessageOption.defaultValue = "Shared events test delivery";
11592
+ }
11593
+ var eventsCommand = registerEventCommands(program2, { source: "machines" });
11594
+ eventsCommand.description("Emit, list, and replay shared events");
11595
+ var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
12782
11596
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
12783
11597
  var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
12784
11598
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {