@hasna/events 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -219,6 +219,16 @@ events events replay --type ticket.created
219
219
  events events replay --dry-run
220
220
  ```
221
221
 
222
+ Machine-readable status:
223
+
224
+ ```bash
225
+ events status --json
226
+ ```
227
+
228
+ The status contract reports event, channel, delivery, file, and transport counts
229
+ only. It does not include event payloads, webhook signing secrets, command
230
+ environment values, or channel targets.
231
+
222
232
  Use `--json` for script-friendly output and `--dir <path>` for isolated data.
223
233
 
224
234
  ## App Integration Pattern
package/dist/cli/index.js CHANGED
@@ -62,6 +62,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
62
62
  function getEventsDataDir(override) {
63
63
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
64
64
  }
65
+ function getActiveEventsDirEnv() {
66
+ if (process.env[HASNA_EVENTS_DIR_ENV])
67
+ return HASNA_EVENTS_DIR_ENV;
68
+ if (process.env[HASNA_EVENTS_HOME_ENV])
69
+ return HASNA_EVENTS_HOME_ENV;
70
+ return null;
71
+ }
65
72
 
66
73
  class JsonEventsStore {
67
74
  dataDir;
@@ -174,6 +181,52 @@ class JsonEventsStore {
174
181
  });
175
182
  }
176
183
  }
184
+ async function getEventsStatus(dataDir) {
185
+ const store = new JsonEventsStore(dataDir);
186
+ await store.init();
187
+ const [channels, events, deliveries] = await Promise.all([
188
+ store.listChannels(),
189
+ store.listEvents(),
190
+ store.listDeliveries()
191
+ ]);
192
+ const transports = channels.reduce((counts, channel) => {
193
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
194
+ return counts;
195
+ }, {});
196
+ return {
197
+ service: "events",
198
+ schemaVersion: "1.0",
199
+ dataDir: store.dataDir,
200
+ env: {
201
+ primary: HASNA_EVENTS_DIR_ENV,
202
+ fallback: HASNA_EVENTS_HOME_ENV,
203
+ active: getActiveEventsDirEnv()
204
+ },
205
+ files: {
206
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
207
+ events: statusFile(store.dataDir, "events.json", events.length),
208
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
209
+ },
210
+ counts: {
211
+ channels: channels.length,
212
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
213
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
214
+ events: events.length,
215
+ deliveries: deliveries.length
216
+ },
217
+ transports,
218
+ safety: {
219
+ includesEventPayloads: false,
220
+ includesWebhookSecrets: false,
221
+ listOutputsRedactSecrets: true,
222
+ statusOutputIsMetadataOnly: true
223
+ }
224
+ };
225
+ }
226
+ function statusFile(dataDir, fileName, records) {
227
+ const path = join(dataDir, fileName);
228
+ return { path, exists: existsSync(path), records };
229
+ }
177
230
 
178
231
  // src/transports.ts
179
232
  import { randomUUID } from "crypto";
@@ -564,10 +617,19 @@ function parseGlobalArgs(argv) {
564
617
  return { json, dir, rest };
565
618
  }
566
619
  function takeOption(args, name) {
620
+ const equalsPrefix = `${name}=`;
621
+ const equalsIndex = args.findIndex((arg) => arg.startsWith(equalsPrefix));
622
+ if (equalsIndex !== -1) {
623
+ const value2 = args[equalsIndex]?.slice(equalsPrefix.length);
624
+ args.splice(equalsIndex, 1);
625
+ return value2;
626
+ }
567
627
  const index = args.indexOf(name);
568
628
  if (index === -1)
569
629
  return;
570
630
  const value = args[index + 1];
631
+ if (value === undefined)
632
+ throw new Error(`${name} requires a value`);
571
633
  args.splice(index, 2);
572
634
  return value;
573
635
  }
@@ -580,7 +642,7 @@ function takeFlag(args, name) {
580
642
  }
581
643
  function takeMany(args, name) {
582
644
  const values = [];
583
- while (args.includes(name)) {
645
+ while (args.includes(name) || args.some((arg) => arg.startsWith(`${name}=`))) {
584
646
  const value = takeOption(args, name);
585
647
  if (value !== undefined)
586
648
  values.push(value);
@@ -643,6 +705,7 @@ Usage:
643
705
  ${name} [--dir <path>] [--json] webhooks list
644
706
  ${name} [--dir <path>] [--json] webhooks remove <id>
645
707
  ${name} [--dir <path>] [--json] webhooks test <id>
708
+ ${name} [--dir <path>] [--json] status
646
709
  ${name} [--dir <path>] [--json] events emit <type>${options.source ? "" : " --source <source>"} [options]
647
710
  ${name} [--dir <path>] [--json] events list [--limit <n>]
648
711
  ${name} [--dir <path>] [--json] events replay [--id <event-id>] [--dry-run]
@@ -703,6 +766,14 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
703
766
  console.log(version());
704
767
  return;
705
768
  }
769
+ if (group === "status") {
770
+ const status = await getEventsStatus(parsed.dir);
771
+ output(parsed, status, () => {
772
+ console.log(`events ${status.counts.events} event(s), ${status.counts.channels} channel(s), ${status.counts.deliveries} delivery record(s)`);
773
+ console.log(`dataDir: ${status.dataDir}`);
774
+ });
775
+ return;
776
+ }
706
777
  const store = new JsonEventsStore(parsed.dir);
707
778
  const client = new EventsClient({ store });
708
779
  if (group === "webhooks") {
@@ -710,6 +781,10 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
710
781
  printWebhooksHelp(options);
711
782
  return;
712
783
  }
784
+ if (tail.includes("--help") || tail.includes("-h")) {
785
+ printWebhooksHelp(options);
786
+ return;
787
+ }
713
788
  await handleWebhooks(client, command, tail, parsed, options);
714
789
  return;
715
790
  }
@@ -718,6 +793,10 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
718
793
  printEventsHelp(options);
719
794
  return;
720
795
  }
796
+ if (tail.includes("--help") || tail.includes("-h")) {
797
+ printEventsHelp(options);
798
+ return;
799
+ }
721
800
  await handleEvents(client, command, tail, parsed, options);
722
801
  return;
723
802
  }
@@ -735,6 +814,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
735
814
  const retryBackoffMs = numberOption(takeOption(args, "--retry-backoff-ms"));
736
815
  const disabled = takeFlag(args, "--disabled");
737
816
  const headerValues = takeMany(args, "--header");
817
+ const commandArgs = takeMany(args, "--arg");
738
818
  const redactions = takeMany(args, "--redact");
739
819
  const filters = parseFilter(args);
740
820
  const target = args[0];
@@ -755,7 +835,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
755
835
  if (transport === "webhook") {
756
836
  channel.webhook = { url: target, secret, headers: parseHeaders(headerValues), timeoutMs };
757
837
  } else if (transport === "command") {
758
- channel.command = { command: target, args: args.slice(1), timeoutMs };
838
+ channel.command = { command: target, args: [...args.slice(1), ...commandArgs], timeoutMs };
759
839
  } else {
760
840
  throw new Error(`Transport ${transport} is reserved for future use and cannot be added yet`);
761
841
  }
package/dist/commander.js CHANGED
@@ -52,6 +52,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
52
52
  function getEventsDataDir(override) {
53
53
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
54
54
  }
55
+ function getActiveEventsDirEnv() {
56
+ if (process.env[HASNA_EVENTS_DIR_ENV])
57
+ return HASNA_EVENTS_DIR_ENV;
58
+ if (process.env[HASNA_EVENTS_HOME_ENV])
59
+ return HASNA_EVENTS_HOME_ENV;
60
+ return null;
61
+ }
55
62
 
56
63
  class JsonEventsStore {
57
64
  dataDir;
@@ -164,6 +171,52 @@ class JsonEventsStore {
164
171
  });
165
172
  }
166
173
  }
174
+ async function getEventsStatus(dataDir) {
175
+ const store = new JsonEventsStore(dataDir);
176
+ await store.init();
177
+ const [channels, events, deliveries] = await Promise.all([
178
+ store.listChannels(),
179
+ store.listEvents(),
180
+ store.listDeliveries()
181
+ ]);
182
+ const transports = channels.reduce((counts, channel) => {
183
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
184
+ return counts;
185
+ }, {});
186
+ return {
187
+ service: "events",
188
+ schemaVersion: "1.0",
189
+ dataDir: store.dataDir,
190
+ env: {
191
+ primary: HASNA_EVENTS_DIR_ENV,
192
+ fallback: HASNA_EVENTS_HOME_ENV,
193
+ active: getActiveEventsDirEnv()
194
+ },
195
+ files: {
196
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
197
+ events: statusFile(store.dataDir, "events.json", events.length),
198
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
199
+ },
200
+ counts: {
201
+ channels: channels.length,
202
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
203
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
204
+ events: events.length,
205
+ deliveries: deliveries.length
206
+ },
207
+ transports,
208
+ safety: {
209
+ includesEventPayloads: false,
210
+ includesWebhookSecrets: false,
211
+ listOutputsRedactSecrets: true,
212
+ statusOutputIsMetadataOnly: true
213
+ }
214
+ };
215
+ }
216
+ function statusFile(dataDir, fileName, records) {
217
+ const path = join(dataDir, fileName);
218
+ return { path, exists: existsSync(path), records };
219
+ }
167
220
 
168
221
  // src/signing.ts
169
222
  import { createHmac, timingSafeEqual } from "crypto";
package/dist/index.js CHANGED
@@ -52,6 +52,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
52
52
  function getEventsDataDir(override) {
53
53
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
54
54
  }
55
+ function getActiveEventsDirEnv() {
56
+ if (process.env[HASNA_EVENTS_DIR_ENV])
57
+ return HASNA_EVENTS_DIR_ENV;
58
+ if (process.env[HASNA_EVENTS_HOME_ENV])
59
+ return HASNA_EVENTS_HOME_ENV;
60
+ return null;
61
+ }
55
62
 
56
63
  class JsonEventsStore {
57
64
  dataDir;
@@ -164,6 +171,52 @@ class JsonEventsStore {
164
171
  });
165
172
  }
166
173
  }
174
+ async function getEventsStatus(dataDir) {
175
+ const store = new JsonEventsStore(dataDir);
176
+ await store.init();
177
+ const [channels, events, deliveries] = await Promise.all([
178
+ store.listChannels(),
179
+ store.listEvents(),
180
+ store.listDeliveries()
181
+ ]);
182
+ const transports = channels.reduce((counts, channel) => {
183
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
184
+ return counts;
185
+ }, {});
186
+ return {
187
+ service: "events",
188
+ schemaVersion: "1.0",
189
+ dataDir: store.dataDir,
190
+ env: {
191
+ primary: HASNA_EVENTS_DIR_ENV,
192
+ fallback: HASNA_EVENTS_HOME_ENV,
193
+ active: getActiveEventsDirEnv()
194
+ },
195
+ files: {
196
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
197
+ events: statusFile(store.dataDir, "events.json", events.length),
198
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
199
+ },
200
+ counts: {
201
+ channels: channels.length,
202
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
203
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
204
+ events: events.length,
205
+ deliveries: deliveries.length
206
+ },
207
+ transports,
208
+ safety: {
209
+ includesEventPayloads: false,
210
+ includesWebhookSecrets: false,
211
+ listOutputsRedactSecrets: true,
212
+ statusOutputIsMetadataOnly: true
213
+ }
214
+ };
215
+ }
216
+ function statusFile(dataDir, fileName, records) {
217
+ const path = join(dataDir, fileName);
218
+ return { path, exists: existsSync(path), records };
219
+ }
167
220
 
168
221
  // src/signing.ts
169
222
  import { createHmac, timingSafeEqual } from "crypto";
@@ -558,7 +611,9 @@ export {
558
611
  redactPaths,
559
612
  matchString,
560
613
  isTimestampWithinTolerance,
614
+ getEventsStatus,
561
615
  getEventsDataDir,
616
+ getActiveEventsDirEnv,
562
617
  eventMatchesFilter,
563
618
  dispatchWebhook,
564
619
  dispatchCommand,
package/dist/storage.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { ChannelConfig, DeliveryResult, EventEnvelope, StoredEventsData } from "./types.js";
1
+ import type { ChannelConfig, DeliveryResult, EventEnvelope, EventsStatus, StoredEventsData } from "./types.js";
2
2
  export declare const HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
3
3
  export declare const HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
4
4
  export declare function getEventsDataDir(override?: string): string;
5
+ export declare function getActiveEventsDirEnv(): EventsStatus["env"]["active"];
5
6
  export interface EventsStore {
6
7
  dataDir: string;
7
8
  init(): Promise<void>;
@@ -42,3 +43,4 @@ export declare class JsonEventsStore implements EventsStore {
42
43
  private readJson;
43
44
  private writeJson;
44
45
  }
46
+ export declare function getEventsStatus(dataDir?: string): Promise<EventsStatus>;
package/dist/storage.js CHANGED
@@ -9,6 +9,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
9
9
  function getEventsDataDir(override) {
10
10
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
11
11
  }
12
+ function getActiveEventsDirEnv() {
13
+ if (process.env[HASNA_EVENTS_DIR_ENV])
14
+ return HASNA_EVENTS_DIR_ENV;
15
+ if (process.env[HASNA_EVENTS_HOME_ENV])
16
+ return HASNA_EVENTS_HOME_ENV;
17
+ return null;
18
+ }
12
19
 
13
20
  class JsonEventsStore {
14
21
  dataDir;
@@ -121,8 +128,56 @@ class JsonEventsStore {
121
128
  });
122
129
  }
123
130
  }
131
+ async function getEventsStatus(dataDir) {
132
+ const store = new JsonEventsStore(dataDir);
133
+ await store.init();
134
+ const [channels, events, deliveries] = await Promise.all([
135
+ store.listChannels(),
136
+ store.listEvents(),
137
+ store.listDeliveries()
138
+ ]);
139
+ const transports = channels.reduce((counts, channel) => {
140
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
141
+ return counts;
142
+ }, {});
143
+ return {
144
+ service: "events",
145
+ schemaVersion: "1.0",
146
+ dataDir: store.dataDir,
147
+ env: {
148
+ primary: HASNA_EVENTS_DIR_ENV,
149
+ fallback: HASNA_EVENTS_HOME_ENV,
150
+ active: getActiveEventsDirEnv()
151
+ },
152
+ files: {
153
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
154
+ events: statusFile(store.dataDir, "events.json", events.length),
155
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
156
+ },
157
+ counts: {
158
+ channels: channels.length,
159
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
160
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
161
+ events: events.length,
162
+ deliveries: deliveries.length
163
+ },
164
+ transports,
165
+ safety: {
166
+ includesEventPayloads: false,
167
+ includesWebhookSecrets: false,
168
+ listOutputsRedactSecrets: true,
169
+ statusOutputIsMetadataOnly: true
170
+ }
171
+ };
172
+ }
173
+ function statusFile(dataDir, fileName, records) {
174
+ const path = join(dataDir, fileName);
175
+ return { path, exists: existsSync(path), records };
176
+ }
124
177
  export {
178
+ getEventsStatus,
125
179
  getEventsDataDir,
180
+ getActiveEventsDirEnv,
126
181
  JsonEventsStore,
127
182
  HASNA_EVENTS_HOME_ENV,
128
183
  HASNA_EVENTS_DIR_ENV
package/dist/types.d.ts CHANGED
@@ -117,3 +117,44 @@ export interface EmitResult<TData extends EventData = EventData> {
117
117
  deliveries: DeliveryResult[];
118
118
  deduped: boolean;
119
119
  }
120
+ export interface EventsStatus {
121
+ service: "events";
122
+ schemaVersion: "1.0";
123
+ dataDir: string;
124
+ env: {
125
+ primary: "HASNA_EVENTS_DIR";
126
+ fallback: "HASNA_EVENTS_HOME";
127
+ active: "HASNA_EVENTS_DIR" | "HASNA_EVENTS_HOME" | null;
128
+ };
129
+ files: {
130
+ channels: {
131
+ path: string;
132
+ exists: boolean;
133
+ records: number;
134
+ };
135
+ events: {
136
+ path: string;
137
+ exists: boolean;
138
+ records: number;
139
+ };
140
+ deliveries: {
141
+ path: string;
142
+ exists: boolean;
143
+ records: number;
144
+ };
145
+ };
146
+ counts: {
147
+ channels: number;
148
+ enabledChannels: number;
149
+ disabledChannels: number;
150
+ events: number;
151
+ deliveries: number;
152
+ };
153
+ transports: Record<string, number>;
154
+ safety: {
155
+ includesEventPayloads: false;
156
+ includesWebhookSecrets: false;
157
+ listOutputsRedactSecrets: true;
158
+ statusOutputIsMetadataOnly: true;
159
+ };
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/events",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Shared event envelopes, local subscriptions, and webhook delivery for Hasna open-source apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",