@hasna/events 0.1.9 → 0.1.11
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 +17 -2
- package/dist/cli/index.js +134 -19
- package/dist/commander.js +69 -13
- package/dist/filter-options.d.ts +4 -2
- package/dist/filter.js +46 -7
- package/dist/index.js +46 -7
- package/dist/types.d.ts +7 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
@@ -18,6 +18,19 @@ function getPathValue(input, path) {
|
|
|
18
18
|
return;
|
|
19
19
|
}, input);
|
|
20
20
|
}
|
|
21
|
+
function getFieldValues(input, path) {
|
|
22
|
+
const values = [];
|
|
23
|
+
const push = (value) => {
|
|
24
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
25
|
+
values.push(value);
|
|
26
|
+
};
|
|
27
|
+
if (path.includes(".") && path in input)
|
|
28
|
+
push(input[path]);
|
|
29
|
+
const nestedValue = getPathValue(input, path);
|
|
30
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
31
|
+
push(nestedValue);
|
|
32
|
+
return values;
|
|
33
|
+
}
|
|
21
34
|
function wildcardToRegExp(pattern, options = {}) {
|
|
22
35
|
let body = "";
|
|
23
36
|
for (let index = 0;index < pattern.length; index += 1) {
|
|
@@ -47,15 +60,41 @@ function matchRecord(input, matcher) {
|
|
|
47
60
|
if (!matcher)
|
|
48
61
|
return true;
|
|
49
62
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
53
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return actual === expected;
|
|
63
|
+
const actualValues = getFieldValues(input, path);
|
|
64
|
+
return matchField(actualValues, expected, path);
|
|
57
65
|
});
|
|
58
66
|
}
|
|
67
|
+
function matchField(actualValues, expected, path) {
|
|
68
|
+
if (isNegativeMatcher(expected)) {
|
|
69
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
70
|
+
}
|
|
71
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
72
|
+
}
|
|
73
|
+
function matchPositiveField(actual, expected, path) {
|
|
74
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
75
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
76
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(actual)) {
|
|
80
|
+
return actual.some((item) => item === expected);
|
|
81
|
+
}
|
|
82
|
+
return actual === expected;
|
|
83
|
+
}
|
|
84
|
+
function stringCandidates(actual) {
|
|
85
|
+
if (actual === undefined)
|
|
86
|
+
return [];
|
|
87
|
+
if (Array.isArray(actual)) {
|
|
88
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
89
|
+
}
|
|
90
|
+
return [String(actual)];
|
|
91
|
+
}
|
|
92
|
+
function isPrimitiveFieldValue(value) {
|
|
93
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
94
|
+
}
|
|
95
|
+
function isNegativeMatcher(value) {
|
|
96
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
97
|
+
}
|
|
59
98
|
function eventMatchesFilter(event, filter) {
|
|
60
99
|
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
100
|
}
|
|
@@ -640,13 +679,12 @@ function parseFieldMatchers(values, label, typed = false) {
|
|
|
640
679
|
return;
|
|
641
680
|
const result = {};
|
|
642
681
|
for (const value of values) {
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
throw new Error(`Invalid ${label} filter, expected path=value: ${value}`);
|
|
646
|
-
const path = value.slice(0, separator);
|
|
682
|
+
const parsed = parseMatcherExpression(value, label);
|
|
683
|
+
const path = parsed.path;
|
|
647
684
|
if (path in result)
|
|
648
685
|
throw new Error(`Duplicate ${label} filter path: ${path}`);
|
|
649
|
-
|
|
686
|
+
const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
|
|
687
|
+
result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
|
|
650
688
|
}
|
|
651
689
|
return result;
|
|
652
690
|
}
|
|
@@ -688,6 +726,24 @@ function parseTypedMatcherValue(value, label) {
|
|
|
688
726
|
}
|
|
689
727
|
throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
|
|
690
728
|
}
|
|
729
|
+
function parseMatcherExpression(value, label) {
|
|
730
|
+
const negativeSeparator = value.indexOf("!=");
|
|
731
|
+
if (negativeSeparator > 0) {
|
|
732
|
+
return {
|
|
733
|
+
path: value.slice(0, negativeSeparator),
|
|
734
|
+
rawValue: value.slice(negativeSeparator + 2),
|
|
735
|
+
negated: true
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const separator = value.indexOf("=");
|
|
739
|
+
if (separator <= 0)
|
|
740
|
+
throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
|
|
741
|
+
return {
|
|
742
|
+
path: value.slice(0, separator),
|
|
743
|
+
rawValue: value.slice(separator + 1),
|
|
744
|
+
negated: false
|
|
745
|
+
};
|
|
746
|
+
}
|
|
691
747
|
|
|
692
748
|
// src/cli/index.ts
|
|
693
749
|
function version() {
|
|
@@ -704,12 +760,23 @@ function parseGlobalArgs(argv) {
|
|
|
704
760
|
let dir;
|
|
705
761
|
for (let index = 0;index < argv.length; index += 1) {
|
|
706
762
|
const arg = argv[index];
|
|
763
|
+
if (arg === "--") {
|
|
764
|
+
rest.push(...argv.slice(index + 1));
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
if (!arg.startsWith("-")) {
|
|
768
|
+
rest.push(...argv.slice(index));
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
707
771
|
if (arg === "--json" || arg === "-j") {
|
|
708
772
|
json = true;
|
|
773
|
+
} else if (arg.startsWith("--dir=")) {
|
|
774
|
+
dir = arg.slice("--dir=".length);
|
|
709
775
|
} else if (arg === "--dir") {
|
|
710
776
|
dir = argv[++index];
|
|
711
777
|
} else {
|
|
712
|
-
rest.push(
|
|
778
|
+
rest.push(...argv.slice(index));
|
|
779
|
+
break;
|
|
713
780
|
}
|
|
714
781
|
}
|
|
715
782
|
return { json, dir, rest };
|
|
@@ -827,10 +894,10 @@ Options:
|
|
|
827
894
|
--source <source> Event source filter
|
|
828
895
|
--subject <subject> Event subject filter
|
|
829
896
|
--severity <severity> Event severity filter
|
|
830
|
-
--data <path=value>
|
|
831
|
-
--metadata <path=value>
|
|
832
|
-
--data-json <path=json>
|
|
833
|
-
--metadata-json <path=json> Event metadata field filter with typed JSON value
|
|
897
|
+
--data <path=value|path!=value> Event data field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
|
|
898
|
+
--metadata <path=value|path!=value> Event metadata field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
|
|
899
|
+
--data-json <path=json|path!=json> Event data field filter with typed JSON value
|
|
900
|
+
--metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
|
|
834
901
|
--honor-filters On webhooks test, skip delivery when the sample event does not match filters
|
|
835
902
|
--transport <kind> webhook or command
|
|
836
903
|
--secret <secret> Webhook signing secret
|
|
@@ -838,6 +905,41 @@ Options:
|
|
|
838
905
|
--redact <path> Redaction path, repeatable
|
|
839
906
|
--no-deliver Available on events emit`);
|
|
840
907
|
}
|
|
908
|
+
function printWebhookAddHelp(options = {}) {
|
|
909
|
+
const name = commandName(options);
|
|
910
|
+
console.log(`${name} webhooks add
|
|
911
|
+
|
|
912
|
+
Usage:
|
|
913
|
+
${name} [--dir <path>] [--json] webhooks add <url|command> [options]
|
|
914
|
+
${name} [--dir <path>] [--json] webhooks add <command> --transport command [options] -- [command-args...]
|
|
915
|
+
|
|
916
|
+
Options:
|
|
917
|
+
--id <id> Channel id
|
|
918
|
+
--name <name> Display name
|
|
919
|
+
--transport <kind> webhook or command
|
|
920
|
+
--type <pattern> Event type filter, supports wildcards
|
|
921
|
+
--source <source> Event source filter
|
|
922
|
+
--subject <subject> Event subject filter
|
|
923
|
+
--severity <severity> Event severity filter
|
|
924
|
+
--data <path=value|path!=value> Event data field filter, repeatable
|
|
925
|
+
--metadata <path=value|path!=value> Event metadata field filter, repeatable
|
|
926
|
+
--data-json <path=json|path!=json> Event data field filter with typed JSON value
|
|
927
|
+
--metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
|
|
928
|
+
--secret <secret> Webhook signing secret
|
|
929
|
+
--header <name=value> Webhook header, repeatable
|
|
930
|
+
--arg <arg> Command argument, repeatable; values may begin with dashes
|
|
931
|
+
--timeout-ms <ms> Transport timeout in milliseconds
|
|
932
|
+
--retry-attempts <n> Maximum delivery attempts
|
|
933
|
+
--retry-backoff-ms <ms> Initial retry backoff in milliseconds
|
|
934
|
+
--redact <path> Redaction path, repeatable
|
|
935
|
+
--disabled Create channel disabled
|
|
936
|
+
|
|
937
|
+
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`);
|
|
942
|
+
}
|
|
841
943
|
function printEventsHelp(options = {}) {
|
|
842
944
|
const name = commandName(options);
|
|
843
945
|
console.log(`${name} events
|
|
@@ -884,6 +986,10 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
|
|
|
884
986
|
printWebhooksHelp(options);
|
|
885
987
|
return;
|
|
886
988
|
}
|
|
989
|
+
if (command === "add" && (tail[0] === "--help" || tail[0] === "-h")) {
|
|
990
|
+
printWebhookAddHelp(options);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
887
993
|
if (tail.includes("--help") || tail.includes("-h")) {
|
|
888
994
|
printWebhooksHelp(options);
|
|
889
995
|
return;
|
|
@@ -907,7 +1013,7 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
|
|
|
907
1013
|
}
|
|
908
1014
|
async function handleWebhooks(client, command, tail, parsed, options) {
|
|
909
1015
|
if (command === "add") {
|
|
910
|
-
const args =
|
|
1016
|
+
const { args, delimiterArgs } = splitDelimiter(tail);
|
|
911
1017
|
const transport = takeOption(args, "--transport") ?? "webhook";
|
|
912
1018
|
const id = takeOption(args, "--id") ?? crypto.randomUUID();
|
|
913
1019
|
const name = takeOption(args, "--name");
|
|
@@ -938,7 +1044,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
938
1044
|
if (transport === "webhook") {
|
|
939
1045
|
channel.webhook = { url: target, secret, headers: parseHeaders(headerValues), timeoutMs };
|
|
940
1046
|
} else if (transport === "command") {
|
|
941
|
-
channel.command = { command: target, args: [...args.slice(1), ...commandArgs], timeoutMs };
|
|
1047
|
+
channel.command = { command: target, args: [...args.slice(1), ...commandArgs, ...delimiterArgs], timeoutMs };
|
|
942
1048
|
} else {
|
|
943
1049
|
throw new Error(`Transport ${transport} is reserved for future use and cannot be added yet`);
|
|
944
1050
|
}
|
|
@@ -1011,6 +1117,15 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
1011
1117
|
}
|
|
1012
1118
|
throw new Error(`Unknown webhooks command: ${command ?? ""}`);
|
|
1013
1119
|
}
|
|
1120
|
+
function splitDelimiter(values) {
|
|
1121
|
+
const delimiterIndex = values.indexOf("--");
|
|
1122
|
+
if (delimiterIndex === -1)
|
|
1123
|
+
return { args: [...values], delimiterArgs: [] };
|
|
1124
|
+
return {
|
|
1125
|
+
args: values.slice(0, delimiterIndex),
|
|
1126
|
+
delimiterArgs: values.slice(delimiterIndex + 1)
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1014
1129
|
async function handleEvents(client, command, tail, parsed, options) {
|
|
1015
1130
|
if (command === "emit") {
|
|
1016
1131
|
const args = [...tail];
|
package/dist/commander.js
CHANGED
|
@@ -8,6 +8,19 @@ function getPathValue(input, path) {
|
|
|
8
8
|
return;
|
|
9
9
|
}, input);
|
|
10
10
|
}
|
|
11
|
+
function getFieldValues(input, path) {
|
|
12
|
+
const values = [];
|
|
13
|
+
const push = (value) => {
|
|
14
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
15
|
+
values.push(value);
|
|
16
|
+
};
|
|
17
|
+
if (path.includes(".") && path in input)
|
|
18
|
+
push(input[path]);
|
|
19
|
+
const nestedValue = getPathValue(input, path);
|
|
20
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
21
|
+
push(nestedValue);
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
11
24
|
function wildcardToRegExp(pattern, options = {}) {
|
|
12
25
|
let body = "";
|
|
13
26
|
for (let index = 0;index < pattern.length; index += 1) {
|
|
@@ -37,15 +50,41 @@ function matchRecord(input, matcher) {
|
|
|
37
50
|
if (!matcher)
|
|
38
51
|
return true;
|
|
39
52
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
43
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return actual === expected;
|
|
53
|
+
const actualValues = getFieldValues(input, path);
|
|
54
|
+
return matchField(actualValues, expected, path);
|
|
47
55
|
});
|
|
48
56
|
}
|
|
57
|
+
function matchField(actualValues, expected, path) {
|
|
58
|
+
if (isNegativeMatcher(expected)) {
|
|
59
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
60
|
+
}
|
|
61
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
62
|
+
}
|
|
63
|
+
function matchPositiveField(actual, expected, path) {
|
|
64
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
65
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
66
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(actual)) {
|
|
70
|
+
return actual.some((item) => item === expected);
|
|
71
|
+
}
|
|
72
|
+
return actual === expected;
|
|
73
|
+
}
|
|
74
|
+
function stringCandidates(actual) {
|
|
75
|
+
if (actual === undefined)
|
|
76
|
+
return [];
|
|
77
|
+
if (Array.isArray(actual)) {
|
|
78
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
79
|
+
}
|
|
80
|
+
return [String(actual)];
|
|
81
|
+
}
|
|
82
|
+
function isPrimitiveFieldValue(value) {
|
|
83
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
84
|
+
}
|
|
85
|
+
function isNegativeMatcher(value) {
|
|
86
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
87
|
+
}
|
|
49
88
|
function eventMatchesFilter(event, filter) {
|
|
50
89
|
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
90
|
}
|
|
@@ -651,13 +690,12 @@ function parseFieldMatchers(values, label, typed = false) {
|
|
|
651
690
|
return;
|
|
652
691
|
const result = {};
|
|
653
692
|
for (const value of values) {
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
throw new Error(`Invalid ${label} filter, expected path=value: ${value}`);
|
|
657
|
-
const path = value.slice(0, separator);
|
|
693
|
+
const parsed = parseMatcherExpression(value, label);
|
|
694
|
+
const path = parsed.path;
|
|
658
695
|
if (path in result)
|
|
659
696
|
throw new Error(`Duplicate ${label} filter path: ${path}`);
|
|
660
|
-
|
|
697
|
+
const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
|
|
698
|
+
result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
|
|
661
699
|
}
|
|
662
700
|
return result;
|
|
663
701
|
}
|
|
@@ -699,6 +737,24 @@ function parseTypedMatcherValue(value, label) {
|
|
|
699
737
|
}
|
|
700
738
|
throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
|
|
701
739
|
}
|
|
740
|
+
function parseMatcherExpression(value, label) {
|
|
741
|
+
const negativeSeparator = value.indexOf("!=");
|
|
742
|
+
if (negativeSeparator > 0) {
|
|
743
|
+
return {
|
|
744
|
+
path: value.slice(0, negativeSeparator),
|
|
745
|
+
rawValue: value.slice(negativeSeparator + 2),
|
|
746
|
+
negated: true
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const separator = value.indexOf("=");
|
|
750
|
+
if (separator <= 0)
|
|
751
|
+
throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
|
|
752
|
+
return {
|
|
753
|
+
path: value.slice(0, separator),
|
|
754
|
+
rawValue: value.slice(separator + 1),
|
|
755
|
+
negated: false
|
|
756
|
+
};
|
|
757
|
+
}
|
|
702
758
|
|
|
703
759
|
// src/commander.ts
|
|
704
760
|
function parseJsonObject(value, fallback) {
|
|
@@ -741,7 +797,7 @@ function wantsJson(actionOptions, command) {
|
|
|
741
797
|
}
|
|
742
798
|
function registerWebhookCommands(program, options) {
|
|
743
799
|
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) => {
|
|
800
|
+
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
801
|
const timestamp = new Date().toISOString();
|
|
746
802
|
const channel = {
|
|
747
803
|
id: actionOptions.id,
|
package/dist/filter-options.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { EventFilter,
|
|
2
|
-
type MatcherValue =
|
|
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
|
@@ -8,6 +8,19 @@ function getPathValue(input, path) {
|
|
|
8
8
|
return;
|
|
9
9
|
}, input);
|
|
10
10
|
}
|
|
11
|
+
function getFieldValues(input, path) {
|
|
12
|
+
const values = [];
|
|
13
|
+
const push = (value) => {
|
|
14
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
15
|
+
values.push(value);
|
|
16
|
+
};
|
|
17
|
+
if (path.includes(".") && path in input)
|
|
18
|
+
push(input[path]);
|
|
19
|
+
const nestedValue = getPathValue(input, path);
|
|
20
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
21
|
+
push(nestedValue);
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
11
24
|
function wildcardToRegExp(pattern, options = {}) {
|
|
12
25
|
let body = "";
|
|
13
26
|
for (let index = 0;index < pattern.length; index += 1) {
|
|
@@ -37,15 +50,41 @@ function matchRecord(input, matcher) {
|
|
|
37
50
|
if (!matcher)
|
|
38
51
|
return true;
|
|
39
52
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
43
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return actual === expected;
|
|
53
|
+
const actualValues = getFieldValues(input, path);
|
|
54
|
+
return matchField(actualValues, expected, path);
|
|
47
55
|
});
|
|
48
56
|
}
|
|
57
|
+
function matchField(actualValues, expected, path) {
|
|
58
|
+
if (isNegativeMatcher(expected)) {
|
|
59
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
60
|
+
}
|
|
61
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
62
|
+
}
|
|
63
|
+
function matchPositiveField(actual, expected, path) {
|
|
64
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
65
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
66
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(actual)) {
|
|
70
|
+
return actual.some((item) => item === expected);
|
|
71
|
+
}
|
|
72
|
+
return actual === expected;
|
|
73
|
+
}
|
|
74
|
+
function stringCandidates(actual) {
|
|
75
|
+
if (actual === undefined)
|
|
76
|
+
return [];
|
|
77
|
+
if (Array.isArray(actual)) {
|
|
78
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
79
|
+
}
|
|
80
|
+
return [String(actual)];
|
|
81
|
+
}
|
|
82
|
+
function isPrimitiveFieldValue(value) {
|
|
83
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
84
|
+
}
|
|
85
|
+
function isNegativeMatcher(value) {
|
|
86
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
87
|
+
}
|
|
49
88
|
function eventMatchesFilter(event, filter) {
|
|
50
89
|
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
90
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,19 @@ function getPathValue(input, path) {
|
|
|
8
8
|
return;
|
|
9
9
|
}, input);
|
|
10
10
|
}
|
|
11
|
+
function getFieldValues(input, path) {
|
|
12
|
+
const values = [];
|
|
13
|
+
const push = (value) => {
|
|
14
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
15
|
+
values.push(value);
|
|
16
|
+
};
|
|
17
|
+
if (path.includes(".") && path in input)
|
|
18
|
+
push(input[path]);
|
|
19
|
+
const nestedValue = getPathValue(input, path);
|
|
20
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
21
|
+
push(nestedValue);
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
11
24
|
function wildcardToRegExp(pattern, options = {}) {
|
|
12
25
|
let body = "";
|
|
13
26
|
for (let index = 0;index < pattern.length; index += 1) {
|
|
@@ -37,15 +50,41 @@ function matchRecord(input, matcher) {
|
|
|
37
50
|
if (!matcher)
|
|
38
51
|
return true;
|
|
39
52
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
43
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return actual === expected;
|
|
53
|
+
const actualValues = getFieldValues(input, path);
|
|
54
|
+
return matchField(actualValues, expected, path);
|
|
47
55
|
});
|
|
48
56
|
}
|
|
57
|
+
function matchField(actualValues, expected, path) {
|
|
58
|
+
if (isNegativeMatcher(expected)) {
|
|
59
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
60
|
+
}
|
|
61
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
62
|
+
}
|
|
63
|
+
function matchPositiveField(actual, expected, path) {
|
|
64
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
65
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
66
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(actual)) {
|
|
70
|
+
return actual.some((item) => item === expected);
|
|
71
|
+
}
|
|
72
|
+
return actual === expected;
|
|
73
|
+
}
|
|
74
|
+
function stringCandidates(actual) {
|
|
75
|
+
if (actual === undefined)
|
|
76
|
+
return [];
|
|
77
|
+
if (Array.isArray(actual)) {
|
|
78
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
79
|
+
}
|
|
80
|
+
return [String(actual)];
|
|
81
|
+
}
|
|
82
|
+
function isPrimitiveFieldValue(value) {
|
|
83
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
84
|
+
}
|
|
85
|
+
function isNegativeMatcher(value) {
|
|
86
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
87
|
+
}
|
|
49
88
|
function eventMatchesFilter(event, filter) {
|
|
50
89
|
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
90
|
}
|
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,
|
|
36
|
-
metadata?: Record<string,
|
|
40
|
+
data?: Record<string, FieldMatcher>;
|
|
41
|
+
metadata?: Record<string, FieldMatcher>;
|
|
37
42
|
}
|
|
38
43
|
export interface RetryPolicy {
|
|
39
44
|
maxAttempts?: number;
|