@hasna/events 0.1.9 → 0.1.10

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
@@ -207,8 +207,11 @@ events webhooks remove ops
207
207
  Field filters can match nested `data` or `metadata` values. Plain
208
208
  `--data`/`--metadata` values are strings, which keeps ids and slugs such as
209
209
  `001` intact. Use `--data-json` or `--metadata-json` for typed JSON predicates.
210
- Dot paths access nested object keys; dots inside key names are not escaped yet,
211
- and array traversal is not special-cased. Wildcard behavior stays broad for
210
+ Dot paths access nested object keys; dots inside key names are not escaped yet.
211
+ When the actual event value is an array, string filters match any primitive
212
+ array member, which is useful for tag routing such as `data.tags=auto:route`.
213
+ Use `path!=value` or `path!=json` for negative predicates such as
214
+ `metadata-json 'automation.no_auto!=true'`. Wildcard behavior stays broad for
212
215
  legacy source/type/subject filters. For field paths ending in `_path` or `.path`,
213
216
  `*` matches one path segment and `**` matches recursively.
214
217
 
@@ -218,12 +221,24 @@ events webhooks add loops \
218
221
  --transport command \
219
222
  --source todos \
220
223
  --type task.created \
224
+ --timeout-ms 15000 \
225
+ --retry-attempts 3 \
226
+ --retry-backoff-ms 500 \
221
227
  --metadata 'project_path=/home/hasna/workspace/hasna/opensource/*' \
222
228
  --metadata-json 'route_enabled=true' \
229
+ --metadata-json 'automation.no_auto!=true' \
230
+ --data 'tags=auto:route' \
223
231
  --arg events \
224
232
  --arg handle \
225
233
  --arg todos-task
226
234
 
235
+ # Command args that begin with dashes can be passed either form:
236
+ events webhooks add events --id json-route --transport command --arg --json
237
+ events webhooks add events --id json-route --transport command --arg=--json
238
+
239
+ # For nested CLIs, put child positional args and flags after an explicit delimiter.
240
+ events webhooks add events --id nested-route --transport command -- handle todos-task --json
241
+
227
242
  events webhooks match open-source-task-route \
228
243
  --source todos \
229
244
  --type task.created \
package/dist/cli/index.js CHANGED
@@ -48,14 +48,40 @@ function matchRecord(input, matcher) {
48
48
  return true;
49
49
  return Object.entries(matcher).every(([path, expected]) => {
50
50
  const actual = getPathValue(input, path);
51
- if (typeof expected === "string" || Array.isArray(expected)) {
52
- return matchString(actual === undefined ? undefined : String(actual), expected, {
53
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
54
- });
55
- }
56
- return actual === expected;
51
+ return matchField(actual, expected, path);
57
52
  });
58
53
  }
54
+ function matchField(actual, expected, path) {
55
+ if (isNegativeMatcher(expected)) {
56
+ return !matchPositiveField(actual, expected.not, path);
57
+ }
58
+ return matchPositiveField(actual, expected, path);
59
+ }
60
+ function matchPositiveField(actual, expected, path) {
61
+ if (typeof expected === "string" || Array.isArray(expected)) {
62
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
63
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
64
+ }));
65
+ }
66
+ if (Array.isArray(actual)) {
67
+ return actual.some((item) => item === expected);
68
+ }
69
+ return actual === expected;
70
+ }
71
+ function stringCandidates(actual) {
72
+ if (actual === undefined)
73
+ return [];
74
+ if (Array.isArray(actual)) {
75
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
76
+ }
77
+ return [String(actual)];
78
+ }
79
+ function isPrimitiveFieldValue(value) {
80
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
81
+ }
82
+ function isNegativeMatcher(value) {
83
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
84
+ }
59
85
  function eventMatchesFilter(event, filter) {
60
86
  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);
61
87
  }
