@helyx/module-automod 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +725 -0
  3. package/README.md +80 -0
  4. package/dist/activity-resource.d.ts +3 -0
  5. package/dist/activity-resource.js +114 -0
  6. package/dist/case-link-provider.d.ts +3 -0
  7. package/dist/case-link-provider.js +13 -0
  8. package/dist/configuration.d.ts +22 -0
  9. package/dist/configuration.js +85 -0
  10. package/dist/constants.d.ts +30 -0
  11. package/dist/constants.js +37 -0
  12. package/dist/contracts.d.ts +30 -0
  13. package/dist/contracts.js +51 -0
  14. package/dist/domain.d.ts +61 -0
  15. package/dist/domain.js +274 -0
  16. package/dist/engine/canonicalisation.d.ts +9 -0
  17. package/dist/engine/canonicalisation.js +150 -0
  18. package/dist/engine/compile.d.ts +4 -0
  19. package/dist/engine/compile.js +169 -0
  20. package/dist/engine/confidence.d.ts +13 -0
  21. package/dist/engine/confidence.js +57 -0
  22. package/dist/engine/configuration-cache.d.ts +29 -0
  23. package/dist/engine/configuration-cache.js +115 -0
  24. package/dist/engine/contracts.d.ts +82 -0
  25. package/dist/engine/contracts.js +25 -0
  26. package/dist/engine/index.d.ts +11 -0
  27. package/dist/engine/index.js +11 -0
  28. package/dist/engine/matcher.d.ts +3 -0
  29. package/dist/engine/matcher.js +206 -0
  30. package/dist/engine/observations.d.ts +45 -0
  31. package/dist/engine/observations.js +105 -0
  32. package/dist/engine/operation-id.d.ts +18 -0
  33. package/dist/engine/operation-id.js +24 -0
  34. package/dist/engine/similar-message-window.d.ts +68 -0
  35. package/dist/engine/similar-message-window.js +259 -0
  36. package/dist/engine/term-import.d.ts +15 -0
  37. package/dist/engine/term-import.js +43 -0
  38. package/dist/enhanced-configuration.d.ts +16 -0
  39. package/dist/enhanced-configuration.js +91 -0
  40. package/dist/events.d.ts +4 -0
  41. package/dist/events.js +371 -0
  42. package/dist/health.d.ts +9 -0
  43. package/dist/health.js +45 -0
  44. package/dist/index.d.ts +10 -0
  45. package/dist/index.js +141 -0
  46. package/dist/policy-provider.d.ts +3 -0
  47. package/dist/policy-provider.js +66 -0
  48. package/dist/publication-support.d.ts +14 -0
  49. package/dist/publication-support.js +69 -0
  50. package/dist/publication.d.ts +32 -0
  51. package/dist/publication.js +333 -0
  52. package/dist/receipt-statistics.d.ts +11 -0
  53. package/dist/receipt-statistics.js +58 -0
  54. package/dist/records.d.ts +299 -0
  55. package/dist/records.js +288 -0
  56. package/dist/repository-model.d.ts +54 -0
  57. package/dist/repository-model.js +132 -0
  58. package/dist/repository.d.ts +133 -0
  59. package/dist/repository.js +523 -0
  60. package/dist/resource-cursor.d.ts +5 -0
  61. package/dist/resource-cursor.js +45 -0
  62. package/dist/resource-presentation.d.ts +21 -0
  63. package/dist/resource-presentation.js +111 -0
  64. package/dist/resource-validation.d.ts +6 -0
  65. package/dist/resource-validation.js +80 -0
  66. package/dist/resources.d.ts +4 -0
  67. package/dist/resources.js +319 -0
  68. package/dist/scanner-configuration.d.ts +10 -0
  69. package/dist/scanner-configuration.js +146 -0
  70. package/dist/scanner-provider.d.ts +5 -0
  71. package/dist/scanner-provider.js +310 -0
  72. package/dist/scanner-result-validation.d.ts +3 -0
  73. package/dist/scanner-result-validation.js +61 -0
  74. package/dist/settings-preview.d.ts +6 -0
  75. package/dist/settings-preview.js +84 -0
  76. package/manifest.json +1601 -0
  77. package/migrations/0001_automod_standard.sql +114 -0
  78. package/migrations/0002_automod_enhanced_receipts.sql +40 -0
  79. package/package.json +61 -0
