@hasna/microservices 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/index.js +696 -11
  2. package/bin/mcp.js +1 -1
  3. package/package.json +2 -1
package/bin/index.js CHANGED
@@ -847,7 +847,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
847
847
  this._exitCallback = (err) => {
848
848
  if (err.code !== "commander.executeSubCommandAsync") {
849
849
  throw err;
850
- } else {}
850
+ }
851
851
  };
852
852
  }
853
853
  return this;
@@ -1872,6 +1872,690 @@ var require_commander = __commonJS((exports) => {
1872
1872
  exports.InvalidOptionArgumentError = InvalidArgumentError;
1873
1873
  });
1874
1874
 
1875
+ // node_modules/.bun/@hasna+events@0.1.6/node_modules/@hasna/events/dist/commander.js
1876
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
1877
+ import { existsSync } from "fs";
1878
+ import { homedir } from "os";
1879
+ import { join } from "path";
1880
+ import { createHmac, timingSafeEqual } from "crypto";
1881
+ import { randomUUID } from "crypto";
1882
+ import { spawn } from "child_process";
1883
+ import { randomUUID as randomUUID2 } from "crypto";
1884
+ function getPathValue(input, path) {
1885
+ return path.split(".").reduce((value, part) => {
1886
+ if (value && typeof value === "object" && part in value) {
1887
+ return value[part];
1888
+ }
1889
+ return;
1890
+ }, input);
1891
+ }
1892
+ function wildcardToRegExp(pattern) {
1893
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
1894
+ return new RegExp(`^${escaped}$`);
1895
+ }
1896
+ function matchString(value, matcher) {
1897
+ if (matcher === undefined)
1898
+ return true;
1899
+ if (value === undefined)
1900
+ return false;
1901
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
1902
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
1903
+ }
1904
+ function matchRecord(input, matcher) {
1905
+ if (!matcher)
1906
+ return true;
1907
+ return Object.entries(matcher).every(([path, expected]) => {
1908
+ const actual = getPathValue(input, path);
1909
+ if (typeof expected === "string" || Array.isArray(expected)) {
1910
+ return matchString(actual === undefined ? undefined : String(actual), expected);
1911
+ }
1912
+ return actual === expected;
1913
+ });
1914
+ }
1915
+ function eventMatchesFilter(event, filter) {
1916
+ 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);
1917
+ }
1918
+ function channelMatchesEvent(channel, event) {
1919
+ if (!channel.enabled)
1920
+ return false;
1921
+ if (!channel.filters || channel.filters.length === 0)
1922
+ return true;
1923
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
1924
+ }
1925
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
1926
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
1927
+ function getEventsDataDir(override) {
1928
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
1929
+ }
1930
+
1931
+ class JsonEventsStore {
1932
+ dataDir;
1933
+ channelsPath;
1934
+ eventsPath;
1935
+ deliveriesPath;
1936
+ constructor(dataDir = getEventsDataDir()) {
1937
+ this.dataDir = dataDir;
1938
+ this.channelsPath = join(dataDir, "channels.json");
1939
+ this.eventsPath = join(dataDir, "events.json");
1940
+ this.deliveriesPath = join(dataDir, "deliveries.json");
1941
+ }
1942
+ async init() {
1943
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
1944
+ await chmod(this.dataDir, 448).catch(() => {
1945
+ return;
1946
+ });
1947
+ await this.ensureArrayFile(this.channelsPath);
1948
+ await this.ensureArrayFile(this.eventsPath);
1949
+ await this.ensureArrayFile(this.deliveriesPath);
1950
+ }
1951
+ async addChannel(channel) {
1952
+ await this.init();
1953
+ const channels = await this.readJson(this.channelsPath, []);
1954
+ const index = channels.findIndex((item) => item.id === channel.id);
1955
+ if (index >= 0) {
1956
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
1957
+ } else {
1958
+ channels.push(channel);
1959
+ }
1960
+ await this.writeJson(this.channelsPath, channels);
1961
+ return index >= 0 ? channels[index] : channel;
1962
+ }
1963
+ async listChannels() {
1964
+ await this.init();
1965
+ return this.readJson(this.channelsPath, []);
1966
+ }
1967
+ async getChannel(id) {
1968
+ const channels = await this.listChannels();
1969
+ return channels.find((channel) => channel.id === id);
1970
+ }
1971
+ async removeChannel(id) {
1972
+ await this.init();
1973
+ const channels = await this.readJson(this.channelsPath, []);
1974
+ const next = channels.filter((channel) => channel.id !== id);
1975
+ await this.writeJson(this.channelsPath, next);
1976
+ return next.length !== channels.length;
1977
+ }
1978
+ async appendEvent(event) {
1979
+ await this.init();
1980
+ const events = await this.readJson(this.eventsPath, []);
1981
+ events.push(event);
1982
+ await this.writeJson(this.eventsPath, events);
1983
+ return event;
1984
+ }
1985
+ async listEvents() {
1986
+ await this.init();
1987
+ return this.readJson(this.eventsPath, []);
1988
+ }
1989
+ async findEventByIdentity(identity) {
1990
+ const events = await this.listEvents();
1991
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
1992
+ }
1993
+ async appendDelivery(result) {
1994
+ await this.init();
1995
+ const deliveries = await this.readJson(this.deliveriesPath, []);
1996
+ deliveries.push(result);
1997
+ await this.writeJson(this.deliveriesPath, deliveries);
1998
+ return result;
1999
+ }
2000
+ async listDeliveries() {
2001
+ await this.init();
2002
+ return this.readJson(this.deliveriesPath, []);
2003
+ }
2004
+ async exportData() {
2005
+ return {
2006
+ channels: await this.listChannels(),
2007
+ events: await this.listEvents(),
2008
+ deliveries: await this.listDeliveries()
2009
+ };
2010
+ }
2011
+ async ensureArrayFile(path) {
2012
+ if (!existsSync(path)) {
2013
+ await writeFile(path, `[]
2014
+ `, { encoding: "utf-8", mode: 384 });
2015
+ }
2016
+ await chmod(path, 384).catch(() => {
2017
+ return;
2018
+ });
2019
+ }
2020
+ async readJson(path, fallback) {
2021
+ try {
2022
+ const raw = await readFile(path, "utf-8");
2023
+ if (!raw.trim())
2024
+ return fallback;
2025
+ return JSON.parse(raw);
2026
+ } catch (error) {
2027
+ if (error.code === "ENOENT")
2028
+ return fallback;
2029
+ throw error;
2030
+ }
2031
+ }
2032
+ async writeJson(path, value) {
2033
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
2034
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
2035
+ `, { encoding: "utf-8", mode: 384 });
2036
+ await rename(tempPath, path);
2037
+ await chmod(path, 384).catch(() => {
2038
+ return;
2039
+ });
2040
+ }
2041
+ }
2042
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
2043
+ function buildSignatureBase(timestamp, body) {
2044
+ return `${timestamp}.${body}`;
2045
+ }
2046
+ function signPayload(secret, timestamp, body) {
2047
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
2048
+ return `sha256=${digest}`;
2049
+ }
2050
+ function now() {
2051
+ return new Date().toISOString();
2052
+ }
2053
+ function truncate(value, max = 4096) {
2054
+ return value.length > max ? `${value.slice(0, max)}...` : value;
2055
+ }
2056
+ function buildWebhookRequest(event, channel) {
2057
+ if (!channel.webhook)
2058
+ throw new Error(`Channel ${channel.id} has no webhook config`);
2059
+ const body = JSON.stringify(event);
2060
+ const timestamp = event.time;
2061
+ const headers = {
2062
+ "Content-Type": "application/json",
2063
+ "User-Agent": "@hasna/events",
2064
+ "X-Hasna-Event-Id": event.id,
2065
+ "X-Hasna-Event-Type": event.type,
2066
+ "X-Hasna-Timestamp": timestamp,
2067
+ ...channel.webhook.headers
2068
+ };
2069
+ if (channel.webhook.secret) {
2070
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
2071
+ }
2072
+ return { body, headers };
2073
+ }
2074
+ async function dispatchWebhook(event, channel, options = {}) {
2075
+ if (!channel.webhook)
2076
+ throw new Error(`Channel ${channel.id} has no webhook config`);
2077
+ const startedAt = now();
2078
+ const { body, headers } = buildWebhookRequest(event, channel);
2079
+ const controller = new AbortController;
2080
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
2081
+ try {
2082
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
2083
+ method: "POST",
2084
+ headers,
2085
+ body,
2086
+ signal: controller.signal
2087
+ });
2088
+ const responseBody = truncate(await response.text());
2089
+ return {
2090
+ attempt: 1,
2091
+ status: response.ok ? "success" : "failed",
2092
+ startedAt,
2093
+ completedAt: now(),
2094
+ responseStatus: response.status,
2095
+ responseBody,
2096
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
2097
+ };
2098
+ } catch (error) {
2099
+ return {
2100
+ attempt: 1,
2101
+ status: "failed",
2102
+ startedAt,
2103
+ completedAt: now(),
2104
+ error: error instanceof Error ? error.message : String(error)
2105
+ };
2106
+ } finally {
2107
+ clearTimeout(timeout);
2108
+ }
2109
+ }
2110
+ async function dispatchCommand(event, channel) {
2111
+ if (!channel.command)
2112
+ throw new Error(`Channel ${channel.id} has no command config`);
2113
+ const startedAt = now();
2114
+ const eventJson = JSON.stringify(event);
2115
+ const env = {
2116
+ ...process.env,
2117
+ ...channel.command.env,
2118
+ HASNA_CHANNEL_ID: channel.id,
2119
+ HASNA_EVENT_ID: event.id,
2120
+ HASNA_EVENT_TYPE: event.type,
2121
+ HASNA_EVENT_SOURCE: event.source,
2122
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
2123
+ HASNA_EVENT_SEVERITY: event.severity,
2124
+ HASNA_EVENT_TIME: event.time,
2125
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
2126
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
2127
+ HASNA_EVENT_JSON: eventJson
2128
+ };
2129
+ return new Promise((resolve) => {
2130
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
2131
+ cwd: channel.command.cwd,
2132
+ env,
2133
+ stdio: ["pipe", "pipe", "pipe"]
2134
+ });
2135
+ let stdout = "";
2136
+ let stderr = "";
2137
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
2138
+ child.stdin.end(eventJson);
2139
+ child.stdout.on("data", (chunk) => {
2140
+ stdout += chunk.toString();
2141
+ });
2142
+ child.stderr.on("data", (chunk) => {
2143
+ stderr += chunk.toString();
2144
+ });
2145
+ child.on("error", (error) => {
2146
+ clearTimeout(timeout);
2147
+ resolve({
2148
+ attempt: 1,
2149
+ status: "failed",
2150
+ startedAt,
2151
+ completedAt: now(),
2152
+ stdout: truncate(stdout),
2153
+ stderr: truncate(stderr),
2154
+ error: error.message
2155
+ });
2156
+ });
2157
+ child.on("close", (code, signal) => {
2158
+ clearTimeout(timeout);
2159
+ const success = code === 0;
2160
+ resolve({
2161
+ attempt: 1,
2162
+ status: success ? "success" : "failed",
2163
+ startedAt,
2164
+ completedAt: now(),
2165
+ stdout: truncate(stdout),
2166
+ stderr: truncate(stderr),
2167
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
2168
+ });
2169
+ });
2170
+ });
2171
+ }
2172
+ async function dispatchChannel(event, channel, options = {}) {
2173
+ if (channel.transport === "webhook")
2174
+ return dispatchWebhook(event, channel, options);
2175
+ if (channel.transport === "command")
2176
+ return dispatchCommand(event, channel);
2177
+ return {
2178
+ attempt: 1,
2179
+ status: "skipped",
2180
+ startedAt: now(),
2181
+ completedAt: now(),
2182
+ error: `Unsupported transport: ${channel.transport}`
2183
+ };
2184
+ }
2185
+ function createDeliveryResult(event, channel, attempts) {
2186
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
2187
+ return {
2188
+ id: randomUUID(),
2189
+ eventId: event.id,
2190
+ channelId: channel.id,
2191
+ transport: channel.transport,
2192
+ status,
2193
+ attempts,
2194
+ createdAt: attempts[0]?.startedAt ?? now(),
2195
+ completedAt: attempts.at(-1)?.completedAt ?? now()
2196
+ };
2197
+ }
2198
+ function createEvent(input) {
2199
+ return {
2200
+ id: input.id ?? randomUUID2(),
2201
+ source: input.source,
2202
+ type: input.type,
2203
+ time: normalizeTime(input.time),
2204
+ subject: input.subject,
2205
+ severity: input.severity ?? "info",
2206
+ data: input.data ?? {},
2207
+ message: input.message,
2208
+ dedupeKey: input.dedupeKey,
2209
+ schemaVersion: input.schemaVersion ?? "1.0",
2210
+ metadata: input.metadata ?? {}
2211
+ };
2212
+ }
2213
+
2214
+ class EventsClient {
2215
+ store;
2216
+ redactors;
2217
+ transportOptions;
2218
+ constructor(options = {}) {
2219
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
2220
+ this.redactors = options.redactors ?? [];
2221
+ this.transportOptions = { fetchImpl: options.fetchImpl };
2222
+ }
2223
+ async addChannel(input) {
2224
+ const timestamp = new Date().toISOString();
2225
+ return this.store.addChannel({
2226
+ ...input,
2227
+ createdAt: input.createdAt ?? timestamp,
2228
+ updatedAt: input.updatedAt ?? timestamp
2229
+ });
2230
+ }
2231
+ async listChannels() {
2232
+ return this.store.listChannels();
2233
+ }
2234
+ async removeChannel(id) {
2235
+ return this.store.removeChannel(id);
2236
+ }
2237
+ async emit(input, options = {}) {
2238
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
2239
+ if (options.dedupe !== false) {
2240
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
2241
+ if (existing) {
2242
+ return { event: existing, deliveries: [], deduped: true };
2243
+ }
2244
+ }
2245
+ await this.store.appendEvent(event);
2246
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
2247
+ return { event, deliveries, deduped: false };
2248
+ }
2249
+ async listEvents() {
2250
+ return this.store.listEvents();
2251
+ }
2252
+ async listDeliveries() {
2253
+ return this.store.listDeliveries();
2254
+ }
2255
+ async deliver(event) {
2256
+ const channels = await this.store.listChannels();
2257
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
2258
+ const deliveries = [];
2259
+ for (const channel of selected) {
2260
+ const eventForChannel = await this.applyRedaction(event, channel);
2261
+ const result = await this.deliverWithRetry(eventForChannel, channel);
2262
+ await this.store.appendDelivery(result);
2263
+ deliveries.push(result);
2264
+ }
2265
+ return deliveries;
2266
+ }
2267
+ async testChannel(id, input = {}) {
2268
+ const channel = await this.store.getChannel(id);
2269
+ if (!channel)
2270
+ throw new Error(`Channel not found: ${id}`);
2271
+ const event = createEvent({
2272
+ source: input.source ?? "hasna.events",
2273
+ type: input.type ?? "events.test",
2274
+ subject: input.subject ?? id,
2275
+ severity: input.severity ?? "info",
2276
+ data: input.data ?? { test: true },
2277
+ message: input.message ?? "Hasna events test delivery",
2278
+ dedupeKey: input.dedupeKey,
2279
+ schemaVersion: input.schemaVersion,
2280
+ metadata: input.metadata,
2281
+ time: input.time,
2282
+ id: input.id
2283
+ });
2284
+ const eventForChannel = await this.applyRedaction(event, channel);
2285
+ const result = await this.deliverWithRetry(eventForChannel, channel);
2286
+ await this.store.appendDelivery(result);
2287
+ return result;
2288
+ }
2289
+ async replay(options = {}) {
2290
+ const events = (await this.store.listEvents()).filter((event) => {
2291
+ if (options.eventId && event.id !== options.eventId)
2292
+ return false;
2293
+ if (options.source && event.source !== options.source)
2294
+ return false;
2295
+ if (options.type && event.type !== options.type)
2296
+ return false;
2297
+ return true;
2298
+ });
2299
+ if (options.dryRun)
2300
+ return { events, deliveries: [] };
2301
+ const deliveries = [];
2302
+ for (const event of events) {
2303
+ deliveries.push(...await this.deliver(event));
2304
+ }
2305
+ return { events, deliveries };
2306
+ }
2307
+ async applyRedaction(event, channel) {
2308
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
2309
+ for (const redactor of this.redactors) {
2310
+ next = await redactor(next, channel);
2311
+ }
2312
+ return next;
2313
+ }
2314
+ async deliverWithRetry(event, channel) {
2315
+ const policy = normalizeRetryPolicy(channel.retry);
2316
+ const attempts = [];
2317
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
2318
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
2319
+ attempt.attempt = index + 1;
2320
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
2321
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
2322
+ }
2323
+ attempts.push(attempt);
2324
+ if (attempt.status !== "failed")
2325
+ break;
2326
+ if (attempt.nextBackoffMs)
2327
+ await Bun.sleep(attempt.nextBackoffMs);
2328
+ }
2329
+ return createDeliveryResult(event, channel, attempts);
2330
+ }
2331
+ }
2332
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
2333
+ if (paths.length === 0)
2334
+ return event;
2335
+ const copy = structuredClone(event);
2336
+ for (const path of paths) {
2337
+ setPath(copy, path, replacement);
2338
+ }
2339
+ return copy;
2340
+ }
2341
+ function sanitizeChannelForOutput(channel) {
2342
+ const copy = structuredClone(channel);
2343
+ if (copy.webhook?.secret)
2344
+ copy.webhook.secret = "[REDACTED]";
2345
+ if (copy.command?.env) {
2346
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
2347
+ }
2348
+ return copy;
2349
+ }
2350
+ function sanitizeChannelsForOutput(channels) {
2351
+ return channels.map(sanitizeChannelForOutput);
2352
+ }
2353
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
2354
+ return redactValue(event, replacement);
2355
+ }
2356
+ function shouldRedactKey(key) {
2357
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
2358
+ }
2359
+ function redactValue(value, replacement) {
2360
+ if (Array.isArray(value))
2361
+ return value.map((item) => redactValue(item, replacement));
2362
+ if (!value || typeof value !== "object")
2363
+ return value;
2364
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
2365
+ key,
2366
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
2367
+ ]));
2368
+ }
2369
+ function setPath(input, path, replacement) {
2370
+ const parts = path.split(".");
2371
+ let cursor = input;
2372
+ for (const part of parts.slice(0, -1)) {
2373
+ const next = cursor[part];
2374
+ if (!next || typeof next !== "object")
2375
+ return;
2376
+ cursor = next;
2377
+ }
2378
+ const last = parts.at(-1);
2379
+ if (last && last in cursor)
2380
+ cursor[last] = replacement;
2381
+ }
2382
+ function normalizeTime(value) {
2383
+ if (!value)
2384
+ return new Date().toISOString();
2385
+ return value instanceof Date ? value.toISOString() : value;
2386
+ }
2387
+ function normalizeRetryPolicy(policy) {
2388
+ return {
2389
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
2390
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
2391
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
2392
+ };
2393
+ }
2394
+ function parseJsonObject(value, fallback) {
2395
+ if (!value)
2396
+ return fallback;
2397
+ const parsed = JSON.parse(value);
2398
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2399
+ throw new Error("Expected a JSON object");
2400
+ }
2401
+ return parsed;
2402
+ }
2403
+ function parseHeaders(values) {
2404
+ if (!values?.length)
2405
+ return;
2406
+ const headers = {};
2407
+ for (const value of values) {
2408
+ const separator = value.indexOf("=");
2409
+ if (separator === -1)
2410
+ throw new Error(`Invalid header, expected name=value: ${value}`);
2411
+ headers[value.slice(0, separator)] = value.slice(separator + 1);
2412
+ }
2413
+ return headers;
2414
+ }
2415
+ function parseFilter(options) {
2416
+ const filter2 = {};
2417
+ if (options.source)
2418
+ filter2.source = options.source;
2419
+ if (options.type)
2420
+ filter2.type = options.type;
2421
+ if (options.subject)
2422
+ filter2.subject = options.subject;
2423
+ if (options.severity)
2424
+ filter2.severity = options.severity;
2425
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
2426
+ }
2427
+ function createClient(options) {
2428
+ if (options.createClient)
2429
+ return options.createClient();
2430
+ return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
2431
+ }
2432
+ function print(value, json, text) {
2433
+ if (json)
2434
+ console.log(JSON.stringify(value, null, 2));
2435
+ else
2436
+ console.log(text);
2437
+ }
2438
+ function hasJsonOption(options) {
2439
+ return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
2440
+ }
2441
+ function wantsJson(actionOptions, command) {
2442
+ return hasJsonOption(actionOptions) || hasJsonOption(command);
2443
+ }
2444
+ function registerWebhookCommands(program, options) {
2445
+ const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
2446
+ 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, command) => {
2447
+ const timestamp = new Date().toISOString();
2448
+ const channel = {
2449
+ id: actionOptions.id,
2450
+ name: actionOptions.name,
2451
+ enabled: !actionOptions.disabled,
2452
+ transport: actionOptions.transport,
2453
+ filters: parseFilter(actionOptions),
2454
+ retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
2455
+ redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
2456
+ createdAt: timestamp,
2457
+ updatedAt: timestamp
2458
+ };
2459
+ if (actionOptions.transport === "webhook") {
2460
+ channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
2461
+ } else if (actionOptions.transport === "command") {
2462
+ channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
2463
+ } else {
2464
+ throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
2465
+ }
2466
+ const saved = await createClient(options).addChannel(channel);
2467
+ print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
2468
+ });
2469
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
2470
+ const channels = await createClient(options).listChannels();
2471
+ if (wantsJson(actionOptions, command)) {
2472
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
2473
+ return;
2474
+ }
2475
+ if (!channels.length) {
2476
+ console.log("No channels configured.");
2477
+ return;
2478
+ }
2479
+ for (const channel of channels) {
2480
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
2481
+ }
2482
+ });
2483
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
2484
+ const removed = await createClient(options).removeChannel(id);
2485
+ print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
2486
+ });
2487
+ 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, command) => {
2488
+ const result = await createClient(options).testChannel(id, {
2489
+ source: options.source,
2490
+ type: actionOptions.type,
2491
+ subject: actionOptions.subject ?? id,
2492
+ message: actionOptions.message,
2493
+ data: parseJsonObject(actionOptions.data, { test: true })
2494
+ });
2495
+ print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
2496
+ });
2497
+ return webhooks;
2498
+ }
2499
+ function registerEventCommands(program, options) {
2500
+ const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
2501
+ 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, command) => {
2502
+ const result = await createClient(options).emit({
2503
+ source: actionOptions.source ?? options.source,
2504
+ type,
2505
+ subject: actionOptions.subject,
2506
+ severity: actionOptions.severity,
2507
+ message: actionOptions.message,
2508
+ dedupeKey: actionOptions.dedupeKey,
2509
+ data: parseJsonObject(actionOptions.data, {}),
2510
+ metadata: parseJsonObject(actionOptions.metadata, {})
2511
+ }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
2512
+ print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
2513
+ });
2514
+ 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, command) => {
2515
+ let rows = await createClient(options).listEvents();
2516
+ if (actionOptions.source)
2517
+ rows = rows.filter((event) => event.source === actionOptions.source);
2518
+ if (actionOptions.type)
2519
+ rows = rows.filter((event) => event.type === actionOptions.type);
2520
+ if (actionOptions.limit)
2521
+ rows = rows.slice(-actionOptions.limit);
2522
+ if (wantsJson(actionOptions, command)) {
2523
+ console.log(JSON.stringify(rows, null, 2));
2524
+ return;
2525
+ }
2526
+ if (!rows.length) {
2527
+ console.log("No events recorded.");
2528
+ return;
2529
+ }
2530
+ for (const event of rows)
2531
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
2532
+ });
2533
+ 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, command) => {
2534
+ const result = await createClient(options).replay({
2535
+ eventId: actionOptions.id,
2536
+ source: actionOptions.source,
2537
+ type: actionOptions.type,
2538
+ dryRun: actionOptions.dryRun
2539
+ });
2540
+ print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
2541
+ });
2542
+ return events;
2543
+ }
2544
+ function registerEventsCommands(program, options) {
2545
+ registerWebhookCommands(program, options);
2546
+ registerEventCommands(program, options);
2547
+ }
2548
+ function parseNumber(value) {
2549
+ const parsed = Number(value);
2550
+ if (!Number.isFinite(parsed))
2551
+ throw new Error(`Expected a number, got ${value}`);
2552
+ return parsed;
2553
+ }
2554
+ function collectValues(value, previous) {
2555
+ previous.push(value);
2556
+ return previous;
2557
+ }
2558
+
1875
2559
  // src/cli/index.tsx
1876
2560
  import fs from "fs";
1877
2561
  import path from "path";
@@ -2385,7 +3069,7 @@ var {
2385
3069
  // src/lib/installer.ts
2386
3070
  import { execFileSync, execSync } from "child_process";
2387
3071
  import { accessSync, constants } from "fs";
2388
- import { join } from "path";
3072
+ import { join as join2 } from "path";
2389
3073
 
2390
3074
  // src/lib/registry.ts
2391
3075
  var MICROSERVICES = [
@@ -2814,13 +3498,13 @@ function searchMicroservices(query) {
2814
3498
  function getBunGlobalBinDir(env2 = process.env) {
2815
3499
  const bunInstall = env2.BUN_INSTALL?.trim();
2816
3500
  if (bunInstall) {
2817
- return join(bunInstall, "bin");
3501
+ return join2(bunInstall, "bin");
2818
3502
  }
2819
3503
  const home = env2.HOME?.trim();
2820
3504
  if (!home) {
2821
3505
  throw new Error("Unable to determine Bun global bin path: HOME is not set.");
2822
3506
  }
2823
- return join(home, ".bun", "bin");
3507
+ return join2(home, ".bun", "bin");
2824
3508
  }
2825
3509
  function resolveMicroserviceBinary(name) {
2826
3510
  const meta = getMicroservice(name);
@@ -2828,7 +3512,7 @@ function resolveMicroserviceBinary(name) {
2828
3512
  return null;
2829
3513
  const binDir = getBunGlobalBinDir();
2830
3514
  const accessMode = process.platform === "win32" ? constants.F_OK : constants.X_OK;
2831
- const candidates = process.platform === "win32" ? [join(binDir, `${meta.binary}.cmd`), join(binDir, `${meta.binary}.exe`)] : [join(binDir, meta.binary)];
3515
+ const candidates = process.platform === "win32" ? [join2(binDir, `${meta.binary}.cmd`), join2(binDir, `${meta.binary}.exe`)] : [join2(binDir, meta.binary)];
2832
3516
  for (const candidate of candidates) {
2833
3517
  try {
2834
3518
  accessSync(candidate, accessMode);
@@ -2902,15 +3586,15 @@ function getMicroserviceStatus(name) {
2902
3586
  }
2903
3587
 
2904
3588
  // src/lib/package-info.ts
2905
- import { existsSync, readFileSync } from "fs";
2906
- import { dirname, join as join2, resolve } from "path";
3589
+ import { existsSync as existsSync2, readFileSync } from "fs";
3590
+ import { dirname, join as join3, resolve } from "path";
2907
3591
  import { fileURLToPath } from "url";
2908
3592
  var DEFAULT_VERSION = "0.0.1";
2909
3593
  function findNearestPackageJson(startDir) {
2910
3594
  let dir = resolve(startDir);
2911
3595
  while (true) {
2912
- const candidate = join2(dir, "package.json");
2913
- if (existsSync(candidate)) {
3596
+ const candidate = join3(dir, "package.json");
3597
+ if (existsSync2(candidate)) {
2914
3598
  return candidate;
2915
3599
  }
2916
3600
  const parent = dirname(dir);
@@ -3169,7 +3853,7 @@ program2.command("serve-all").description("Start HTTP servers for all installed
3169
3853
  console.log(source_default.bold(`
3170
3854
  Starting ${installed.length} installed microservices...
3171
3855
  `));
3172
- const { spawn } = await import("child_process");
3856
+ const { spawn: spawn2 } = await import("child_process");
3173
3857
  const procs = [];
3174
3858
  const colors = [
3175
3859
  source_default.cyan,
@@ -3182,7 +3866,7 @@ Starting ${installed.length} installed microservices...
3182
3866
  const m = installed[i];
3183
3867
  const color = colors[i % colors.length];
3184
3868
  const prefix = color(`[${m.name.padEnd(10)}] `);
3185
- const proc = spawn(m.binary, ["serve"], {
3869
+ const proc = spawn2(m.binary, ["serve"], {
3186
3870
  env: process.env,
3187
3871
  stdio: ["ignore", "pipe", "pipe"]
3188
3872
  });
@@ -3391,4 +4075,5 @@ Scaffolding microservice-${name}...
3391
4075
  4. Run 'bun run build'
3392
4076
  `));
3393
4077
  });
4078
+ registerEventsCommands(program2, { source: "microservices" });
3394
4079
  program2.parse();
package/bin/mcp.js CHANGED
@@ -13816,7 +13816,7 @@ class JSONSchemaGenerator {
13816
13816
  if (val === undefined) {
13817
13817
  if (this.unrepresentable === "throw") {
13818
13818
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
13819
- } else {}
13819
+ }
13820
13820
  } else if (typeof val === "bigint") {
13821
13821
  if (this.unrepresentable === "throw") {
13822
13822
  throw new Error("BigInt literals cannot be represented in JSON Schema");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/microservices",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "21 production-grade microservice building blocks for AI-native SaaS — auth, billing, LLM gateway, agent registry, RAG, guardrails, tracing, and more. Each with PostgreSQL, HTTP API, MCP server, and CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,6 +63,7 @@
63
63
  "typescript": "^5"
64
64
  },
65
65
  "dependencies": {
66
+ "@hasna/events": "^0.1.6",
66
67
  "@modelcontextprotocol/sdk": "^1.26.0",
67
68
  "chalk": "^5.3.0",
68
69
  "commander": "^12.1.0",