@@ -640,13 +666,12 @@ function parseFieldMatchers(values, label, typed = false) {
640
666
  return;
641
667
  const result = {};
642
668
  for (const value of values) {
643
- const separator = value.indexOf("=");
644
- if (separator <= 0)
645
- throw new Error(`Invalid ${label} filter, expected path=value: ${value}`);
646
- const path = value.slice(0, separator);
669
+ const parsed = parseMatcherExpression(value, label);
670
+ const path = parsed.path;
647
671
  if (path in result)
648
672
  throw new Error(`Duplicate ${label} filter path: ${path}`);
649
- result[path] = typed ? parseTypedMatcherValue(value.slice(separator + 1), label) : value.slice(separator + 1);
673
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
674
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
650
675
  }
651
676
  return result;
652
677
  }
@@ -688,6 +713,24 @@ function parseTypedMatcherValue(value, label) {
688
713
  }
689
714
  throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
690
715
  }
716
+ function parseMatcherExpression(value, label) {
717
+ const negativeSeparator = value.indexOf("!=");
718
+ if (negativeSeparator > 0) {
719
+ return {
720
+ path: value.slice(0, negativeSeparator),
721
+ rawValue: value.slice(negativeSeparator + 2),
722
+ negated: true
723
+ };
724
+ }
725
+ const separator = value.indexOf("=");
726
+ if (separator <= 0)
727
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
728
+ return {
729
+ path: value.slice(0, separator),
730
+ rawValue: value.slice(separator + 1),
731
+ negated: false
732
+ };
733
+ }
691
734
 
692
735
  // src/cli/index.ts
