@helyx/bot 1.2.1 → 1.3.0

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.
@@ -105,6 +105,7 @@ export class BotModerationActionRegistry {
105
105
  const evidenceClaims = {
106
106
  caseIds: new Set(),
107
107
  threadIds: new Set(),
108
+ openingMessageIds: new Set(),
108
109
  };
109
110
  const requestServices = createRequestBoundServices(this.services, evidenceClaims);
110
111
  return await this.#actionProvider.execute({
@@ -117,15 +118,30 @@ export class BotModerationActionRegistry {
117
118
  !OPERATION_KEY_PATTERN.test(delivery.caseId) ||
118
119
  !OPERATION_KEY_PATTERN.test(delivery.operationKey) ||
119
120
  !evidenceClaims.caseIds.has(delivery.caseId) ||
120
- !evidenceClaims.threadIds.has(delivery.threadId))
121
+ !evidenceClaims.threadIds.has(delivery.threadId) ||
122
+ (delivery.openingMessageId !== undefined &&
123
+ (!ID_PATTERN.test(delivery.openingMessageId) ||
124
+ !evidenceClaims.openingMessageIds.has(`${delivery.threadId}:${delivery.openingMessageId}`))))
121
125
  return { outcome: "evidence_unavailable" };
122
126
  if (!this.evidenceDelivery)
123
127
  return { outcome: "evidence_unavailable" };
124
128
  return await this.evidenceDelivery.deliverEvidence({
125
129
  guildId: delivery.guildId,
126
130
  threadId: delivery.threadId,
131
+ ...(delivery.openingMessageId
132
+ ? { openingMessageId: delivery.openingMessageId }
133
+ : {}),
127
134
  evidenceReference: input.request.evidenceReference,
128
135
  operationKey: delivery.operationKey,
136
+ context: {
137
+ severity: input.request.severity,
138
+ occurredAt: input.request.occurredAt,
139
+ ...(policy.ruleName ? { ruleName: policy.ruleName } : {}),
140
+ confidenceClass: input.request.confidenceClass,
141
+ ...(input.request.confidenceScore !== undefined
142
+ ? { confidenceScore: input.request.confidenceScore }
143
+ : {}),
144
+ },
129
145
  });
130
146
  },
131
147
  },
