@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,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import { HELYX_AUTOMOD_ENHANCED_SCANNING_FEATURE } from "@helyx/module-manifest";
3
+ import { AUTOMOD_ENHANCED_CONFIG_PROTOCOL, AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_SCANNER_LIMITS, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
4
+ import { AUTOMOD_MODULE_ID } from "./constants.js";
5
+ import { buildEnhancedScannerConfiguration, } from "./enhanced-configuration.js";
6
+ import { currentConfiguration } from "./publication.js";
7
+ import { AutoModerationRepository, } from "./repository.js";
8
+ const MAXIMUM_SCANNER_GUILDS = 5_000;
9
+ const SNAPSHOT_CACHE_MS = 15_000;
10
+ const CONFIGURATION_REFRESH_MS = 60_000;
11
+ const CONFIGURATION_LIFETIME_MS = 15 * 60_000;
12
+ const SNAPSHOT_READ_CONCURRENCY = 16;
13
+ export function createScannerConfigurationSource() {
14
+ let cached = null;
15
+ let loading = null;
16
+ return {
17
+ async getPage(services, input) {
18
+ if (!Number.isInteger(input.limit) ||
19
+ input.limit < 1 ||
20
+ input.limit > AUTOMOD_SCANNER_LIMITS.configurationPageServers)
21
+ throw new Error("Enhanced configuration page size is invalid.");
22
+ const now = Date.now();
23
+ if (!cached || now - cached.createdAt >= SNAPSHOT_CACHE_MS) {
24
+ loading ??= buildSnapshot(services).finally(() => {
25
+ loading = null;
26
+ });
27
+ cached = await loading;
28
+ }
29
+ return configurationPage(cached, input);
30
+ },
31
+ invalidate() {
32
+ cached = null;
33
+ },
34
+ };
35
+ }
36
+ async function buildSnapshot(services) {
37
+ const repository = new AutoModerationRepository(services);
38
+ const guildIds = await repository.listPublishedGuildIds(MAXIMUM_SCANNER_GUILDS);
39
+ const items = [];
40
+ for (let index = 0; index < guildIds.length; index += SNAPSHOT_READ_CONCURRENCY) {
41
+ const now = Date.now();
42
+ const configurations = await Promise.all(guildIds
43
+ .slice(index, index + SNAPSHOT_READ_CONCURRENCY)
44
+ .map((guildId) => eligibleConfiguration(services, repository, guildId, now).catch(() => null)));
45
+ for (const configuration of configurations)
46
+ if (configuration)
47
+ items.push(configuration);
48
+ }
49
+ items.sort((left, right) => left.guildId.localeCompare(right.guildId, "en"));
50
+ const fingerprint = createHash("sha256")
51
+ .update(JSON.stringify(items.map((item) => [
52
+ item.guildId,
53
+ item.configurationRevision,
54
+ item.fingerprint,
55
+ ])))
56
+ .digest("hex");
57
+ return {
58
+ createdAt: Date.now(),
59
+ fingerprint,
60
+ items: Object.freeze(items),
61
+ };
62
+ }
63
+ async function eligibleConfiguration(services, repository, guildId, now) {
64
+ if (!(await services
65
+ .get(HELYX_SERVICE_NAMES.installations)
66
+ .isModuleEnabled(guildId, AUTOMOD_MODULE_ID)))
67
+ return null;
68
+ const access = await services
69
+ .get(HELYX_SERVICE_NAMES.featureAccess)
70
+ .check({
71
+ guildId,
72
+ moduleId: AUTOMOD_MODULE_ID,
73
+ featureId: HELYX_AUTOMOD_ENHANCED_SCANNING_FEATURE,
74
+ });
75
+ if (access.decision !== "granted")
76
+ return null;
77
+ const configuration = await currentConfiguration(services, guildId);
78
+ if (configuration.value.configuredMode !== "enhanced")
79
+ return null;
80
+ const rules = (await repository.listAllPublishedRules(guildId)).filter((rule) => rule.configurationRevision === configuration.revision);
81
+ if (!rules.length)
82
+ return null;
83
+ const item = buildEnhancedScannerConfiguration({
84
+ guildId,
85
+ configurationRevision: configuration.revision,
86
+ configuration: configuration.value,
87
+ rules: rules.map(ruleSource),
88
+ refreshAfter: new Date(now + CONFIGURATION_REFRESH_MS).toISOString(),
89
+ expiresAt: new Date(now + CONFIGURATION_LIFETIME_MS).toISOString(),
90
+ });
91
+ return encodedBytes(item) < AUTOMOD_SCANNER_LIMITS.configurationPageBytes
92
+ ? item
93
+ : null;
94
+ }
95
+ function configurationPage(snapshot, input) {
96
+ const index = input.cursor
97
+ ? decodeCursor(input.cursor, snapshot.fingerprint)
98
+ : 0;
99
+ const items = [];
100
+ for (let next = index; next < snapshot.items.length && items.length < input.limit; next += 1) {
101
+ const item = snapshot.items[next];
102
+ if (!item)
103
+ break;
104
+ const candidate = [...items, item];
105
+ const candidateNext = index + candidate.length;
106
+ const page = pageValue(snapshot, candidate, candidateNext);
107
+ if (encodedBytes(page) > AUTOMOD_SCANNER_LIMITS.configurationPageBytes) {
108
+ if (!items.length)
109
+ throw new Error("Enhanced configuration cannot fit in one page.");
110
+ break;
111
+ }
112
+ items.push(item);
113
+ }
114
+ return pageValue(snapshot, items, index + items.length);
115
+ }
116
+ function pageValue(snapshot, items, nextIndex) {
117
+ return {
118
+ protocol: AUTOMOD_ENHANCED_CONFIG_PROTOCOL,
119
+ providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
120
+ contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
121
+ snapshotFingerprint: snapshot.fingerprint,
122
+ items,
123
+ ...(nextIndex < snapshot.items.length
124
+ ? { nextCursor: encodeCursor(snapshot.fingerprint, nextIndex) }
125
+ : {}),
126
+ };
127
+ }
128
+ function encodeCursor(fingerprint, index) {
129
+ return Buffer.from(JSON.stringify({ fingerprint, index }), "utf8").toString("base64url");
130
+ }
131
+ function decodeCursor(cursor, fingerprint) {
132
+ const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
133
+ if (decoded.fingerprint !== fingerprint ||
134
+ !Number.isSafeInteger(decoded.index) ||
135
+ decoded.index < 0 ||
136
+ decoded.index > MAXIMUM_SCANNER_GUILDS)
137
+ throw new Error("Enhanced configuration cursor is stale or invalid.");
138
+ return decoded.index;
139
+ }
140
+ function encodedBytes(value) {
141
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
142
+ }
143
+ function ruleSource(rule) {
144
+ return { ruleId: rule.ruleId, draft: rule.draft };
145
+ }
146
+ //# sourceMappingURL=scanner-configuration.js.map
@@ -0,0 +1,5 @@
1
+ import { type AutoModerationScannerProviderContributionV1 } from "@helyx/sdk";
2
+ export declare function createAutoModerationScannerProvider(): AutoModerationScannerProviderContributionV1 & {
3
+ invalidate(): void;
4
+ };
5
+ //# sourceMappingURL=scanner-provider.d.ts.map
@@ -0,0 +1,310 @@
1
+ import { AUTOMOD_ENHANCED_CONFIG_PROTOCOL, AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_EVIDENCE_PROTOCOL, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_ENHANCED_RESULT_PROTOCOL, AUTOMOD_SCANNER_LIMITS, HELYX_SERVICE_NAMES, MODERATION_ACTION_CONTRACT_VERSION, MODERATION_ACTION_PROVIDER_ID, } from "@helyx/sdk";
2
+ import { AUTOMOD_MODULE_ID } from "./constants.js";
3
+ import { countAutoModerationReceipt } from "./receipt-statistics.js";
4
+ import { enforcementActionFamily, enforcementClaimFingerprint, enforcementClaimKey, } from "./domain.js";
5
+ import { buildEnhancedScannerConfiguration, } from "./enhanced-configuration.js";
6
+ import { ENHANCED_ENGINE_VERSION, ENHANCED_SCORING_VERSION, confidenceClass, enhancedOperationId, } from "./engine/index.js";
7
+ import { currentConfiguration } from "./publication.js";
8
+ import { AutoModerationRepository, } from "./repository.js";
9
+ import { createScannerConfigurationSource } from "./scanner-configuration.js";
10
+ import { validateEnhancedResult } from "./scanner-result-validation.js";
11
+ export function createAutoModerationScannerProvider() {
12
+ const configurations = createScannerConfigurationSource();
13
+ return {
14
+ providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
15
+ contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
16
+ configurationProtocol: AUTOMOD_ENHANCED_CONFIG_PROTOCOL,
17
+ resultProtocol: AUTOMOD_ENHANCED_RESULT_PROTOCOL,
18
+ evidenceProtocol: AUTOMOD_ENHANCED_EVIDENCE_PROTOCOL,
19
+ getConfigurationPage: (context, input) => configurations.getPage(context.services, input),
20
+ validateResult: validateEnhancedResult,
21
+ handleResult: (context, result) => handleEnhancedResult(context.services, context.evidenceReference, result),
22
+ invalidate() {
23
+ configurations.invalidate();
24
+ },
25
+ };
26
+ }
27
+ async function handleEnhancedResult(services, evidenceReference, result) {
28
+ const now = Date.now();
29
+ const issuedAt = Date.parse(result.issuedAt);
30
+ const deadlineAt = Date.parse(result.deadlineAt);
31
+ const observedAt = Date.parse(result.observation.observedAt);
32
+ if (!Number.isFinite(issuedAt) ||
33
+ !Number.isFinite(deadlineAt) ||
34
+ !Number.isFinite(observedAt) ||
35
+ issuedAt > now + AUTOMOD_SCANNER_LIMITS.clockSkewSeconds * 1_000 ||
36
+ deadlineAt <= now ||
37
+ deadlineAt <= issuedAt ||
38
+ deadlineAt - issuedAt >
39
+ AUTOMOD_SCANNER_LIMITS.actionDeadlineSeconds * 1_000 ||
40
+ observedAt > issuedAt + AUTOMOD_SCANNER_LIMITS.clockSkewSeconds * 1_000 ||
41
+ issuedAt - observedAt > AUTOMOD_SCANNER_LIMITS.actionDeadlineSeconds * 1_000)
42
+ return rejected("stale");
43
+ const repository = new AutoModerationRepository(services);
44
+ const rule = await repository.findRule(result.guildId, result.ruleId);
45
+ const configuration = await currentConfiguration(services, result.guildId);
46
+ if (!rule ||
47
+ rule.status !== "published" ||
48
+ configuration.value.configuredMode !== "enhanced" ||
49
+ result.configurationRevision !== configuration.revision ||
50
+ rule.configurationRevision !== configuration.revision)
51
+ return rejected("stale_revision");
52
+ const scannerConfiguration = buildEnhancedScannerConfiguration({
53
+ guildId: result.guildId,
54
+ configurationRevision: configuration.revision,
55
+ configuration: configuration.value,
56
+ rules: [ruleSource(rule)],
57
+ refreshAfter: result.issuedAt,
58
+ expiresAt: result.deadlineAt,
59
+ });
60
+ const scannerRule = scannerConfiguration.rules[0];
61
+ const term = scannerRule?.terms.find(({ termId }) => termId === result.termId);
62
+ if (!scannerRule ||
63
+ !term ||
64
+ result.detectorVersion !== ENHANCED_ENGINE_VERSION ||
65
+ result.scoringVersion !== ENHANCED_SCORING_VERSION ||
66
+ result.categoryId !== scannerRule.categoryId ||
67
+ result.severity !== scannerRule.severity ||
68
+ result.effectiveThreshold !==
69
+ scannerConfiguration.categoryThresholds[result.severity] ||
70
+ result.confidenceScore < result.effectiveThreshold ||
71
+ result.confidenceClass !== confidenceClass(result.confidenceScore) ||
72
+ !methodAllowed(term, result.method) ||
73
+ scannerRule.exemptChannelIds.includes(result.channelId) ||
74
+ (result.parentChannelId !== null &&
75
+ scannerRule.exemptChannelIds.includes(result.parentChannelId)))
76
+ return rejected("source_unresolved");
77
+ const expectedOperationId = enhancedOperationId({
78
+ environment: result.environment,
79
+ observation: {
80
+ guildId: result.guildId,
81
+ channelId: result.channelId,
82
+ messageId: result.messageId,
83
+ type: result.observation.type,
84
+ version: result.observation.version,
85
+ },
86
+ configurationRevision: result.configurationRevision,
87
+ detectorVersion: result.detectorVersion,
88
+ ruleId: result.ruleId,
89
+ termId: result.termId,
90
+ resultClass: result.confidenceClass,
91
+ });
92
+ if (expectedOperationId !== result.operationId)
93
+ return rejected("invalid_envelope");
94
+ const actionFamily = enforcementActionFamily(rule.draft.actionPolicy);
95
+ const claimKey = enforcementClaimKey({
96
+ guildId: result.guildId,
97
+ configurationRevision: rule.configurationRevision,
98
+ subjectUserId: result.authorUserId,
99
+ messageId: result.messageId,
100
+ channelId: result.channelId,
101
+ sourceEventId: result.operationId,
102
+ });
103
+ const claim = await repository.claimEnforcement({
104
+ guildId: result.guildId,
105
+ claimKey,
106
+ rule,
107
+ subjectUserId: result.authorUserId,
108
+ channelId: result.channelId,
109
+ messageId: result.messageId,
110
+ actionFamily,
111
+ source: "enhanced",
112
+ requestFingerprint: enforcementClaimFingerprint({
113
+ claimKey,
114
+ logicalRuleId: rule.ruleId,
115
+ ruleRevision: rule.revision,
116
+ actionFamily,
117
+ }),
118
+ occurredAt: new Date(observedAt),
119
+ });
120
+ if (claim.conflict)
121
+ return rejected("invalid_envelope");
122
+ const receipt = await repository.appendEnhancedReceipt({
123
+ result,
124
+ rule,
125
+ claimKey,
126
+ occurredAt: new Date(observedAt),
127
+ });
128
+ const resumingReceipt = !receipt.created;
129
+ if (resumingReceipt) {
130
+ const existingReceipt = await repository.findReceiptByEventKey({
131
+ guildId: result.guildId,
132
+ eventKey: result.operationId,
133
+ });
134
+ if (!existingReceipt || existingReceipt.outcome !== "observed")
135
+ return { outcome: "duplicate", operationId: result.operationId };
136
+ }
137
+ if (!claim.created &&
138
+ (!resumingReceipt ||
139
+ claim.source !== "enhanced" ||
140
+ claim.outcome !== "pending")) {
141
+ await repository.settleReceipt({
142
+ guildId: result.guildId,
143
+ eventKey: result.operationId,
144
+ outcome: claim.outcome === "pending" ? "review_required" : claim.outcome,
145
+ ...(claim.moderationCaseId
146
+ ? { moderationCaseId: claim.moderationCaseId }
147
+ : {}),
148
+ safeCode: claim.safeCode ?? "first_source_won",
149
+ });
150
+ return { outcome: "duplicate", operationId: result.operationId };
151
+ }
152
+ await countEnhancedReceipt(services, result, rule.ruleId);
153
+ await emitEnhancedTrigger(services, result, rule.ruleId);
154
+ if (!services.has(HELYX_SERVICE_NAMES.moderationActions)) {
155
+ await settleUnavailable(repository, result, claimKey);
156
+ return { outcome: "accepted", operationId: result.operationId };
157
+ }
158
+ const request = {
159
+ operationKey: `automod:${claimKey}`,
160
+ detectionSource: "enhanced",
161
+ guildId: result.guildId,
162
+ subjectUserId: result.authorUserId,
163
+ sourceMessage: {
164
+ channelId: result.channelId,
165
+ messageId: result.messageId,
166
+ },
167
+ sourceEventId: result.operationId,
168
+ occurredAt: new Date(observedAt),
169
+ callerModuleId: AUTOMOD_MODULE_ID,
170
+ ruleId: rule.ruleId,
171
+ configurationRevision: rule.configurationRevision,
172
+ reasonCode: "enhanced_rule_triggered",
173
+ severity: rule.category,
174
+ confidenceClass: result.confidenceClass,
175
+ confidenceScore: result.confidenceScore,
176
+ requestKind: "detection",
177
+ ...(evidenceReference ? { evidenceReference } : {}),
178
+ };
179
+ let actionResult;
180
+ try {
181
+ actionResult = await services
182
+ .get(HELYX_SERVICE_NAMES.moderationActions)
183
+ .execute({
184
+ providerId: MODERATION_ACTION_PROVIDER_ID,
185
+ contractVersion: MODERATION_ACTION_CONTRACT_VERSION,
186
+ callerModuleId: AUTOMOD_MODULE_ID,
187
+ request,
188
+ });
189
+ }
190
+ catch {
191
+ await settleDispatchUnconfirmed(repository, result, claimKey);
192
+ return { outcome: "accepted", operationId: result.operationId };
193
+ }
194
+ await settleActionResult(repository, result, claimKey, actionResult);
195
+ return { outcome: "accepted", operationId: result.operationId };
196
+ }
197
+ async function settleDispatchUnconfirmed(repository, result, claimKey) {
198
+ const safeCode = "moderation_dispatch_unconfirmed";
199
+ await repository.settleReceipt({
200
+ guildId: result.guildId,
201
+ eventKey: result.operationId,
202
+ outcome: "review_required",
203
+ safeCode,
204
+ });
205
+ await repository.settleClaim({
206
+ guildId: result.guildId,
207
+ claimKey,
208
+ outcome: "review_required",
209
+ safeCode,
210
+ occurredAt: new Date(),
211
+ });
212
+ }
213
+ function methodAllowed(term, method) {
214
+ if (method === "normalised")
215
+ return term.normalisedMatching;
216
+ if (method === "fuzzy")
217
+ return (term.fuzzyMatching && (term.kind === "word" || term.kind === "phrase"));
218
+ if (method === "wildcard")
219
+ return term.kind === "wildcard";
220
+ if (method === "safe_regex")
221
+ return term.kind === "safe_regex";
222
+ if (method === "similar_message")
223
+ return term.kind === "similar_message";
224
+ if (method === "phrase")
225
+ return term.kind === "phrase";
226
+ if (method === "exact")
227
+ return term.kind === "word";
228
+ return false;
229
+ }
230
+ async function settleActionResult(repository, result, claimKey, actionResult) {
231
+ const caseId = "caseId" in actionResult ? actionResult.caseId : undefined;
232
+ const safeCode = actionResult.outcome === "rejected"
233
+ ? actionResult.code
234
+ : "safeCode" in actionResult
235
+ ? actionResult.safeCode
236
+ : undefined;
237
+ await repository.settleReceipt({
238
+ guildId: result.guildId,
239
+ eventKey: result.operationId,
240
+ outcome: actionResult.outcome,
241
+ ...(caseId ? { moderationCaseId: caseId } : {}),
242
+ ...(safeCode ? { safeCode } : {}),
243
+ });
244
+ await repository.settleClaim({
245
+ guildId: result.guildId,
246
+ claimKey,
247
+ outcome: actionResult.outcome,
248
+ ...(caseId ? { moderationCaseId: caseId } : {}),
249
+ ...(actionResult.outcome === "applied" ||
250
+ actionResult.outcome === "already_applied"
251
+ ? {
252
+ repeatOrdinal: actionResult.offenceOrdinal,
253
+ selectedAction: actionResult.selectedAction.type,
254
+ }
255
+ : {}),
256
+ ...(safeCode ? { safeCode } : {}),
257
+ occurredAt: new Date(),
258
+ });
259
+ }
260
+ async function settleUnavailable(repository, result, claimKey) {
261
+ await repository.settleReceipt({
262
+ guildId: result.guildId,
263
+ eventKey: result.operationId,
264
+ outcome: "native_only",
265
+ safeCode: "moderation_unavailable",
266
+ });
267
+ await repository.settleClaim({
268
+ guildId: result.guildId,
269
+ claimKey,
270
+ outcome: "native_only",
271
+ safeCode: "moderation_unavailable",
272
+ occurredAt: new Date(),
273
+ });
274
+ }
275
+ async function countEnhancedReceipt(services, result, ruleId) {
276
+ await countAutoModerationReceipt(services, {
277
+ guildId: result.guildId,
278
+ ruleId,
279
+ eventKey: result.operationId,
280
+ source: "enhanced",
281
+ actionType: "detection",
282
+ occurredAt: new Date(result.observation.observedAt),
283
+ }).catch(() => undefined);
284
+ }
285
+ async function emitEnhancedTrigger(services, result, ruleId) {
286
+ if (!services.has(HELYX_SERVICE_NAMES.moduleLogging))
287
+ return;
288
+ await services
289
+ .get(HELYX_SERVICE_NAMES.moduleLogging)
290
+ .emit({
291
+ guildId: result.guildId,
292
+ moduleId: AUTOMOD_MODULE_ID,
293
+ eventId: "automod-rule-triggered",
294
+ summary: "An Enhanced Auto Moderation rule triggered.",
295
+ details: [
296
+ { label: "Rule", value: ruleId },
297
+ { label: "Method", value: result.method },
298
+ { label: "Outcome", value: "Validated by Helyx" },
299
+ ],
300
+ idempotencyKey: result.operationId,
301
+ })
302
+ .catch(() => undefined);
303
+ }
304
+ function ruleSource(rule) {
305
+ return { ruleId: rule.ruleId, draft: rule.draft };
306
+ }
307
+ function rejected(code) {
308
+ return { outcome: "rejected", code };
309
+ }
310
+ //# sourceMappingURL=scanner-provider.js.map
@@ -0,0 +1,3 @@
1
+ import { type AutoModerationEnhancedResultV1 } from "@helyx/sdk";
2
+ export declare function validateEnhancedResult(input: unknown): AutoModerationEnhancedResultV1;
3
+ //# sourceMappingURL=scanner-result-validation.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { z } from "zod";
2
+ import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_ENHANCED_RESULT_PROTOCOL, AUTOMOD_SCANNER_LIMITS, } from "@helyx/sdk";
3
+ const snowflake = z.string().regex(/^\d{17,20}$/u);
4
+ const safeIdentifier = z.string().min(1).max(200);
5
+ const resultSchema = z
6
+ .object({
7
+ protocol: z.literal(AUTOMOD_ENHANCED_RESULT_PROTOCOL),
8
+ providerId: z.literal(AUTOMOD_ENHANCED_PROVIDER_ID),
9
+ contractVersion: z.literal(AUTOMOD_ENHANCED_CONTRACT_VERSION),
10
+ detectorVersion: safeIdentifier,
11
+ scoringVersion: safeIdentifier,
12
+ operationId: z.string().regex(/^[a-f0-9]{64}$/u),
13
+ nonce: z.string().regex(/^[A-Za-z0-9_-]{16,160}$/u),
14
+ issuedAt: z.iso.datetime({ offset: true }),
15
+ deadlineAt: z.iso.datetime({ offset: true }),
16
+ environment: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u),
17
+ guildId: snowflake,
18
+ channelId: snowflake,
19
+ parentChannelId: snowflake.nullable(),
20
+ messageId: snowflake,
21
+ authorUserId: snowflake,
22
+ observation: z
23
+ .object({
24
+ type: z.enum(["create", "edit"]),
25
+ version: safeIdentifier,
26
+ observedAt: z.iso.datetime({ offset: true }),
27
+ })
28
+ .strict(),
29
+ ruleId: safeIdentifier,
30
+ categoryId: safeIdentifier,
31
+ termId: safeIdentifier,
32
+ configurationRevision: safeIdentifier,
33
+ severity: z.enum(["low", "medium", "high", "critical"]),
34
+ method: z.enum([
35
+ "exact",
36
+ "phrase",
37
+ "wildcard",
38
+ "safe_regex",
39
+ "normalised",
40
+ "fuzzy",
41
+ "similar_message",
42
+ ]),
43
+ confidenceScore: z.number().int().min(0).max(100),
44
+ confidenceClass: z.enum([
45
+ "exact",
46
+ "native_equivalent",
47
+ "high",
48
+ "medium",
49
+ "low",
50
+ ]),
51
+ effectiveThreshold: z.number().int().min(60).max(100),
52
+ correlationId: z.uuid(),
53
+ })
54
+ .strict();
55
+ export function validateEnhancedResult(input) {
56
+ if (Buffer.byteLength(JSON.stringify(input), "utf8") >
57
+ AUTOMOD_SCANNER_LIMITS.resultBytes)
58
+ throw new Error("Enhanced result exceeds its byte limit.");
59
+ return resultSchema.parse(input);
60
+ }
61
+ //# sourceMappingURL=scanner-result-validation.js.map
@@ -0,0 +1,6 @@
1
+ import type { ManagedResourceValue, ModuleSettingsPreviewContext } from "@helyx/sdk";
2
+ export declare function previewEnhancedSettings(context: ModuleSettingsPreviewContext, input: {
3
+ value: Record<string, unknown>;
4
+ input: Readonly<Record<string, ManagedResourceValue>>;
5
+ }): Promise<Readonly<Record<string, ManagedResourceValue>>>;
6
+ //# sourceMappingURL=settings-preview.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { buildEnhancedScannerConfiguration, } from "./enhanced-configuration.js";
2
+ import { EnhancedConfigurationError, compileEnhancedConfiguration, scanEnhancedMessage, } from "./engine/index.js";
3
+ import { parseAutoModerationConfiguration } from "./configuration.js";
4
+ import { AutoModerationRepository } from "./repository.js";
5
+ export async function previewEnhancedSettings(context, input) {
6
+ const content = input.input.content;
7
+ if (typeof content !== "string" || !content.trim())
8
+ throw new Error("Enhanced preview requires fictional message content.");
9
+ const configuration = parseAutoModerationConfiguration(input.value);
10
+ const rules = await publishedRules(context);
11
+ const scannerConfiguration = buildEnhancedScannerConfiguration({
12
+ guildId: context.guildId,
13
+ configurationRevision: "preview",
14
+ configuration,
15
+ rules,
16
+ refreshAfter: "1970-01-01T00:00:00.000Z",
17
+ expiresAt: "9999-12-31T23:59:59.999Z",
18
+ });
19
+ let compiled;
20
+ try {
21
+ compiled = compileEnhancedConfiguration(scannerConfiguration);
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof EnhancedConfigurationError))
25
+ throw error;
26
+ return {
27
+ outcome: "configuration_invalid",
28
+ issues: error.issues.map(({ code, path, message }) => ({
29
+ code,
30
+ path,
31
+ message,
32
+ })),
33
+ };
34
+ }
35
+ const result = scanEnhancedMessage(compiled, {
36
+ content,
37
+ channelId: "preview",
38
+ parentChannelId: null,
39
+ memberRoleIds: [],
40
+ source: "user",
41
+ });
42
+ if (result.outcome !== "detected")
43
+ return {
44
+ outcome: result.outcome,
45
+ reason: result.reason,
46
+ evaluatedRules: rules.length,
47
+ note: regexNote(compiled.rules),
48
+ };
49
+ return {
50
+ outcome: result.outcome,
51
+ ruleId: result.detection.ruleId,
52
+ category: result.detection.severity,
53
+ method: result.detection.method,
54
+ confidenceScore: result.detection.confidenceScore,
55
+ confidenceClass: result.detection.confidenceClass,
56
+ effectiveThreshold: result.detection.effectiveThreshold,
57
+ nativeCoverage: result.detection.nativeCoverage,
58
+ evidence: result.detection.evidence,
59
+ evaluatedRules: rules.length,
60
+ note: regexNote(compiled.rules),
61
+ };
62
+ }
63
+ async function publishedRules(context) {
64
+ const repository = new AutoModerationRepository(context.services);
65
+ const result = [];
66
+ let cursor = null;
67
+ do {
68
+ const page = await repository.listRules({
69
+ guildId: context.guildId,
70
+ limit: 50,
71
+ status: "published",
72
+ ...(cursor ? { cursor } : {}),
73
+ });
74
+ result.push(...page.items.map((rule) => ({ ruleId: rule.ruleId, draft: rule.draft })));
75
+ cursor = page.nextCursor;
76
+ } while (cursor);
77
+ return result;
78
+ }
79
+ function regexNote(rules) {
80
+ return rules.some((rule) => rule.terms.some((term) => !term.enhancedAvailable))
81
+ ? "Custom regex is covered by Discord native Auto Moderation only."
82
+ : "All evaluated terms have Enhanced matching support.";
83
+ }
84
+ //# sourceMappingURL=settings-preview.js.map