693
736
  function version() {
@@ -704,12 +747,23 @@ function parseGlobalArgs(argv) {
704
747
  let dir;
705
748
  for (let index = 0;index < argv.length; index += 1) {
706
749
  const arg = argv[index];
750
+ if (arg === "--") {
751
+ rest.push(...argv.slice(index + 1));
752
+ break;
753
+ }
754
+ if (!arg.startsWith("-")) {
755
+ rest.push(...argv.slice(index));
756
+ break;
757
+ }
707
758
  if (arg === "--json" || arg === "-j") {
708
759
  json = true;
760
+ } else if (arg.startsWith("--dir=")) {
761
+ dir = arg.slice("--dir=".length);
709
762
  } else if (arg === "--dir") {
710
763
  dir = argv[++index];
711
764
  } else {
712
- rest.push(arg);
765
+ rest.push(...argv.slice(index));
766
+ break;
713
767
  }
714
768
  }
715
769
  return { json, dir, rest };
@@ -827,10 +881,10 @@ Options:
827
881
  --source <source> Event source filter
828
882
  --subject <subject> Event subject filter
829
883
  --severity <severity> Event severity filter
830
- --data <path=value> Event data field filter, repeatable; string values, dot paths, * segment wildcard, ** recursive wildcard
831
- --metadata <path=value> Event metadata field filter, repeatable; string values, dot paths, * segment wildcard, ** recursive wildcard
832
- --data-json <path=json> Event data field filter with typed JSON value
833
- --metadata-json <path=json> Event metadata field filter with typed JSON value
884
+ --data <path=value|path!=value> Event data field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
885
+ --metadata <path=value|path!=value> Event metadata field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
886
+ --data-json <path=json|path!=json> Event data field filter with typed JSON value
887
+ --metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
834
888
  --honor-filters On webhooks test, skip delivery when the sample event does not match filters
835
889
  --transport <kind> webhook or command
836
890
  --secret <secret> Webhook signing secret
@@ -838,6 +892,41 @@ Options:
838
892
  --redact <path> Redaction path, repeatable
839
893
  --no-deliver Available on events emit`);
840
894
  }
895
+ function printWebhookAddHelp(options = {}) {
896
+ const name = commandName(options);
897
+ console.log(`${name} webhooks add
898
+
899
+ Usage:
900
+ ${name} [--dir <path>] [--json] webhooks add <url|command> [options]
901
+ ${name} [--dir <path>] [--json] webhooks add <command> --transport command [options] -- [command-args...]
902
+
903
+ Options:
904
+ --id <id> Channel id
905
+ --name <name> Display name
906
+ --transport <kind> webhook or command
907
+ --type <pattern> Event type filter, supports wildcards
908
+ --source <source> Event source filter
909
+ --subject <subject> Event subject filter
910
+ --severity <severity> Event severity filter
911
+ --data <path=value|path!=value> Event data field filter, repeatable
912
+ --metadata <path=value|path!=value> Event metadata field filter, repeatable
913
+ --data-json <path=json|path!=json> Event data field filter with typed JSON value
914
+ --metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
915
+ --secret <secret> Webhook signing secret
916
+ --header <name=value> Webhook header, repeatable
917
+ --arg <arg> Command argument, repeatable; values may begin with dashes
918
+ --timeout-ms <ms> Transport timeout in milliseconds
919
+ --retry-attempts <n> Maximum delivery attempts
920
+ --retry-backoff-ms <ms> Initial retry backoff in milliseconds
921
+ --redact <path> Redaction path, repeatable
922
+ --disabled Create channel disabled
923
+
924
+ Examples:
925
+ ${name} webhooks add https://example.com/webhooks/hasna --id ops --retry-attempts 3 --retry-backoff-ms 500
926
+ ${name} webhooks add bun --id command-hook --transport command --arg run --arg ./handler.ts --arg --json
927
+ ${name} webhooks add bun --id command-hook --transport command --arg=--json
928
+ ${name} webhooks add bun --id command-hook --transport command -- run ./handler.ts --json`);
929
+ }
841
930
  function printEventsHelp(options = {}) {
842
931
  const name = commandName(options);
843
932
  console.log(`${name} events
@@ -884,6 +973,10 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
884
973
  printWebhooksHelp(options);
885
974
  return;
886
975
  }
976
+ if (command === "add" && (tail[0] === "--help" || tail[0] === "-h")) {
977
+ printWebhookAddHelp(options);
978
+ return;
979
+ }
887
980
  if (tail.includes("--help") || tail.includes("-h")) {
888
981
  printWebhooksHelp(options);
889
982
  return;
@@ -907,7 +1000,7 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
907
1000
  }
908
1001
  async function handleWebhooks(client, command, tail, parsed, options) {
909
1002
  if (command === "add") {
910
- const args = [...tail];
1003
+ const { args, delimiterArgs } = splitDelimiter(tail);
911
1004
  const transport = takeOption(args, "--transport") ?? "webhook";
912
1005
  const id = takeOption(args, "--id") ?? crypto.randomUUID();
913
1006
  const name = takeOption(args, "--name");
@@ -938,7 +1031,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
938
1031
  if (transport === "webhook") {
939
1032
  channel.webhook = { url: target, secret, headers: parseHeaders(headerValues), timeoutMs };
940
1033
  } else if (transport === "command") {
941
- channel.command = { command: target, args: [...args.slice(1), ...commandArgs], timeoutMs };
1034
+ channel.command = { command: target, args: [...args.slice(1), ...commandArgs, ...delimiterArgs], timeoutMs };
942
1035
  } else {
943
1036
  throw new Error(`Transport ${transport} is reserved for future use and cannot be added yet`);
944
1037
  }
@@ -1011,6 +1104,15 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1011
1104
  }
1012
1105
  throw new Error(`Unknown webhooks command: ${command ?? ""}`);
1013
1106
  }
1107
+ function splitDelimiter(values) {
1108
+ const delimiterIndex = values.indexOf("--");
1109
+ if (delimiterIndex === -1)
1110
+ return { args: [...values], delimiterArgs: [] };
1111
+ return {
1112
+ args: values.slice(0, delimiterIndex),
1113
+ delimiterArgs: values.slice(delimiterIndex + 1)
1114
+ };
1115
+ }
1014
1116
  async function handleEvents(client, command, tail, parsed, options) {
1015
1117
  if (command === "emit") {
1016
1118
  const args = [...tail];
package/dist/commander.js CHANGED
@@ -38,14 +38,40 @@ function matchRecord(input, matcher) {
38
38
  return true;
39
39
  return Object.entries(matcher).every(([path, expected]) => {
40
40
  const actual = getPathValue(input, path);
41
- if (typeof expected === "string" || Array.isArray(expected)) {
42
- return matchString(actual === undefined ? undefined : String(actual), expected, {
43
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
44
- });
45
- }
46
- return actual === expected;
41
+ return matchField(actual, expected, path);
47
42
  });
48
43
  }
44
+ function matchField(actual, expected, path) {
45
+ if (isNegativeMatcher(expected)) {
46
+ return !matchPositiveField(actual, expected.not, path);
47
+ }
48
+ return matchPositiveField(actual, expected, path);
49
+ }
50
+ function matchPositiveField(actual, expected, path) {
51
+ if (typeof expected === "string" || Array.isArray(expected)) {
52
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
53
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
54
+ }));
55
+ }
56
+ if (Array.isArray(actual)) {
57
+ return actual.some((item) => item === expected);
58
+ }
59
+ return actual === expected;
60
+ }
61
+ function stringCandidates(actual) {
62
+ if (actual === undefined)
63
+ return [];
64
+ if (Array.isArray(actual)) {
65
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
66
+ }
67
+ return [String(actual)];
68
+ }
69
+ function isPrimitiveFieldValue(value) {
70
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
71
+ }
72
+ function isNegativeMatcher(value) {
73
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
74
+ }
49
75
  function eventMatchesFilter(event, filter) {
50
76
  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);