@@ -0,0 +1,68 @@
1
+ import { type CompiledEnhancedConfiguration, type EnhancedScanResult } from "./contracts.js";
2
+ export interface SimilarMessageWindowOptions {
3
+ ttlMs: number;
4
+ occurrenceThreshold: number;
5
+ similarityThreshold: number;
6
+ maximumMembers: number;
7
+ maximumFingerprints: number;
8
+ maximumBytes: number;
9
+ maximumFeaturesPerMessage: number;
10
+ }
11
+ export interface SimilarMessageWindowMetrics {
12
+ members: number;
13
+ fingerprints: number;
14
+ bytes: number;
15
+ evictions: number;
16
+ overflows: number;
17
+ }
18
+ export declare class SimilarMessageWindow {
19
+ #private;
20
+ constructor(secret: Uint8Array, options?: Partial<SimilarMessageWindowOptions>);
21
+ observe(input: {
22
+ guildId: string;
23
+ memberId: string;
24
+ channelId: string;
25
+ content: string;
26
+ observedAtMs: number;
27
+ }): {
28
+ outcome: "recorded";
29
+ count: number;
30
+ } | {
31
+ outcome: "similar_message";
32
+ count: number;
33
+ similarity: number;
34
+ confidenceScore: number;
35
+ } | {
36
+ outcome: "ignored";
37
+ reason: "insufficient_content" | "message_size_limit";
38
+ };
39
+ metrics(): SimilarMessageWindowMetrics;
40
+ clear(): void;
41
+ }
42
+ export declare function scanSimilarMessage(configuration: CompiledEnhancedConfiguration, window: SimilarMessageWindow, input: {
43
+ guildId: string;
44
+ memberId: string;
45
+ channelId: string;
46
+ parentChannelId: string | null;
47
+ memberRoleIds: readonly string[];
48
+ content: string;
49
+ observedAtMs: number;
50
+ source: "user" | "bot" | "webhook" | "system" | "unknown";
51
+ }): EnhancedScanResult;
52
+ /**
53
+ * Runs the stateless detectors and the bounded cross-channel window for the
54
+ * same observation. Contextual state is updated even when a stronger direct
55
+ * match wins, so Hosted Ecko and the embedded scanner retain identical spam
56
+ * semantics.
57
+ */
58
+ export declare function scanEnhancedMessageWithContext(configuration: CompiledEnhancedConfiguration, window: SimilarMessageWindow, input: {
59
+ guildId: string;
60
+ memberId: string;
61
+ channelId: string;
62
+ parentChannelId: string | null;
63
+ memberRoleIds: readonly string[];
64
+ content: string;
65
+ observedAtMs: number;
66
+ source: "user" | "bot" | "webhook" | "system" | "unknown";
67
+ }): EnhancedScanResult;
68
+ //# sourceMappingURL=similar-message-window.d.ts.map
@@ -0,0 +1,259 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { canonicaliseText } from "./canonicalisation.js";
3
+ import { confidenceClass } from "./confidence.js";
4
+ import { scanEnhancedMessage } from "./matcher.js";
5
+ import { ENHANCED_ENGINE_LIMITS, } from "./contracts.js";
6
+ const DEFAULT_OPTIONS = Object.freeze({
7
+ ttlMs: 5 * 60_000,
8
+ occurrenceThreshold: 3,
9
+ similarityThreshold: 0.8,
10
+ maximumMembers: 10_000,
11
+ maximumFingerprints: 50_000,
12
+ maximumBytes: 32 * 1_024 * 1_024,
13
+ maximumFeaturesPerMessage: 64,
14
+ });
15
+ export class SimilarMessageWindow {
16
+ #secret;
17
+ #options;
18
+ #members = new Map();
19
+ #sequence = 0;
20
+ #fingerprints = 0;
21
+ #bytes = 0;
22
+ #evictions = 0;
23
+ #overflows = 0;
24
+ constructor(secret, options = {}) {
25
+ if (secret.byteLength < 32)
26
+ throw new Error("Similar-message fingerprint keys require 32 bytes.");
27
+ this.#secret = new Uint8Array(secret);
28
+ this.#options = validateOptions({ ...DEFAULT_OPTIONS, ...options });
29
+ }
30
+ observe(input) {
31
+ this.#prune(input.observedAtMs);
32
+ if (input.content.length > ENHANCED_ENGINE_LIMITS.messageCharacters ||
33
+ new TextEncoder().encode(input.content).byteLength >
34
+ ENHANCED_ENGINE_LIMITS.messageUtf8Bytes)
35
+ return { outcome: "ignored", reason: "message_size_limit" };
36
+ const canonical = canonicaliseText(input.content).normalisedTokens;
37
+ const rawFeatures = shingles(canonical, this.#options.maximumFeaturesPerMessage);
38
+ if (!rawFeatures.length)
39
+ return { outcome: "ignored", reason: "insufficient_content" };
40
+ const features = rawFeatures.map((feature) => this.#hash(`f:${feature}`));
41
+ const digest = this.#hash(`m:${canonical.join(" ")}`);
42
+ const channelDigest = this.#hash(`c:${input.channelId}`);
43
+ const memberKey = this.#hash(`s:${input.guildId}:${input.memberId}`);
44
+ const window = this.#members.get(memberKey) ?? {
45
+ key: memberKey,
46
+ records: [],
47
+ accessed: 0,
48
+ };
49
+ const comparable = window.records
50
+ .map((record) => ({
51
+ record,
52
+ similarity: record.digest === digest ? 1 : jaccard(record.features, features),
53
+ }))
54
+ .filter(({ similarity }) => similarity >= this.#options.similarityThreshold);
55
+ const highest = comparable.reduce((maximum, item) => Math.max(maximum, item.similarity), 0);
56
+ const record = this.#record(digest, features, channelDigest, input.observedAtMs);
57
+ window.records.push(record);
58
+ window.accessed = record.accessed;
59
+ this.#members.set(memberKey, window);
60
+ this.#fingerprints += 1;
61
+ this.#bytes += record.bytes;
62
+ this.#evictToBounds();
63
+ const count = comparable.length + 1;
64
+ const channelCount = new Set([
65
+ channelDigest,
66
+ ...comparable.map(({ record }) => record.channelDigest),
67
+ ]).size;
68
+ return count >= this.#options.occurrenceThreshold && channelCount >= 2
69
+ ? {
70
+ outcome: "similar_message",
71
+ count,
72
+ similarity: highest,
73
+ confidenceScore: Math.min(95, Math.max(60, Math.round(highest * 100))),
74
+ }
75
+ : { outcome: "recorded", count };
76
+ }
77
+ metrics() {
78
+ return {
79
+ members: this.#members.size,
80
+ fingerprints: this.#fingerprints,
81
+ bytes: this.#bytes,
82
+ evictions: this.#evictions,
83
+ overflows: this.#overflows,
84
+ };
85
+ }
86
+ clear() {
87
+ this.#members.clear();
88
+ this.#fingerprints = 0;
89
+ this.#bytes = 0;
90
+ }
91
+ #hash(value) {
92
+ return createHmac("sha256", this.#secret)
93
+ .update(value)
94
+ .digest("base64url")
95
+ .slice(0, 22);
96
+ }
97
+ #record(digest, features, channelDigest, observedAtMs) {
98
+ return {
99
+ digest,
100
+ features,
101
+ channelDigest,
102
+ observedAtMs,
103
+ accessed: ++this.#sequence,
104
+ bytes: 64 + digest.length + channelDigest.length + features.length * 22,
105
+ };
106
+ }
107
+ #prune(observedAtMs) {
108
+ for (const [memberKey, window] of this.#members) {
109
+ const retained = [];
110
+ for (const record of window.records)
111
+ if (record.observedAtMs + this.#options.ttlMs > observedAtMs)
112
+ retained.push(record);
113
+ else
114
+ this.#removeRecord(record);
115
+ if (retained.length)
116
+ window.records = retained;
117
+ else
118
+ this.#members.delete(memberKey);
119
+ }
120
+ }
121
+ #evictToBounds() {
122
+ while (this.#members.size > this.#options.maximumMembers ||
123
+ this.#fingerprints > this.#options.maximumFingerprints ||
124
+ this.#bytes > this.#options.maximumBytes) {
125
+ this.#overflows += 1;
126
+ const oldest = [...this.#members.values()].sort((left, right) => left.accessed - right.accessed || left.key.localeCompare(right.key))[0];
127
+ if (!oldest)
128
+ return;
129
+ const record = oldest.records.shift();
130
+ if (record)
131
+ this.#removeRecord(record);
132
+ if (!oldest.records.length)
133
+ this.#members.delete(oldest.key);
134
+ else
135
+ oldest.accessed = oldest.records[0].accessed;
136
+ this.#evictions += 1;
137
+ }
138
+ }
139
+ #removeRecord(record) {
140
+ this.#fingerprints -= 1;
141
+ this.#bytes -= record.bytes;
142
+ }
143
+ }
144
+ export function scanSimilarMessage(configuration, window, input) {
145
+ if (input.source !== "user")
146
+ return { outcome: "not_scannable", reason: `source_${input.source}` };
147
+ const detectors = configuration.rules.flatMap((rule) => rule.terms
148
+ .filter((term) => term.kind === "similar_message")
149
+ .map((term) => ({ rule, term })));
150
+ const eligible = detectors.flatMap(({ rule, term }) => ruleExempt(rule, input) ? [] : [{ rule, term }]);
151
+ if (!eligible.length)
152
+ return detectors.length
153
+ ? { outcome: "exempt", reason: "all_rules_exempt" }
154
+ : { outcome: "not_detected", reason: "no_similar_message_rule" };
155
+ const observed = window.observe(input);
156
+ if (observed.outcome !== "similar_message")
157
+ return {
158
+ outcome: observed.outcome === "ignored" ? "not_scannable" : "not_detected",
159
+ reason: observed.outcome === "ignored" ? observed.reason : "window_not_met",
160
+ };
161
+ const detections = eligible
162
+ .map(({ rule, term }) => ({
163
+ rule,
164
+ term,
165
+ threshold: Math.max(configuration.source.categoryThresholds[rule.severity], term.minimumConfidence ?? 60),
166
+ }))
167
+ .filter(({ threshold }) => observed.confidenceScore >= threshold)
168
+ .sort((left, right) => severityRank(right.rule.severity) - severityRank(left.rule.severity) ||
169
+ left.rule.ruleId.localeCompare(right.rule.ruleId) ||
170
+ left.term.termId.localeCompare(right.term.termId));
171
+ const selected = detections[0];
172
+ if (!selected)
173
+ return { outcome: "not_detected", reason: "below_threshold" };
174
+ return {
175
+ outcome: "detected",
176
+ detection: {
177
+ ruleId: selected.rule.ruleId,
178
+ categoryId: selected.rule.categoryId,
179
+ termId: selected.term.termId,
180
+ severity: selected.rule.severity,
181
+ method: "similar_message",
182
+ confidenceScore: observed.confidenceScore,
183
+ confidenceClass: confidenceClass(observed.confidenceScore),
184
+ effectiveThreshold: selected.threshold,
185
+ nativeCoverage: "spam",
186
+ evidence: [],
187
+ },
188
+ };
189
+ }
190
+ /**
191
+ * Runs the stateless detectors and the bounded cross-channel window for the
192
+ * same observation. Contextual state is updated even when a stronger direct
193
+ * match wins, so Hosted Ecko and the embedded scanner retain identical spam
194
+ * semantics.
195
+ */
196
+ export function scanEnhancedMessageWithContext(configuration, window, input) {
197
+ const direct = scanEnhancedMessage(configuration, input);
198
+ const contextual = scanSimilarMessage(configuration, window, input);
199
+ return direct.outcome === "detected" ? direct : contextual;
200
+ }
201
+ function shingles(tokens, maximum) {
202
+ if (!tokens.length)
203
+ return [];
204
+ const values = new Set();
205
+ for (let index = 0; index < tokens.length; index += 1) {
206
+ values.add(tokens[index]);
207
+ if (index + 1 < tokens.length)
208
+ values.add(`${tokens[index]} ${tokens[index + 1]}`);
209
+ if (values.size >= maximum)
210
+ break;
211
+ }
212
+ return [...values].sort().slice(0, maximum);
213
+ }
214
+ function jaccard(left, right) {
215
+ const leftSet = new Set(left);
216
+ const rightSet = new Set(right);
217
+ let intersection = 0;
218
+ for (const value of leftSet)
219
+ if (rightSet.has(value))
220
+ intersection += 1;
221
+ const union = leftSet.size + rightSet.size - intersection;
222
+ return union ? intersection / union : 0;
223
+ }
224
+ function validateOptions(options) {
225
+ if (!Number.isInteger(options.ttlMs) ||
226
+ options.ttlMs < 1 ||
227
+ options.ttlMs > 10 * 60_000 ||
228
+ !Number.isInteger(options.occurrenceThreshold) ||
229
+ options.occurrenceThreshold < 2 ||
230
+ options.similarityThreshold < 0.5 ||
231
+ options.similarityThreshold > 1 ||
232
+ !Number.isInteger(options.maximumMembers) ||
233
+ options.maximumMembers < 1 ||
234
+ !Number.isInteger(options.maximumFingerprints) ||
235
+ options.maximumFingerprints < options.maximumMembers ||
236
+ !Number.isInteger(options.maximumBytes) ||
237
+ options.maximumBytes < 1_024 ||
238
+ !Number.isInteger(options.maximumFeaturesPerMessage) ||
239
+ options.maximumFeaturesPerMessage < 1 ||
240
+ options.maximumFeaturesPerMessage > 128)
241
+ throw new Error("Similar-message window bounds are invalid.");
242
+ return Object.freeze(options);
243
+ }
244
+ function ruleExempt(rule, input) {
245
+ return (rule.exemptChannelIds.includes(input.channelId) ||
246
+ (input.parentChannelId !== null &&
247
+ rule.exemptChannelIds.includes(input.parentChannelId)) ||
248
+ input.memberRoleIds.some((roleId) => rule.exemptRoleIds.includes(roleId)));
249
+ }
250
+ function severityRank(value) {
251
+ return value === "critical"
252
+ ? 4
253
+ : value === "high"
254
+ ? 3
255
+ : value === "medium"
256
+ ? 2
257
+ : 1;
258
+ }
259
+ //# sourceMappingURL=similar-message-window.js.map
@@ -0,0 +1,15 @@
1
+ export interface EnhancedTermImportResult {
2
+ accepted: readonly string[];
3
+ duplicateCount: number;
4
+ rejected: readonly {
5
+ sourceIndex: number;
6
+ value: string;
7
+ code: "term_too_long" | "term_limit" | "empty_canonical_term";
8
+ }[];
9
+ }
10
+ export declare function parseEnhancedTermImport(content: string, options?: {
11
+ maximumBytes?: number;
12
+ maximumTerms?: number;
13
+ maximumTermCharacters?: number;
14
+ }): EnhancedTermImportResult;
15
+ //# sourceMappingURL=term-import.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { canonicalTermKey } from "./canonicalisation.js";
2
+ export function parseEnhancedTermImport(content, options = {}) {
3
+ const maximumBytes = options.maximumBytes ?? 4_000;
4
+ const maximumTerms = options.maximumTerms ?? 1_000;
5
+ const maximumTermCharacters = options.maximumTermCharacters ?? 60;
6
+ if (new TextEncoder().encode(content).byteLength > maximumBytes)
7
+ throw new Error("The import exceeds the configured UTF-8 byte limit.");
8
+ const accepted = [];
9
+ const rejected = [];
10
+ const seen = new Set();
11
+ let duplicateCount = 0;
12
+ const entries = content.split(/\r?\n|,/u);
13
+ for (const [sourceIndex, raw] of entries.entries()) {
14
+ const value = raw.trim();
15
+ if (!value)
16
+ continue;
17
+ const canonical = canonicalTermKey(value.replaceAll("*", ""));
18
+ if (!canonical) {
19
+ rejected.push({ sourceIndex, value, code: "empty_canonical_term" });
20
+ continue;
21
+ }
22
+ if (value.length > maximumTermCharacters) {
23
+ rejected.push({ sourceIndex, value, code: "term_too_long" });
24
+ continue;
25
+ }
26
+ if (seen.has(canonical)) {
27
+ duplicateCount += 1;
28
+ continue;
29
+ }
30
+ seen.add(canonical);
31
+ if (accepted.length >= maximumTerms) {
32
+ rejected.push({ sourceIndex, value, code: "term_limit" });
33
+ continue;
34
+ }
35
+ accepted.push(value);
36
+ }
37
+ return {
38
+ accepted: Object.freeze(accepted),
39
+ duplicateCount,
40
+ rejected: Object.freeze(rejected),
41
+ };
42
+ }
43
+ //# sourceMappingURL=term-import.js.map
@@ -0,0 +1,16 @@
1
+ import type { AutoModerationScannerConfigurationV1 } from "@helyx/sdk";
2
+ import type { AutoModerationConfiguration } from "./configuration.js";
3
+ import type { AutoModerationRuleDraft } from "./domain.js";
4
+ export interface EnhancedConfigurationRuleSource {
5
+ ruleId: string;
6
+ draft: AutoModerationRuleDraft;
7
+ }
8
+ export declare function buildEnhancedScannerConfiguration(input: {
9
+ guildId: string;
10
+ configurationRevision: string;
11
+ configuration: AutoModerationConfiguration;
12
+ rules: readonly EnhancedConfigurationRuleSource[];
13
+ refreshAfter: string;
14
+ expiresAt: string;
15
+ }): AutoModerationScannerConfigurationV1;
16
+ //# sourceMappingURL=enhanced-configuration.d.ts.map
@@ -0,0 +1,91 @@
1
+ import { createHash } from "node:crypto";
2
+ import { ENHANCED_ENGINE_VERSION, ENHANCED_SCORING_VERSION, confidenceThresholds, } from "./engine/index.js";
3
+ export function buildEnhancedScannerConfiguration(input) {
4
+ const rules = input.rules
5
+ .filter(({ draft }) => draft.kind === "keyword" || draft.kind === "spam")
6
+ .map(({ ruleId, draft }) => scannerRule(ruleId, draft, input.configuration))
7
+ .sort((left, right) => left.ruleId.localeCompare(right.ruleId));
8
+ const categoryThresholds = confidenceThresholds({
9
+ profile: input.configuration.enhancedSensitivityProfile,
10
+ custom: input.configuration.enhancedMinimumConfidence,
11
+ overridesEnabled: input.configuration.enhancedCategoryOverridesEnabled,
12
+ overrides: {
13
+ low: input.configuration.enhancedLowMinimumConfidence,
14
+ medium: input.configuration.enhancedMediumMinimumConfidence,
15
+ high: input.configuration.enhancedHighMinimumConfidence,
16
+ critical: input.configuration.enhancedCriticalMinimumConfidence,
17
+ },
18
+ });
19
+ const canonical = {
20
+ guildId: input.guildId,
21
+ configurationRevision: input.configurationRevision,
22
+ engineVersion: ENHANCED_ENGINE_VERSION,
23
+ scoringVersion: ENHANCED_SCORING_VERSION,
24
+ categoryThresholds,
25
+ rules,
26
+ };
27
+ return {
28
+ ...canonical,
29
+ fingerprint: createHash("sha256")
30
+ .update(JSON.stringify(canonical))
31
+ .digest("hex"),
32
+ refreshAfter: input.refreshAfter,
33
+ expiresAt: input.expiresAt,
34
+ };
35
+ }
36
+ function scannerRule(ruleId, draft, configuration) {
37
+ const terms = draft.kind === "spam"
38
+ ? [similarMessageTerm(ruleId)]
39
+ : [...draft.keywordFilter, ...draft.regexPatterns]
40
+ .map((value, index) => {
41
+ const kind = index >= draft.keywordFilter.length
42
+ ? "safe_regex"
43
+ : termKind(value);
44
+ return {
45
+ termId: stableTermId(ruleId, kind, value),
46
+ kind,
47
+ value,
48
+ allowList: [...draft.allowList].sort(),
49
+ normalisedMatching: draft.enhancedNormalisedMatching !== false,
50
+ fuzzyMatching: (kind === "word" || kind === "phrase") &&
51
+ draft.enhancedFuzzyMatching === true,
52
+ };
53
+ })
54
+ .sort((left, right) => left.termId.localeCompare(right.termId));
55
+ return {
56
+ ruleId,
57
+ name: draft.name,
58
+ categoryId: draft.category,
59
+ severity: draft.category,
60
+ terms,
61
+ exemptRoleIds: merged(configuration.defaultExemptRoleIds, draft.ignoredRoleIds),
62
+ exemptChannelIds: merged(configuration.defaultExemptChannelIds, draft.ignoredChannelIds),
63
+ actionPolicy: draft.actionPolicy,
64
+ openViolationThread: draft.openViolationThread,
65
+ };
66
+ }
67
+ function similarMessageTerm(ruleId) {
68
+ return {
69
+ termId: stableTermId(ruleId, "similar_message", "cross-channel-similar-message"),
70
+ kind: "similar_message",
71
+ value: "cross-channel-similar-message",
72
+ allowList: [],
73
+ normalisedMatching: false,
74
+ fuzzyMatching: false,
75
+ };
76
+ }
77
+ function termKind(value) {
78
+ if (value.startsWith("*") || value.endsWith("*"))
79
+ return "wildcard";
80
+ return /\s/u.test(value.trim()) ? "phrase" : "word";
81
+ }
82
+ function stableTermId(ruleId, kind, value) {
83
+ return createHash("sha256")
84
+ .update(JSON.stringify([ruleId, kind, value]))
85
+ .digest("hex")
86
+ .slice(0, 24);
87
+ }
88
+ function merged(left, right) {
89
+ return [...new Set([...left, ...right])].sort();
90
+ }
91
+ //# sourceMappingURL=enhanced-configuration.js.map
@@ -0,0 +1,4 @@
1
+ import { type EventContributions } from "@helyx/sdk";
2
+ import type { AutoModerationPublicationService } from "./publication.js";
3
+ export declare function createAutoModerationEvents(publication: AutoModerationPublicationService): EventContributions;
4
+ //# sourceMappingURL=events.d.ts.map