@hasna/events 0.1.12 → 0.1.14

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
@@ -108,11 +108,15 @@ function channelMatchesEvent(channel, event) {
108
108
 
109
109
  // src/storage.ts
110
110
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
111
+ import { Buffer as Buffer2 } from "buffer";
111
112
  import { existsSync } from "fs";
112
113
  import { homedir } from "os";
113
114
  import { join } from "path";
114
115
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
115
116
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
117
+ var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
118
+ var DEFAULT_EVENT_PAGE_LIMIT = 100;
119
+ var MAX_EVENT_PAGE_LIMIT = 1000;
116
120
  function getEventsDataDir(override) {
117
121
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
118
122
  }
@@ -126,11 +130,13 @@ function getActiveEventsDirEnv() {
126
130
 
127
131
  class JsonEventsStore {
128
132
  dataDir;
133
+ runtime;
129
134
  channelsPath;
130
135
  eventsPath;
131
136
  deliveriesPath;
132
137
  constructor(dataDir = getEventsDataDir()) {
133
138
  this.dataDir = dataDir;
139
+ this.runtime = localJsonRuntime(dataDir);
134
140
  this.channelsPath = join(dataDir, "channels.json");
135
141
  this.eventsPath = join(dataDir, "events.json");
136
142
  this.deliveriesPath = join(dataDir, "deliveries.json");
@@ -178,13 +184,58 @@ class JsonEventsStore {
178
184
  await this.writeJson(this.eventsPath, events);
179
185
  return event;
180
186
  }
181
- async listEvents() {
187
+ async appendEventOnce(event, options = {}) {
182
188
  await this.init();
183
- return this.readJson(this.eventsPath, []);
189
+ const events = await this.readJson(this.eventsPath, []);
190
+ const dedupe = options.dedupe !== false;
191
+ if (dedupe) {
192
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
193
+ if (existing) {
194
+ return {
195
+ event: existing,
196
+ stored: false,
197
+ deduped: true,
198
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
199
+ };
200
+ }
201
+ }
202
+ events.push(event);
203
+ await this.writeJson(this.eventsPath, events);
204
+ return {
205
+ event,
206
+ stored: true,
207
+ deduped: false,
208
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
209
+ };
210
+ }
211
+ async listEvents(options = {}) {
212
+ await this.init();
213
+ const events = await this.readJson(this.eventsPath, []);
214
+ return queryEvents(events, options);
215
+ }
216
+ async listEventsPage(options = {}) {
217
+ await this.init();
218
+ const events = await this.readJson(this.eventsPath, []);
219
+ const queried = queryEvents(events, {
220
+ eventId: options.eventId,
221
+ source: options.source,
222
+ type: options.type
223
+ });
224
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
225
+ const limit = normalizeEventPageLimit(options.limit);
226
+ const pageEvents = queried.slice(offset, offset + limit);
227
+ const nextOffset = offset + pageEvents.length;
228
+ const hasMore = nextOffset < queried.length;
229
+ return {
230
+ events: pageEvents,
231
+ cursor: options.cursor,
232
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
233
+ hasMore
234
+ };
184
235
  }
185
236
  async findEventByIdentity(identity) {
186
237
  const events = await this.listEvents();
187
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
238
+ return findEventByIdentity(events, identity);
188
239
  }
189
240
  async appendDelivery(result) {
190
241
  await this.init();
@@ -235,6 +286,83 @@ class JsonEventsStore {
235
286
  });
236
287
  }
237
288
  }
289
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
290
+ return {
291
+ mode: "local-files",
292
+ name: "json-events-store",
293
+ remote: false,
294
+ localFiles: true,
295
+ localSqlite: false,
296
+ postgres: false,
297
+ s3: false,
298
+ aws: false,
299
+ durable: true,
300
+ idempotency: "best-effort-local",
301
+ replayCursors: true,
302
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
303
+ };
304
+ }
305
+ function encodeLocalJsonEventCursor(offset, options = {}) {
306
+ if (!Number.isInteger(offset) || offset < 0)
307
+ throw new Error(`Invalid event cursor offset: ${offset}`);
308
+ const payload = {
309
+ offset,
310
+ eventId: options.eventId,
311
+ source: options.source,
312
+ type: options.type
313
+ };
314
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
315
+ }
316
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
317
+ if (!cursor)
318
+ return 0;
319
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
320
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
321
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
322
+ let payload;
323
+ try {
324
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
325
+ } catch {
326
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
327
+ }
328
+ const offset = payload.offset;
329
+ if (!Number.isInteger(offset) || offset < 0)
330
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
331
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
332
+ assertCursorFilter("source", payload.source, options.source);
333
+ assertCursorFilter("type", payload.type, options.type);
334
+ return offset;
335
+ }
336
+ function normalizeEventPageLimit(limit) {
337
+ if (limit === undefined)
338
+ return DEFAULT_EVENT_PAGE_LIMIT;
339
+ if (!Number.isInteger(limit) || limit < 1)
340
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
341
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
342
+ }
343
+ function queryEvents(events, options) {
344
+ let rows = events;
345
+ if (options.eventId)
346
+ rows = rows.filter((event) => event.id === options.eventId);
347
+ if (options.source)
348
+ rows = rows.filter((event) => event.source === options.source);
349
+ if (options.type)
350
+ rows = rows.filter((event) => event.type === options.type);
351
+ if (options.cursor) {
352
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
353
+ rows = rows.slice(offset);
354
+ }
355
+ if (options.limit !== undefined)
356
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
357
+ return rows;
358
+ }
359
+ function assertCursorFilter(name, cursorValue, optionValue) {
360
+ if (cursorValue !== optionValue)
361
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
362
+ }
363
+ function findEventByIdentity(events, identity) {
364
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
365
+ }
238
366
  async function getEventsStatus(dataDir) {
239
367
  const store = new JsonEventsStore(dataDir);
240
368
  await store.init();
@@ -251,6 +379,7 @@ async function getEventsStatus(dataDir) {
251
379
  service: "events",
252
380
  schemaVersion: "1.0",
253
381
  dataDir: store.dataDir,
382
+ storage: store.runtime,
254
383
  env: {
255
384
  primary: HASNA_EVENTS_DIR_ENV,
256
385
  fallback: HASNA_EVENTS_HOME_ENV,
@@ -447,6 +576,52 @@ function createDeliveryResult(event, channel, attempts) {
447
576
  };
448
577
  }
449
578
 
579
+ // src/catalog.ts
580
+ class EventValidationError extends Error {
581
+ eventType;
582
+ issues;
583
+ constructor(eventType, issues) {
584
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
585
+ super(`Event validation failed for type "${eventType}": ${detail}`);
586
+ this.name = "EventValidationError";
587
+ this.eventType = eventType;
588
+ this.issues = issues;
589
+ }
590
+ }
591
+
592
+ class EventTypeCatalog {
593
+ definitions = new Map;
594
+ register(definition) {
595
+ this.definitions.set(definition.type, definition);
596
+ return this;
597
+ }
598
+ unregister(type) {
599
+ return this.definitions.delete(type);
600
+ }
601
+ has(type) {
602
+ return this.definitions.has(type);
603
+ }
604
+ get(type) {
605
+ return this.definitions.get(type);
606
+ }
607
+ list() {
608
+ return [...this.definitions.values()];
609
+ }
610
+ validateEvent(event) {
611
+ const definition = this.definitions.get(event.type);
612
+ if (!definition)
613
+ return { ok: true };
614
+ return definition.validate(event.data, event);
615
+ }
616
+ assertEventValid(event) {
617
+ const result = this.validateEvent(event);
618
+ if (!result.ok) {
619
+ throw new EventValidationError(event.type, result.issues);
620
+ }
621
+ }
622
+ }
623
+ var defaultEventTypeCatalog = new EventTypeCatalog;
624
+
450
625
  // src/index.ts
451
626
  function createEvent(input) {
452
627
  return {
@@ -468,10 +643,14 @@ class EventsClient {
468
643
  store;
469
644
  redactors;
470
645
  transportOptions;
646
+ catalog;
647
+ validateCatalogTypes;
471
648
  constructor(options = {}) {
472
649
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
473
650
  this.redactors = options.redactors ?? [];
474
651
  this.transportOptions = { fetchImpl: options.fetchImpl };
652
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
653
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
475
654
  }
476
655
  async addChannel(input) {
477
656
  const timestamp = new Date().toISOString();
@@ -489,18 +668,40 @@ class EventsClient {
489
668
  }
490
669
  async emit(input, options = {}) {
491
670
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
492
- if (options.dedupe !== false) {
493
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
494
- if (existing) {
495
- return { event: existing, deliveries: [], deduped: true };
496
- }
671
+ if (options.validate ?? this.validateCatalogTypes) {
672
+ this.catalog.assertEventValid(event);
673
+ }
674
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
675
+ if (append.deduped) {
676
+ return { event: append.event, deliveries: [], deduped: true };
497
677
  }
498
- await this.store.appendEvent(event);
499
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
500
- return { event, deliveries, deduped: false };
678
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
679
+ return { event: append.event, deliveries, deduped: false };
501
680
  }
502
- async listEvents() {
503
- return this.store.listEvents();
681
+ async listEvents(options = {}) {
682
+ if (Object.keys(options).length === 0)
683
+ return this.store.listEvents();
684
+ return queryClientEvents(await this.store.listEvents(), options);
685
+ }
686
+ async listEventsPage(options = {}) {
687
+ if (this.store.listEventsPage)
688
+ return this.store.listEventsPage(options);
689
+ const events = queryClientEvents(await this.store.listEvents(), {
690
+ eventId: options.eventId,
691
+ source: options.source,
692
+ type: options.type
693
+ });
694
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
695
+ const limit = normalizeEventPageLimit(options.limit);
696
+ const pageEvents = events.slice(offset, offset + limit);
697
+ const nextOffset = offset + pageEvents.length;
698
+ const hasMore = nextOffset < events.length;
699
+ return {
700
+ events: pageEvents,
701
+ cursor: options.cursor,
702
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
703
+ hasMore
704
+ };
504
705
  }
505
706
  async listDeliveries() {
506
707
  return this.store.listDeliveries();
@@ -568,22 +769,37 @@ class EventsClient {
568
769
  return result;
569
770
  }
570
771
  async replay(options = {}) {
571
- const events = (await this.store.listEvents()).filter((event) => {
572
- if (options.eventId && event.id !== options.eventId)
573
- return false;
574
- if (options.source && event.source !== options.source)
575
- return false;
576
- if (options.type && event.type !== options.type)
577
- return false;
578
- return true;
579
- });
772
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
580
773
  if (options.dryRun)
581
- return { events, deliveries: [] };
774
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
582
775
  const deliveries = [];
583
- for (const event of events) {
776
+ for (const event of page.events) {
584
777
  deliveries.push(...await this.deliver(event));
585
778
  }
586
- return { events, deliveries };
779
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
780
+ }
781
+ async appendEvent(event, options) {
782
+ if (this.store.appendEventOnce) {
783
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
784
+ }
785
+ if (options.dedupe) {
786
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
787
+ if (existing) {
788
+ return {
789
+ event: existing,
790
+ stored: false,
791
+ deduped: true,
792
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
793
+ };
794
+ }
795
+ }
796
+ const stored = await this.store.appendEvent(event);
797
+ return {
798
+ event: stored,
799
+ stored: true,
800
+ deduped: false,
801
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
802
+ };
587
803
  }
588
804
  async applyRedaction(event, channel) {
589
805
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -660,6 +876,20 @@ function setPath(input, path, replacement) {
660
876
  if (last && last in cursor)
661
877
  cursor[last] = replacement;
662
878
  }
879
+ function queryClientEvents(events, options) {
880
+ let rows = events;
881
+ if (options.eventId)
882
+ rows = rows.filter((event) => event.id === options.eventId);
883
+ if (options.source)
884
+ rows = rows.filter((event) => event.source === options.source);
885
+ if (options.type)
886
+ rows = rows.filter((event) => event.type === options.type);
887
+ if (options.cursor)
888
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
889
+ if (options.limit !== undefined)
890
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
891
+ return rows;
892
+ }
663
893
  function normalizeTime(value) {
664
894
  if (!value)
665
895
  return new Date().toISOString();
@@ -862,31 +1092,31 @@ function printHelp(options = {}) {
862
1092
  console.log(`${name} ${version()}
863
1093
 
864
1094
  Usage:
865
- ${name} [--dir <path>] [--json] webhooks add <url|command> [options]
866
- ${name} [--dir <path>] [--json] webhooks list
867
- ${name} [--dir <path>] [--json] webhooks remove <id>
868
- ${name} [--dir <path>] [--json] webhooks test <id>
869
- ${name} [--dir <path>] [--json] webhooks match <id>
870
- ${name} [--dir <path>] [--json] webhooks status
1095
+ ${name} [--dir <path>] [--json] channels add <url|command> [options]
1096
+ ${name} [--dir <path>] [--json] channels list
1097
+ ${name} [--dir <path>] [--json] channels remove <id>
1098
+ ${name} [--dir <path>] [--json] channels test <id>
1099
+ ${name} [--dir <path>] [--json] channels match <id>
1100
+ ${name} [--dir <path>] [--json] channels status
871
1101
  ${name} [--dir <path>] [--json] status
872
1102
  ${name} [--dir <path>] [--json] events emit <type>${options.source ? "" : " --source <source>"} [options]
873
1103
  ${name} [--dir <path>] [--json] events list [--limit <n>]
874
- ${name} [--dir <path>] [--json] events replay [--id <event-id>] [--dry-run]
1104
+ ${name} [--dir <path>] [--json] events replay [--id <event-id>] [--cursor <cursor>] [--limit <n>] [--dry-run]
875
1105
 
876
1106
  Environment:
877
1107
  HASNA_EVENTS_DIR or HASNA_EVENTS_HOME overrides the default ${getEventsDataDir()}`);
878
1108
  }
879
- function printWebhooksHelp(options = {}) {
1109
+ function printChannelsHelp(options = {}) {
880
1110
  const name = commandName(options);
881
- console.log(`${name} webhooks
1111
+ console.log(`${name} channels
882
1112
 
883
1113
  Usage:
884
- ${name} [--dir <path>] [--json] webhooks add <url|command> [options]
885
- ${name} [--dir <path>] [--json] webhooks list
886
- ${name} [--dir <path>] [--json] webhooks remove <id>
887
- ${name} [--dir <path>] [--json] webhooks test <id>
888
- ${name} [--dir <path>] [--json] webhooks match <id>
889
- ${name} [--dir <path>] [--json] webhooks status
1114
+ ${name} [--dir <path>] [--json] channels add <url|command> [options]
1115
+ ${name} [--dir <path>] [--json] channels list
1116
+ ${name} [--dir <path>] [--json] channels remove <id>
1117
+ ${name} [--dir <path>] [--json] channels test <id>
1118
+ ${name} [--dir <path>] [--json] channels match <id>
1119
+ ${name} [--dir <path>] [--json] channels status
890
1120
 
891
1121
  Options:
892
1122
  --id <id> Channel id for add
@@ -898,20 +1128,20 @@ Options:
898
1128
  --metadata <path=value|path!=value> Event metadata field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
899
1129
  --data-json <path=json|path!=json> Event data field filter with typed JSON value
900
1130
  --metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
901
- --honor-filters On webhooks test, skip delivery when the sample event does not match filters
1131
+ --honor-filters On channels test, skip delivery when the sample event does not match filters
902
1132
  --transport <kind> webhook or command
903
1133
  --secret <secret> Webhook signing secret
904
1134
  --header <name=value> Webhook header, repeatable
905
1135
  --redact <path> Redaction path, repeatable
906
1136
  --no-deliver Available on events emit`);
907
1137
  }
908
- function printWebhookAddHelp(options = {}) {
1138
+ function printChannelAddHelp(options = {}) {
909
1139
  const name = commandName(options);
910
- console.log(`${name} webhooks add
1140
+ console.log(`${name} channels add
911
1141
 
912
1142
  Usage:
913
- ${name} [--dir <path>] [--json] webhooks add <url|command> [options]
914
- ${name} [--dir <path>] [--json] webhooks add <command> --transport command [options] -- [command-args...]
1143
+ ${name} [--dir <path>] [--json] channels add <url|command> [options]
1144
+ ${name} [--dir <path>] [--json] channels add <command> --transport command [options] -- [command-args...]
915
1145
 
916
1146
  Options:
917
1147
  --id <id> Channel id
@@ -935,10 +1165,10 @@ Options:
935
1165
  --disabled Create channel disabled
936
1166
 
937
1167
  Examples:
938
- ${name} webhooks add https://example.com/webhooks/hasna --id ops --retry-attempts 3 --retry-backoff-ms 500
939
- ${name} webhooks add bun --id command-hook --transport command --arg run --arg ./handler.ts --arg --json
940
- ${name} webhooks add bun --id command-hook --transport command --arg=--json
941
- ${name} webhooks add bun --id command-hook --transport command -- run ./handler.ts --json`);
1168
+ ${name} channels add https://example.com/channels/hasna --id ops --retry-attempts 3 --retry-backoff-ms 500
1169
+ ${name} channels add bun --id command-hook --transport command --arg run --arg ./handler.ts --arg --json
1170
+ ${name} channels add bun --id command-hook --transport command --arg=--json
1171
+ ${name} channels add bun --id command-hook --transport command -- run ./handler.ts --json`);
942
1172
  }
943
1173
  function printEventsHelp(options = {}) {
944
1174
  const name = commandName(options);
@@ -947,7 +1177,7 @@ function printEventsHelp(options = {}) {
947
1177
  Usage:
948
1178
  ${name} [--dir <path>] [--json] events emit <type>${options.source ? "" : " --source <source>"} [options]
949
1179
  ${name} [--dir <path>] [--json] events list [--limit <n>]
950
- ${name} [--dir <path>] [--json] events replay [--id <event-id>] [--dry-run]
1180
+ ${name} [--dir <path>] [--json] events replay [--id <event-id>] [--cursor <cursor>] [--limit <n>] [--dry-run]
951
1181
 
952
1182
  Options:
953
1183
  --source <source> Event source${options.source ? ` (default: ${options.source})` : ""}
@@ -957,7 +1187,9 @@ Options:
957
1187
  --dedupe-key <key> Deduplicate repeated events
958
1188
  --data <json> JSON object payload
959
1189
  --metadata <json> JSON object metadata
960
- --no-deliver Record without delivering webhooks
1190
+ --no-deliver Record without delivering channels
1191
+ --cursor <cursor> Opaque cursor returned by a previous replay page
1192
+ --limit <n> Maximum events to replay
961
1193
  --dry-run Preview replay matches without delivery`);
962
1194
  }
963
1195
  async function runEventsCli(argv = process.argv.slice(2), options = {}) {
@@ -981,20 +1213,20 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
981
1213
  }
982
1214
  const store = new JsonEventsStore(parsed.dir);
983
1215
  const client = new EventsClient({ store });
984
- if (group === "webhooks") {
1216
+ if (group === "channels") {
985
1217
  if (!command || command === "--help" || command === "-h") {
986
- printWebhooksHelp(options);
1218
+ printChannelsHelp(options);
987
1219
  return;
988
1220
  }
989
1221
  if (command === "add" && (tail[0] === "--help" || tail[0] === "-h")) {
990
- printWebhookAddHelp(options);
1222
+ printChannelAddHelp(options);
991
1223
  return;
992
1224
  }
993
1225
  if (tail.includes("--help") || tail.includes("-h")) {
994
- printWebhooksHelp(options);
1226
+ printChannelsHelp(options);
995
1227
  return;
996
1228
  }
997
- await handleWebhooks(client, command, tail, parsed, options);
1229
+ await handleChannels(client, command, tail, parsed, options);
998
1230
  return;
999
1231
  }
1000
1232
  if (group === "events") {
@@ -1011,7 +1243,7 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
1011
1243
  }
1012
1244
  throw new Error(`Unknown command group: ${group}`);
1013
1245
  }
1014
- async function handleWebhooks(client, command, tail, parsed, options) {
1246
+ async function handleChannels(client, command, tail, parsed, options) {
1015
1247
  if (command === "add") {
1016
1248
  const { args, delimiterArgs } = splitDelimiter(tail);
1017
1249
  const transport = takeOption(args, "--transport") ?? "webhook";
@@ -1028,7 +1260,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1028
1260
  const filters = parseFilter(args);
1029
1261
  const target = args[0];
1030
1262
  if (!target)
1031
- throw new Error("webhooks add requires a URL or command target");
1263
+ throw new Error("channels add requires a URL or command target");
1032
1264
  const now2 = new Date().toISOString();
1033
1265
  const channel = {
1034
1266
  id,
@@ -1077,7 +1309,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1077
1309
  if (command === "remove") {
1078
1310
  const id = tail[0];
1079
1311
  if (!id)
1080
- throw new Error("webhooks remove requires a channel id");
1312
+ throw new Error("channels remove requires a channel id");
1081
1313
  const removed = await client.removeChannel(id);
1082
1314
  output(parsed, { removed }, () => console.log(removed ? `Removed ${id}` : `Channel not found: ${id}`));
1083
1315
  return;
@@ -1086,7 +1318,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1086
1318
  const args = [...tail];
1087
1319
  const id = args.shift();
1088
1320
  if (!id)
1089
- throw new Error("webhooks test requires a channel id");
1321
+ throw new Error("channels test requires a channel id");
1090
1322
  const honorFilters = takeFlag(args, "--honor-filters");
1091
1323
  const result = await client.testChannel(id, {
1092
1324
  source: takeOption(args, "--source") ?? options.source ?? "hasna.events",
@@ -1103,7 +1335,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1103
1335
  const args = [...tail];
1104
1336
  const id = args.shift();
1105
1337
  if (!id)
1106
- throw new Error("webhooks match requires a channel id");
1338
+ throw new Error("channels match requires a channel id");
1107
1339
  const result = await client.matchChannel(id, {
1108
1340
  source: takeOption(args, "--source") ?? options.source ?? "hasna.events",
1109
1341
  type: takeOption(args, "--type") ?? "events.test",
@@ -1115,7 +1347,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1115
1347
  output(parsed, result, () => console.log(`${result.matched ? "matched" : "skipped"}: ${result.channelId}`));
1116
1348
  return;
1117
1349
  }
1118
- throw new Error(`Unknown webhooks command: ${command ?? ""}`);
1350
+ throw new Error(`Unknown channels command: ${command ?? ""}`);
1119
1351
  }
1120
1352
  function splitDelimiter(values) {
1121
1353
  const delimiterIndex = values.indexOf("--");
@@ -1178,9 +1410,11 @@ async function handleEvents(client, command, tail, parsed, options) {
1178
1410
  eventId: takeOption(args, "--id"),
1179
1411
  source: takeOption(args, "--source"),
1180
1412
  type: takeOption(args, "--type"),
1413
+ cursor: takeOption(args, "--cursor"),
1414
+ limit: numberOption(takeOption(args, "--limit")),
1181
1415
  dryRun: takeFlag(args, "--dry-run")
1182
1416
  });
1183
- output(parsed, result, () => console.log(`Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`));
1417
+ output(parsed, result, () => console.log(replaySummary(result.events.length, result.deliveries.length, result.nextCursor)));
1184
1418
  return;
1185
1419
  }
1186
1420
  throw new Error(`Unknown events command: ${command ?? ""}`);
@@ -1201,6 +1435,10 @@ function severityOption(value) {
1201
1435
  throw new Error(`Invalid severity: ${value}`);
1202
1436
  return value;
1203
1437
  }
1438
+ function replaySummary(events, deliveries, nextCursor) {
1439
+ const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
1440
+ return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
1441
+ }
1204
1442
  if (import.meta.main) {
1205
1443
  runEventsCli().catch((error) => {
1206
1444
  const parsed = parseGlobalArgs(process.argv.slice(2));
@@ -5,10 +5,24 @@ export interface RegisterEventsCommandsOptions {
5
5
  source: string;
6
6
  dataDir?: string;
7
7
  createClient?: () => EventsClient;
8
- webhooksCommandName?: string;
8
+ channelsCommandName?: string;
9
9
  eventsCommandName?: string;
10
+ /**
11
+ * Default row cap applied to `events list` when the caller does not pass an
12
+ * explicit `--limit`. Guards against dumping the entire event store (a
13
+ * usability/performance hazard for hosts with large stores). Pass `--limit 0`
14
+ * to opt out and list every recorded event. Defaults to
15
+ * {@link DEFAULT_EVENT_LIST_LIMIT}.
16
+ */
17
+ defaultEventListLimit?: number;
10
18
  }
11
- export declare function registerWebhookCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): CommanderCommandLike;
19
+ /**
20
+ * Sane default number of most-recent events returned by `events list` when the
21
+ * host does not configure {@link RegisterEventsCommandsOptions.defaultEventListLimit}
22
+ * and the user does not pass an explicit `--limit`.
23
+ */
24
+ export declare const DEFAULT_EVENT_LIST_LIMIT = 100;
25
+ export declare function registerChannelCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): CommanderCommandLike;
12
26
  export declare function registerEventCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): CommanderCommandLike;
13
27
  export declare function registerEventsCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): void;
14
28
  export {};