51
77
  }
@@ -651,13 +677,12 @@ function parseFieldMatchers(values, label, typed = false) {
651
677
  return;
652
678
  const result = {};
653
679
  for (const value of values) {
654
- const separator = value.indexOf("=");
655
- if (separator <= 0)
656
- throw new Error(`Invalid ${label} filter, expected path=value: ${value}`);
657
- const path = value.slice(0, separator);
680
+ const parsed = parseMatcherExpression(value, label);
681
+ const path = parsed.path;
658
682
  if (path in result)
659
683
  throw new Error(`Duplicate ${label} filter path: ${path}`);
660
- result[path] = typed ? parseTypedMatcherValue(value.slice(separator + 1), label) : value.slice(separator + 1);
684
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
685
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
661
686
  }
662
687
  return result;
663
688
  }
@@ -699,6 +724,24 @@ function parseTypedMatcherValue(value, label) {
699
724
  }
700
725
  throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
701
726
  }
727
+ function parseMatcherExpression(value, label) {
728
+ const negativeSeparator = value.indexOf("!=");
729
+ if (negativeSeparator > 0) {
730
+ return {
731
+ path: value.slice(0, negativeSeparator),
732
+ rawValue: value.slice(negativeSeparator + 2),
733
+ negated: true
734
+ };
735
+ }
736
+ const separator = value.indexOf("=");
737
+ if (separator <= 0)
738
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
739
+ return {
740
+ path: value.slice(0, separator),
741
+ rawValue: value.slice(separator + 1),
742
+ negated: false
743
+ };
744
+ }
702
745
 
703
746
  // src/commander.ts