@@ -168,6 +184,11 @@ function isValidRequest(request) {
168
184
  SAFE_CODE_PATTERN.test(request.reasonCode) &&
169
185
  ["low", "medium", "high", "critical"].includes(request.severity) &&
170
186
  ["exact", "native_equivalent", "high", "medium", "low"].includes(request.confidenceClass) &&
187
+ (request.confidenceScore === undefined ||
188
+ (request.detectionSource === "enhanced" &&
189
+ Number.isInteger(request.confidenceScore) &&
190
+ request.confidenceScore >= 0 &&
191
+ request.confidenceScore <= 100)) &&
171
192
  (request.sourceMessage === null ||
172
193
  (ID_PATTERN.test(request.sourceMessage.channelId) &&
173
194
  (request.sourceMessage.messageId === null ||
@@ -234,9 +255,20 @@ function createRequestBoundServices(services, claims) {
234
255
  },
235
256
  addMember: (input) => service.addMember(input),
236
257
  removeMember: (input) => service.removeMember(input),
237
- send: (input) => service.send(input),
258
+ send: async (input) => {
259
+ const result = await service.send(input);
260
+ if (claims.threadIds.has(input.threadId) &&
261
+ (result.outcome === "sent" || result.outcome === "already_sent"))
262
+ claims.openingMessageIds.add(`${input.threadId}:${result.messageId}`);
263
+ return result;
264
+ },
238
265
  close: (input) => service.close(input),
239
266
  reconcile: (input) => service.reconcile(input),
267
+ ...(service.editControls
268
+ ? {
269
+ editControls: (input) => service.editControls(input),
270
+ }
271
+ : {}),
240
272
  };
241
273
  return bound;
242
274
  }
@@ -254,6 +286,12 @@ function isValidPolicy(policy) {
254
286
  policy.windowStartsAt instanceof Date &&
255
287
  !Number.isNaN(policy.windowStartsAt.getTime()) &&
256
288
  typeof policy.openViolationThread === "boolean" &&
289
+ (policy.deleteSourceMessage === undefined ||
290
+ typeof policy.deleteSourceMessage === "boolean") &&
291
+ (policy.ruleName === undefined ||
292
+ (typeof policy.ruleName === "string" &&
293
+ policy.ruleName.trim().length > 0 &&
294
+ policy.ruleName.length <= 100)) &&
257
295
  validRoleIds(policy.globalIgnoredRoleIds, MODERATION_CONTRACT_LIMITS.ignoredRolesPerScope) &&
258
296
  validRoleIds(policy.ruleIgnoredRoleIds, MODERATION_CONTRACT_LIMITS.ignoredRolesPerScope) &&
259
297
  Boolean(policy.actionPolicy) &&
@@ -325,6 +363,7 @@ function samePolicy(expected, current) {
325
363
  expected.configurationRevision === current.configurationRevision &&
326
364
  expected.windowStartsAt.getTime() === current.windowStartsAt.getTime() &&
327
365
  expected.openViolationThread === current.openViolationThread &&
366
+ expected.deleteSourceMessage === current.deleteSourceMessage &&
328
367
  sameStrings(expected.globalIgnoredRoleIds, current.globalIgnoredRoleIds) &&
329
368
  sameStrings(expected.ruleIgnoredRoleIds, current.ruleIgnoredRoleIds) &&
330
369
  sameAction(expected.actionPolicy.first, current.actionPolicy.first) &&
@@ -1,4 +1,4 @@
1
- import type { ModuleDefinition, PrivacyProvider, PrivacyProviderExecutor, ServiceAccess } from "@helyx/sdk";
1
+ import { type ModuleDefinition, type PrivacyProvider, type PrivacyProviderExecutor, type ServiceAccess } from "@helyx/sdk";
2
2
  export declare function createPrivacyProviderRegistry(modules: readonly ModuleDefinition[], executor: PrivacyProviderExecutor | undefined, services: ServiceAccess): ReadonlyMap<string, PrivacyProvider>;
3
3
  export declare function describePrivacyProviders(providers: ReadonlyMap<string, PrivacyProvider>): readonly {
4
4
  id: string;
@@ -1,12 +1,41 @@
1
+ import { createPrivacyProvider, } from "@helyx/sdk";
1
2
  export function createPrivacyProviderRegistry(modules, executor, services) {
2
3
  if (!executor)
3
4
  return new Map();
4
- return new Map(modules.flatMap((module) => {
5
- if (!module.privacy)
6
- return [];
7
- const provider = module.privacy.create(executor, services);
8
- return [[provider.id, provider]];
9
- }));
5
+ const configurationDeclaration = {
6
+ containsPersonalData: true,
7
+ providerVersion: 1,
8
+ collections: [
9
+ {
10
+ name: "moderation_notification_memberships",
11
+ createdAtField: "updated_at",
12
+ subjectFields: [
13
+ {
14
+ field: "notification_user_id",
15
+ role: "staff-notification-recipient",
16
+ erasure: "set-null",
17
+ },
18
+ ],
19
+ },
20
+ ],
21
+ };
22
+ const configurationProvider = createPrivacyProvider({
23
+ id: "helyx.configuration",
24
+ declaration: configurationDeclaration,
25
+ executor,
26
+ });
27
+ if (modules.some(({ manifest }) => manifest.id === configurationProvider.id)) {
28
+ throw new Error("Core configuration privacy identity cannot be a module identity");
29
+ }
30
+ return new Map([
31
+ [configurationProvider.id, configurationProvider],
32
+ ...modules.flatMap((module) => {
33
+ if (!module.privacy)
34
+ return [];
35
+ const provider = module.privacy.create(executor, services);
36
+ return [[provider.id, provider]];
37
+ }),
38
+ ]);
10
39
  }
11
40
  export function describePrivacyProviders(providers) {
12
41
  return [...providers.values()]
@@ -8,7 +8,7 @@ import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, sh
8
8
  // deadline turned healthy leases into deterministic 503s. Keep the check
9
9
  // bounded while allowing normal network variance.
10
10
  const LEASE_VALIDATION_TIMEOUT_MS = 3_000;
11
- const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.actionDeadlineSeconds * 1_000;
11
+ const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.resultAcknowledgementSeconds * 1_000;
12
12
  export class ShardControlRouter {
13
13
  input;
14
14
  constructor(input) {
@@ -30,7 +30,7 @@ export class ShardControlRouter {
30
30
  }
31
31
  /** Routes an authenticated Ecko result to the current guild owner. */
32
32
  acceptHostedEckoResult(input) {
33
- return this.#route({ kind: "automod.enhanced.result.accept", ...input }, remainingEnhancedDeadline(input.result));
33
+ return this.#route({ kind: "automod.enhanced.result.accept", ...input }, ECKO_RESULT_TIMEOUT_MS);
34
34
  }
35
35
  /** Reads the fenced leader's current Enhanced configuration snapshot. */
36
36
  getHostedEckoConfigurationPage(input) {
@@ -336,17 +336,6 @@ async function executeLeaderOperation(input, operation) {
336
336
  throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
337
337
  return executePrivacy(input.privacy, operation);
338
338
  }
339
- function remainingEnhancedDeadline(result) {
340
- if (!result || typeof result !== "object")
341
- return ECKO_RESULT_TIMEOUT_MS;
342
- const deadlineAt = result["deadlineAt"];
343
- if (typeof deadlineAt !== "string")
344
- return ECKO_RESULT_TIMEOUT_MS;
345
- const parsed = Date.parse(deadlineAt);
346
- if (!Number.isFinite(parsed))
347
- return ECKO_RESULT_TIMEOUT_MS;
348
- return Math.max(1, Math.min(ECKO_RESULT_TIMEOUT_MS, parsed - Date.now()));
349
- }
350
339
  async function executePrivacy(privacy, operation) {
351
340
  if (operation.kind === "privacy.providers.list")
352
341
  return (await privacy.listPrivacyProviders());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -55,9 +55,9 @@
55
55
  "@helyx/core": ">=0.1.0 <0.2.0",
56
56
  "@helyx/database": ">=2.0.0 <3.0.0",
57
57
  "@helyx/framework": ">=0.1.0 <0.2.0",
58
- "@helyx/module-manifest": ">=0.10.0 <0.11.0",
58
+ "@helyx/module-manifest": ">=0.11.0 <0.12.0",
59
59
  "@helyx/platform": ">=1.0.0 <2.0.0",
60
- "@helyx/sdk": ">=0.12.0 <0.13.0",
60
+ "@helyx/sdk": ">=0.13.0 <0.14.0",
61
61
  "@helyx/status-client": ">=0.2.0 <0.3.0",
62
62
  "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
63
63
  "discord.js": "14.27.0",