704
747
  function parseJsonObject(value, fallback) {
@@ -741,7 +784,7 @@ function wantsJson(actionOptions, command) {
741
784
  }
742
785
  function registerWebhookCommands(program, options) {
743
786
  const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
744
- 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("--data <path=value...>", "Event data field filter; string values, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value", collectValues, []).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) => {
787
+ 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("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).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) => {
745
788
  const timestamp = new Date().toISOString();
746
789
  const channel = {
747
790
  id: actionOptions.id,
@@ -1,5 +1,7 @@
1
- import type { EventFilter, StringMatcher } from "./types.js";
2
- type MatcherValue = StringMatcher | number | boolean | null;
1
+ import type { EventFilter, FieldMatcherValue } from "./types.js";
2
+ type MatcherValue = FieldMatcherValue | {
3
+ not: FieldMatcherValue;
4
+ };
3
5
  export interface FilterOptionInput {
4
6
  source?: string;
5
7
  type?: string;
package/dist/filter.js CHANGED
@@ -38,14 +38,40 @@ function matchRecord(input, matcher) {
38
38
  return true;
39
39
  return Object.entries(matcher).every(([path, expected]) => {
40
40
  const actual = getPathValue(input, path);
41
- if (typeof expected === "string" || Array.isArray(expected)) {
42
- return matchString(actual === undefined ? undefined : String(actual), expected, {
43
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
44
- });
45
- }
46
- return actual === expected;
41
+ return matchField(actual, expected, path);
47
42
  });
48
43
  }
44
+ function matchField(actual, expected, path) {
45
+ if (isNegativeMatcher(expected)) {
46
+ return !matchPositiveField(actual, expected.not, path);
47
+ }
48
+ return matchPositiveField(actual, expected, path);
49
+ }
50
+ function matchPositiveField(actual, expected, path) {
51
+ if (typeof expected === "string" || Array.isArray(expected)) {
52
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
53
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
54
+ }));
55
+ }
56
+ if (Array.isArray(actual)) {
57
+ return actual.some((item) => item === expected);
58
+ }
59
+ return actual === expected;
60
+ }
61
+ function stringCandidates(actual) {
62
+ if (actual === undefined)
63
+ return [];
64
+ if (Array.isArray(actual)) {
65
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
66
+ }
67
+ return [String(actual)];
68
+ }
69
+ function isPrimitiveFieldValue(value) {
70
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
71
+ }
72
+ function isNegativeMatcher(value) {
73
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
74
+ }
49
75
  function eventMatchesFilter(event, filter) {
50
76
  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);
51
77
  }
package/dist/index.js CHANGED
@@ -38,14 +38,40 @@ function matchRecord(input, matcher) {
38
38
  return true;
39
39
  return Object.entries(matcher).every(([path, expected]) => {
40
40
  const actual = getPathValue(input, path);
41
- if (typeof expected === "string" || Array.isArray(expected)) {
42
- return matchString(actual === undefined ? undefined : String(actual), expected, {
43
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
44
- });
45
- }
46
- return actual === expected;
41
+ return matchField(actual, expected, path);
47
42
  });
48
43
  }
44
+ function matchField(actual, expected, path) {
45
+ if (isNegativeMatcher(expected)) {
46
+ return !matchPositiveField(actual, expected.not, path);
47
+ }
48
+ return matchPositiveField(actual, expected, path);
49
+ }
50
+ function matchPositiveField(actual, expected, path) {
51
+ if (typeof expected === "string" || Array.isArray(expected)) {
52
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
53
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
54
+ }));
55
+ }
56
+ if (Array.isArray(actual)) {
57
+ return actual.some((item) => item === expected);
58
+ }
59
+ return actual === expected;
60
+ }
61
+ function stringCandidates(actual) {
62
+ if (actual === undefined)
63
+ return [];
64
+ if (Array.isArray(actual)) {
65
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
66
+ }
67
+ return [String(actual)];
68
+ }
69
+ function isPrimitiveFieldValue(value) {
70
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
71
+ }
72
+ function isNegativeMatcher(value) {
73
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
74
+ }
49
75
  function eventMatchesFilter(event, filter) {
50
76
  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);
51
77
  }
package/dist/types.d.ts CHANGED
@@ -27,13 +27,18 @@ export interface EventInput<TData extends EventData = EventData> {
27
27
  metadata?: Record<string, unknown>;
28
28
  }
29
29
  export type StringMatcher = string | string[];
30
+ export type FieldMatcherValue = StringMatcher | number | boolean | null;
31
+ export interface NegativeFieldMatcher {
32
+ not: FieldMatcherValue;
33
+ }
34
+ export type FieldMatcher = FieldMatcherValue | NegativeFieldMatcher;
30
35
  export interface EventFilter {
31
36
  source?: StringMatcher;
32
37
  type?: StringMatcher;
33
38
  subject?: StringMatcher;
34
39
  severity?: StringMatcher;
35
- data?: Record<string, StringMatcher | number | boolean | null>;
36
- metadata?: Record<string, StringMatcher | number | boolean | null>;
40
+ data?: Record<string, FieldMatcher>;
41
+ metadata?: Record<string, FieldMatcher>;
37
42
  }
38
43
  export interface RetryPolicy {
39
44
  maxAttempts?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/events",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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",