@helyx/bot 1.1.0 → 1.2.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 (85) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +16 -1
  3. package/dist/auto-moderation-scanner-registry.d.ts +27 -0
  4. package/dist/auto-moderation-scanner-registry.js +172 -0
  5. package/dist/cloud-connector.js +3 -0
  6. package/dist/cloud-control-dispatcher.js +32 -0
  7. package/dist/command-path-contract.js +1 -0
  8. package/dist/control-permissions.d.ts +2 -2
  9. package/dist/control-permissions.js +4 -2
  10. package/dist/control-server.d.ts +72 -16
  11. package/dist/control-server.js +150 -250
  12. package/dist/dashboard-action-control.d.ts +25 -0
  13. package/dist/dashboard-action-control.js +73 -0
  14. package/dist/dashboard-action-routes.d.ts +9 -0
  15. package/dist/dashboard-action-routes.js +36 -0
  16. package/dist/dashboard-feature-control.d.ts +37 -0
  17. package/dist/dashboard-feature-control.js +206 -0
  18. package/dist/dashboard-module-projection.d.ts +9 -0
  19. package/dist/dashboard-module-projection.js +15 -0
  20. package/dist/dashboard-preview-import-routes.d.ts +6 -0
  21. package/dist/dashboard-preview-import-routes.js +121 -0
  22. package/dist/discord-analytics.d.ts +3 -0
  23. package/dist/discord-analytics.js +49 -5
  24. package/dist/discord-auto-moderation-service.d.ts +21 -0
  25. package/dist/discord-auto-moderation-service.js +425 -0
  26. package/dist/discord-guild-health.d.ts +2 -2
  27. package/dist/discord-member-enforcement-service.d.ts +26 -0
  28. package/dist/discord-member-enforcement-service.js +590 -0
  29. package/dist/discord-message-enforcement-service.d.ts +9 -0
  30. package/dist/discord-message-enforcement-service.js +102 -0
  31. package/dist/discord-violation-thread-service.d.ts +33 -0
  32. package/dist/discord-violation-thread-service.js +789 -0
  33. package/dist/ecko-evidence-receiver.d.ts +32 -0
  34. package/dist/ecko-evidence-receiver.js +173 -0
  35. package/dist/ecko-transport-auth.d.ts +19 -0
  36. package/dist/ecko-transport-auth.js +80 -0
  37. package/dist/ecko-transport-routes.d.ts +19 -0
  38. package/dist/ecko-transport-routes.js +130 -0
  39. package/dist/embedded-auto-moderation-pool.d.ts +46 -0
  40. package/dist/embedded-auto-moderation-pool.js +324 -0
  41. package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
  42. package/dist/embedded-auto-moderation-scanner.js +263 -0
  43. package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
  44. package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
  45. package/dist/embedded-auto-moderation-worker.d.ts +2 -0
  46. package/dist/embedded-auto-moderation-worker.js +13 -0
  47. package/dist/events.d.ts +5 -1
  48. package/dist/events.js +243 -4
  49. package/dist/guilds.d.ts +2 -2
  50. package/dist/guilds.js +14 -8
  51. package/dist/hosted-server-moderation.d.ts +16 -5
  52. package/dist/hosted-server-moderation.js +32 -4
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +84 -14
  55. package/dist/moderation-action-registry.d.ts +11 -0
  56. package/dist/moderation-action-registry.js +375 -0
  57. package/dist/moderation-case-disassociation.d.ts +13 -0
  58. package/dist/moderation-case-disassociation.js +83 -0
  59. package/dist/moderation-platform-services.d.ts +27 -0
  60. package/dist/moderation-platform-services.js +50 -0
  61. package/dist/modules.d.ts +6 -0
  62. package/dist/modules.js +65 -3
  63. package/dist/privileged-message-metadata-cache.d.ts +35 -0
  64. package/dist/privileged-message-metadata-cache.js +70 -0
  65. package/dist/scheduled-task-runner.js +24 -1
  66. package/dist/shard-contracts.d.ts +151 -1
  67. package/dist/shard-contracts.js +142 -1
  68. package/dist/shard-coordinator.d.ts +1 -1
  69. package/dist/shard-ecko-scanner-registry.d.ts +38 -0
  70. package/dist/shard-ecko-scanner-registry.js +96 -0
  71. package/dist/shard-router.d.ts +23 -0
  72. package/dist/shard-router.js +69 -5
  73. package/dist/shard-supervisor.d.ts +11 -0
  74. package/dist/shard-supervisor.js +67 -4
  75. package/dist/shard-worker.d.ts +15 -1
  76. package/dist/shard-worker.js +95 -17
  77. package/dist/transient-violation-evidence.d.ts +39 -0
  78. package/dist/transient-violation-evidence.js +132 -0
  79. package/documentation/README.md +2 -0
  80. package/documentation/examples/.env.example +12 -0
  81. package/documentation/guides/self-hosting.md +17 -1
  82. package/documentation/modules/auto-moderation.md +51 -0
  83. package/documentation/modules/moderation.md +42 -0
  84. package/documentation/reference/environment.md +11 -0
  85. package/package.json +17 -5
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ const snowflake = z.string().regex(/^\d{17,20}$/u);
3
+ const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
4
+ const actionParameters = z.object({
5
+ guildId: snowflake,
6
+ moduleId,
7
+ actionId: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
8
+ });
9
+ const mutationContext = z.object({
10
+ actorUserId: snowflake,
11
+ correlationId: z.uuid(),
12
+ });
13
+ export function registerDashboardActionRoutes(server, service) {
14
+ server.post("/v1/guilds/:guildId/modules/:moduleId/actions/:actionId/preview", async (request) => {
15
+ const params = actionParameters.parse(request.params);
16
+ const body = mutationContext.parse(request.body);
17
+ return service.previewDashboardAction({ ...params, ...body });
18
+ });
19
+ server.post("/v1/guilds/:guildId/modules/:moduleId/actions/:actionId", async (request) => {
20
+ const params = actionParameters.parse(request.params);
21
+ const body = mutationContext
22
+ .extend({
23
+ inspectionRevision: z.string().min(1).max(200).optional(),
24
+ })
25
+ .parse(request.body);
26
+ return service.executeDashboardAction({
27
+ ...params,
28
+ actorUserId: body.actorUserId,
29
+ correlationId: body.correlationId,
30
+ ...(body.inspectionRevision === undefined
31
+ ? {}
32
+ : { inspectionRevision: body.inspectionRevision }),
33
+ });
34
+ });
35
+ }
36
+ //# sourceMappingURL=dashboard-action-routes.js.map
@@ -0,0 +1,37 @@
1
+ import type { DashboardResourceField, DashboardResourceScalarField, DashboardSetting } from "@helyx/module-manifest";
2
+ import { type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type ModuleConfigurationService, type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
3
+ export declare function requireDashboardFeatures(services: ServiceAccess, guildId: string, moduleId: string, featureIds: readonly (string | undefined)[]): Promise<void>;
4
+ export declare function changedSettingFeatureIds(settings: readonly DashboardSetting[], current: Readonly<Record<string, unknown>>, next: Readonly<Record<string, unknown>>): readonly string[];
5
+ export declare function scalarInputFeatureIds(inputs: readonly DashboardResourceScalarField[], value: Readonly<Record<string, ManagedResourceValue>>): readonly string[];
6
+ export declare function configurationPreviewFeatureIds(configurations: ModuleConfigurationService, guildId: string, module: ModuleDefinition, preview: {
7
+ featureId?: string | undefined;
8
+ inputs: readonly DashboardResourceScalarField[];
9
+ }, input: {
10
+ value: Readonly<Record<string, unknown>>;
11
+ input: Readonly<Record<string, ManagedResourceValue>>;
12
+ }): Promise<readonly (string | undefined)[]>;
13
+ export declare function resourceFieldFeatureIds(fields: readonly DashboardResourceField[], value: Readonly<Record<string, ManagedResourceValue>>): readonly string[];
14
+ export declare function validateManagedResourceInput(inputs: readonly DashboardResourceScalarField[], value: Readonly<Record<string, ManagedResourceValue>>): void;
15
+ export declare function isBoundedManagedResourceValue(value: unknown, depth?: number, budget?: {
16
+ keys: number;
17
+ }): value is ManagedResourceValue;
18
+ export declare function hasBoundedManagedResourceBytes(value: unknown): boolean;
19
+ export declare function normalizeManagedResourceDetail(value: Omit<ManagedResourceDetail, "description" | "attributes"> & {
20
+ description?: string | undefined;
21
+ attributes?: Readonly<Record<string, ManagedResourceValue>> | undefined;
22
+ }, attributeKeys: readonly string[]): ManagedResourceDetail;
23
+ export declare function normalizeManagedResourcePage(value: {
24
+ items: readonly (Omit<ManagedResourcePage["items"][number], "description" | "attributes"> & {
25
+ description?: string | undefined;
26
+ attributes?: Readonly<Record<string, ManagedResourceValue>> | undefined;
27
+ })[];
28
+ nextCursor?: string | undefined;
29
+ total?: number | undefined;
30
+ }, attributeKeys: readonly string[]): ManagedResourcePage;
31
+ export declare function normalizeManagedResourceRelationPage(value: {
32
+ items: ManagedResourceRelationPage["items"];
33
+ nextCursor?: string | undefined;
34
+ }, valueKeys: readonly string[]): ManagedResourceRelationPage;
35
+ export declare function projectManagedResourceObject(value: Readonly<Record<string, ManagedResourceValue>>, declaredKeys: readonly string[]): Record<string, ManagedResourceValue>;
36
+ export declare function managedResourceColumnKeys(module: ModuleDefinition, resourceType: string): readonly string[];
37
+ //# sourceMappingURL=dashboard-feature-control.d.ts.map
@@ -0,0 +1,206 @@
1
+ import { DashboardActionValidationError, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { BotControlForbiddenError } from "./control-errors.js";
3
+ const SNOWFLAKE_PATTERN = /^\d{17,20}$/u;
4
+ export async function requireDashboardFeatures(services, guildId, moduleId, featureIds) {
5
+ const unique = [...new Set(featureIds.filter(isDefined))];
6
+ if (unique.length === 0)
7
+ return;
8
+ if (!services.has(HELYX_SERVICE_NAMES.featureAccess))
9
+ throw new BotControlForbiddenError();
10
+ const access = services.get(HELYX_SERVICE_NAMES.featureAccess);
11
+ const decisions = await Promise.all(unique.map((featureId) => access.check({ guildId, moduleId, featureId }))).catch(() => []);
12
+ if (decisions.length !== unique.length ||
13
+ decisions.some(({ decision }) => decision !== "granted"))
14
+ throw new BotControlForbiddenError();
15
+ }
16
+ export function changedSettingFeatureIds(settings, current, next) {
17
+ return settings.flatMap((setting) => {
18
+ if (!Object.hasOwn(next, setting.key))
19
+ return [];
20
+ const changed = !configurationValuesEqual(Object.hasOwn(current, setting.key)
21
+ ? current[setting.key]
22
+ : "default" in setting
23
+ ? setting.default
24
+ : undefined, next[setting.key]);
25
+ if (!changed)
26
+ return [];
27
+ const selectedFeatureId = setting.type === "select" && typeof next[setting.key] === "string"
28
+ ? setting.options.find(({ value }) => value === next[setting.key])
29
+ ?.featureId
30
+ : undefined;
31
+ return [setting.featureId, selectedFeatureId].filter(isDefined);
32
+ });
33
+ }
34
+ export function scalarInputFeatureIds(inputs, value) {
35
+ return inputs.flatMap((input) => input.featureId && Object.hasOwn(value, input.key) ? [input.featureId] : []);
36
+ }
37
+ export async function configurationPreviewFeatureIds(configurations, guildId, module, preview, input) {
38
+ const current = await configurations.get(guildId, module.manifest.id);
39
+ return [
40
+ preview.featureId,
41
+ ...scalarInputFeatureIds(preview.inputs, input.input),
42
+ ...(module.manifest.manifestVersion === "0.5"
43
+ ? changedSettingFeatureIds(module.manifest.dashboard?.settings ?? [], current?.value ?? {}, input.value)
44
+ : []),
45
+ ];
46
+ }
47
+ export function resourceFieldFeatureIds(fields, value) {
48
+ const featureIds = [];
49
+ for (const field of fields) {
50
+ const item = value[field.key];
51
+ if (item === undefined)
52
+ continue;
53
+ if (field.featureId &&
54
+ (!("default" in field) || !configurationValuesEqual(field.default, item)))
55
+ featureIds.push(field.featureId);
56
+ if (field.type !== "list" || !Array.isArray(item))
57
+ continue;
58
+ for (const entry of item) {
59
+ if (!isRecord(entry))
60
+ continue;
61
+ featureIds.push(...scalarInputFeatureIds(field.fields, entry));
62
+ }
63
+ }
64
+ return featureIds;
65
+ }
66
+ export function validateManagedResourceInput(inputs, value) {
67
+ const declared = new Map(inputs.map((input) => [input.key, input]));
68
+ for (const key of Object.keys(value))
69
+ if (!declared.has(key))
70
+ throw new DashboardActionValidationError(`The action value "${key}" is not declared by the module.`);
71
+ for (const input of inputs) {
72
+ const item = value[input.key];
73
+ if (item === undefined) {
74
+ if (input.required)
75
+ throw new DashboardActionValidationError(`${input.label} is required for this action.`);
76
+ continue;
77
+ }
78
+ const valid = input.type === "boolean"
79
+ ? typeof item === "boolean"
80
+ : input.type === "integer"
81
+ ? typeof item === "number" &&
82
+ Number.isInteger(item) &&
83
+ item >= input.minimum &&
84
+ item <= input.maximum &&
85
+ (item - input.minimum) % (input.step ?? 1) === 0
86
+ : input.type === "select"
87
+ ? typeof item === "string" &&
88
+ input.options.some(({ value: option }) => option === item)
89
+ : input.type === "discord-channel" || input.type === "discord-role"
90
+ ? isValidDiscordResourceSelection(input, item)
91
+ : typeof item === "string" &&
92
+ item.length >= input.minLength &&
93
+ item.length <= input.maxLength;
94
+ if (!valid)
95
+ throw new DashboardActionValidationError(`${input.label} is not a valid action value.`);
96
+ }
97
+ }
98
+ function isValidDiscordResourceSelection(input, value) {
99
+ if (input.multiple !== true)
100
+ return typeof value === "string" && SNOWFLAKE_PATTERN.test(value);
101
+ if (!Array.isArray(value))
102
+ return false;
103
+ const minimum = input.minItems ?? (input.required ? 1 : 0);
104
+ const maximum = input.maxItems ?? 25;
105
+ return (value.length >= minimum &&
106
+ value.length <= maximum &&
107
+ new Set(value).size === value.length &&
108
+ value.every((item) => typeof item === "string" && SNOWFLAKE_PATTERN.test(item)));
109
+ }
110
+ export function isBoundedManagedResourceValue(value, depth = 0, budget = { keys: 0 }) {
111
+ if (depth > 8)
112
+ return false;
113
+ if (value === null ||
114
+ typeof value === "boolean" ||
115
+ (typeof value === "number" && Number.isFinite(value)))
116
+ return true;
117
+ if (typeof value === "string")
118
+ return value.length <= 4_000;
119
+ if (Array.isArray(value))
120
+ return (value.length <= 100 &&
121
+ value.every((item) => isBoundedManagedResourceValue(item, depth + 1, budget)));
122
+ if (!isRecord(value))
123
+ return false;
124
+ const prototype = Object.getPrototypeOf(value);
125
+ if (prototype !== Object.prototype && prototype !== null)
126
+ return false;
127
+ const entries = Object.entries(value);
128
+ budget.keys += entries.length;
129
+ return (entries.length <= 100 &&
130
+ budget.keys <= 500 &&
131
+ entries.every(([key, item]) => key.length >= 1 &&
132
+ key.length <= 100 &&
133
+ isBoundedManagedResourceValue(item, depth + 1, budget)));
134
+ }
135
+ export function hasBoundedManagedResourceBytes(value) {
136
+ return (new TextEncoder().encode(JSON.stringify(value)).byteLength <= 48 * 1_024);
137
+ }
138
+ export function normalizeManagedResourceDetail(value, attributeKeys) {
139
+ return {
140
+ id: value.id,
141
+ revision: value.revision,
142
+ status: value.status,
143
+ title: value.title,
144
+ ...(value.description === undefined
145
+ ? {}
146
+ : { description: value.description }),
147
+ ...(value.attributes === undefined || attributeKeys.length === 0
148
+ ? {}
149
+ : {
150
+ attributes: projectManagedResourceObject(value.attributes, attributeKeys),
151
+ }),
152
+ updatedAt: value.updatedAt,
153
+ value: value.value,
154
+ };
155
+ }
156
+ export function normalizeManagedResourcePage(value, attributeKeys) {
157
+ return {
158
+ items: value.items.map((item) => ({
159
+ id: item.id,
160
+ revision: item.revision,
161
+ status: item.status,
162
+ title: item.title,
163
+ ...(item.description === undefined
164
+ ? {}
165
+ : { description: item.description }),
166
+ ...(item.attributes === undefined || attributeKeys.length === 0
167
+ ? {}
168
+ : {
169
+ attributes: projectManagedResourceObject(item.attributes, attributeKeys),
170
+ }),
171
+ updatedAt: item.updatedAt,
172
+ })),
173
+ ...(value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor }),
174
+ ...(value.total === undefined ? {} : { total: value.total }),
175
+ };
176
+ }
177
+ export function normalizeManagedResourceRelationPage(value, valueKeys) {
178
+ return {
179
+ items: value.items.map((item) => ({
180
+ id: item.id,
181
+ value: projectManagedResourceObject(item.value, valueKeys),
182
+ })),
183
+ ...(value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor }),
184
+ };
185
+ }
186
+ export function projectManagedResourceObject(value, declaredKeys) {
187
+ const allowed = new Set(declaredKeys);
188
+ return Object.fromEntries(Object.entries(value)
189
+ .filter(([key]) => allowed.has(key))
190
+ .map(([key, item]) => [key, structuredClone(item)]));
191
+ }
192
+ export function managedResourceColumnKeys(module, resourceType) {
193
+ return (module.manifest.dashboard?.resources
194
+ ?.find(({ id }) => id === resourceType)
195
+ ?.columns?.map(({ key }) => key) ?? []);
196
+ }
197
+ function configurationValuesEqual(left, right) {
198
+ return JSON.stringify(left) === JSON.stringify(right);
199
+ }
200
+ function isDefined(value) {
201
+ return value !== undefined;
202
+ }
203
+ function isRecord(value) {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+ //# sourceMappingURL=dashboard-feature-control.js.map
@@ -9,10 +9,15 @@ interface DashboardLogEventDefinition {
9
9
  }
10
10
  interface DashboardActionDefinition {
11
11
  id: string;
12
+ permissionId?: string | undefined;
13
+ featureId?: string | undefined;
12
14
  label: string;
13
15
  description: string;
14
16
  buttonLabel: string;
15
17
  style: "primary" | "secondary" | "danger";
18
+ confirmation?: string | undefined;
19
+ inputs?: readonly unknown[] | undefined;
20
+ inspection?: "optional" | "required" | undefined;
16
21
  }
17
22
  type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
18
23
  export interface DashboardModuleDefinition {
@@ -27,6 +32,10 @@ export interface DashboardModuleDefinition {
27
32
  tier: ModuleTier;
28
33
  requiredPermissions: string[];
29
34
  settings: readonly unknown[];
35
+ settingsMutationPermissionId?: string;
36
+ settingsPreview?: Readonly<Record<string, unknown>>;
37
+ features: readonly unknown[];
38
+ permissionOperations: readonly unknown[];
30
39
  commands: DashboardCommandDefinition[];
31
40
  logEvents: DashboardLogEventDefinition[];
32
41
  actions: DashboardActionDefinition[];
@@ -15,6 +15,21 @@ export function mapDashboardModule(module) {
15
15
  tier: getModuleTier(module.manifest),
16
16
  requiredPermissions: [...module.manifest.permissions],
17
17
  settings: dashboard.settings,
18
+ ...("settingsMutationPermissionId" in dashboard &&
19
+ dashboard.settingsMutationPermissionId
20
+ ? {
21
+ settingsMutationPermissionId: dashboard.settingsMutationPermissionId,
22
+ }
23
+ : {}),
24
+ ...("settingsPreview" in dashboard && dashboard.settingsPreview
25
+ ? { settingsPreview: structuredClone(dashboard.settingsPreview) }
26
+ : {}),
27
+ features: module.manifest.manifestVersion === "0.5"
28
+ ? module.manifest.features.map((feature) => structuredClone(feature))
29
+ : [],
30
+ permissionOperations: module.manifest.manifestVersion === "0.5"
31
+ ? module.manifest.permissionOperations.map((operation) => structuredClone(operation))
32
+ : [],
18
33
  commands: dashboardCommands(module),
19
34
  logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
20
35
  actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
@@ -0,0 +1,6 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { BotControlHandler } from "./control-server.js";
3
+ /** Registers the bounded, non-persisting preview and import boundaries. */
4
+ export declare function registerDashboardPreviewImportRoutes(server: FastifyInstance, service: BotControlHandler): void;
5
+ export declare function registerDashboardResourceMutationRoutes(server: FastifyInstance, service: BotControlHandler): void;
6
+ //# sourceMappingURL=dashboard-preview-import-routes.d.ts.map
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ import { hasBoundedManagedResourceBytes, isBoundedManagedResourceValue, } from "./dashboard-feature-control.js";
3
+ const snowflake = z.string().regex(/^\d{17,20}$/u);
4
+ const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
5
+ const resourceType = z.string().regex(/^[a-z][a-z0-9_-]*$/u);
6
+ const mutationContext = z.object({
7
+ actorUserId: snowflake,
8
+ correlationId: z.uuid(),
9
+ });
10
+ const configurationValue = z.record(z.string(), z.union([
11
+ z.null(),
12
+ z.string(),
13
+ z.boolean(),
14
+ z.number().finite(),
15
+ z.array(z.string()).max(100),
16
+ ]));
17
+ const managedResourceValue = z.custom((value) => isBoundedManagedResourceValue(value), "Managed resource value exceeds the transport limits");
18
+ const managedResourceObject = z
19
+ .record(z.string().min(1).max(100), managedResourceValue)
20
+ .refine(hasBoundedManagedResourceBytes);
21
+ /** Registers the bounded, non-persisting preview and import boundaries. */
22
+ export function registerDashboardPreviewImportRoutes(server, service) {
23
+ server.post("/v1/guilds/:guildId/modules/:moduleId/configuration/preview", async (request) => {
24
+ const params = z
25
+ .object({ guildId: snowflake, moduleId })
26
+ .strict()
27
+ .parse(request.params);
28
+ const body = mutationContext
29
+ .extend({ value: configurationValue, input: managedResourceObject })
30
+ .strict()
31
+ .parse(request.body);
32
+ return service.previewConfiguration({ ...params, ...body });
33
+ });
34
+ server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/import", async (request) => {
35
+ const params = z
36
+ .object({ guildId: snowflake, moduleId, resourceType })
37
+ .strict()
38
+ .parse(request.params);
39
+ const body = mutationContext
40
+ .extend({
41
+ format: z.literal("utf8_text"),
42
+ content: z
43
+ .string()
44
+ .min(1)
45
+ .refine((value) => Buffer.byteLength(value, "utf8") <= 48 * 1_024, "Managed resource import exceeds the transport byte limit"),
46
+ })
47
+ .strict()
48
+ .parse(request.body);
49
+ return service.importManagedResource({ ...params, ...body });
50
+ });
51
+ }
52
+ export function registerDashboardResourceMutationRoutes(server, service) {
53
+ const resourceParams = z
54
+ .object({ guildId: snowflake, moduleId, resourceType })
55
+ .strict();
56
+ const identifiedResourceParams = resourceParams.extend({
57
+ resourceId: z.string().trim().min(1).max(100),
58
+ });
59
+ server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/preview", async (request) => {
60
+ const params = resourceParams.parse(request.params);
61
+ const body = mutationContext
62
+ .extend({ value: managedResourceObject })
63
+ .strict()
64
+ .parse(request.body);
65
+ return service.previewManagedResource({ ...params, ...body });
66
+ });
67
+ server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType", async (request) => {
68
+ const params = resourceParams.parse(request.params);
69
+ const body = mutationContext
70
+ .extend({
71
+ intent: z.enum(["draft", "publish"]).optional().default("publish"),
72
+ value: managedResourceObject,
73
+ })
74
+ .strict()
75
+ .parse(request.body);
76
+ return service.createManagedResource({ ...params, ...body });
77
+ });
78
+ server.put("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
79
+ const params = identifiedResourceParams.parse(request.params);
80
+ const body = mutationContext
81
+ .extend({
82
+ expectedRevision: z.number().int().min(0),
83
+ value: managedResourceObject.optional().default({}),
84
+ })
85
+ .strict()
86
+ .parse(request.body);
87
+ return service.updateManagedResource({ ...params, ...body });
88
+ });
89
+ server.delete("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
90
+ const params = identifiedResourceParams.parse(request.params);
91
+ const body = mutationContext
92
+ .extend({ expectedRevision: z.number().int().min(0) })
93
+ .strict()
94
+ .parse(request.body);
95
+ return service.deleteManagedResource({ ...params, ...body });
96
+ });
97
+ server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId/actions/:actionId", async (request) => {
98
+ const params = identifiedResourceParams
99
+ .extend({ actionId: resourceType })
100
+ .parse(request.params);
101
+ const body = mutationContext
102
+ .extend({
103
+ expectedRevision: z.number().int().min(0),
104
+ value: managedResourceObject.optional().default({}),
105
+ actorAuthenticatedAt: z.iso.datetime({ offset: true }).optional(),
106
+ })
107
+ .strict()
108
+ .parse(request.body);
109
+ return service.executeManagedResourceAction({
110
+ ...params,
111
+ actorUserId: body.actorUserId,
112
+ correlationId: body.correlationId,
113
+ expectedRevision: body.expectedRevision,
114
+ value: body.value,
115
+ ...(body.actorAuthenticatedAt === undefined
116
+ ? {}
117
+ : { actorAuthenticatedAt: body.actorAuthenticatedAt }),
118
+ });
119
+ });
120
+ }
121
+ //# sourceMappingURL=dashboard-preview-import-routes.js.map
@@ -1,6 +1,7 @@
1
1
  import type { DiscordAnalyticsInspectionService } from "@helyx/sdk";
2
2
  import { type Client } from "discord.js";
3
3
  export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspectionService {
4
+ #private;
4
5
  private readonly client;
5
6
  constructor(client: Client);
6
7
  listChannels(input: {
@@ -25,6 +26,8 @@ export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspe
25
26
  limit: number;
26
27
  includeArchived: boolean;
27
28
  includePrivate: boolean;
29
+ signal?: AbortSignal;
28
30
  }): ReturnType<DiscordAnalyticsInspectionService["listThreads"]>;
31
+ private activeThreads;
29
32
  }
30
33
  //# sourceMappingURL=discord-analytics.d.ts.map
@@ -2,8 +2,10 @@ import { ChannelType, GatewayIntentBits, PermissionFlagsBits, } from "discord.js
2
2
  const MAX_MESSAGE_PAGE = 100;
3
3
  const MAX_MEMBER_PAGE = 1_000;
4
4
  const MAX_THREAD_PAGE = 100;
5
+ const ACTIVE_THREAD_CACHE_MS = 15_000;
5
6
  export class DiscordAnalyticsInspection {
6
7
  client;
8
+ #activeThreads = new WeakMap();
7
9
  constructor(client) {
8
10
  this.client = client;
9
11
  }
@@ -147,12 +149,12 @@ export class DiscordAnalyticsInspection {
147
149
  if (input.cursor && !cursor)
148
150
  return unavailable("not_supported");
149
151
  try {
150
- const parents = await threadParents(guild, input.parentChannelId);
152
+ const parents = await abortable(threadParents(guild, input.parentChannelId), input.signal);
151
153
  const privateCoverage = privateThreadCoverage(guild);
152
154
  const current = cursor ?? { phase: "active", offset: 0 };
153
155
  if (current.phase === "active") {
154
- const active = await guild.channels.fetchActiveThreads(false);
155
- const facts = [...active.threads.values()]
156
+ const active = await abortable(this.activeThreads(guild), input.signal);
157
+ const facts = active
156
158
  .filter((thread) => !input.parentChannelId ||
157
159
  thread.parentId === input.parentChannelId)
158
160
  .sort((left, right) => left.id.localeCompare(right.id))
@@ -183,12 +185,12 @@ export class DiscordAnalyticsInspection {
183
185
  const phase = phases[phaseIndex];
184
186
  while (parentIndex < parents.length) {
185
187
  const parent = parents[parentIndex];
186
- const page = await parent.threads.fetchArchived({
188
+ const page = await abortable(parent.threads.fetchArchived({
187
189
  type: phase,
188
190
  fetchAll: phase === "private" && privateCoverage === "all",
189
191
  limit,
190
192
  ...(before ? { before } : {}),
191
- }, false);
193
+ }, false), input.signal);
192
194
  const threads = [...page.threads.values()].sort((left, right) => left.id.localeCompare(right.id));
193
195
  const items = threads.map((thread) => threadFact(guild, thread));
194
196
  if (page.hasMore && threads.length > 0) {
@@ -228,6 +230,23 @@ export class DiscordAnalyticsInspection {
228
230
  return unavailable("discord_unavailable");
229
231
  }
230
232
  }
233
+ activeThreads(guild) {
234
+ const cached = this.#activeThreads.get(guild);
235
+ if (cached && cached.expiresAt > Date.now())
236
+ return cached.promise;
237
+ const promise = guild.channels
238
+ .fetchActiveThreads(false)
239
+ .then((response) => [...response.threads.values()]);
240
+ this.#activeThreads.set(guild, {
241
+ expiresAt: Date.now() + ACTIVE_THREAD_CACHE_MS,
242
+ promise,
243
+ });
244
+ void promise.catch(() => {
245
+ if (this.#activeThreads.get(guild)?.promise === promise)
246
+ this.#activeThreads.delete(guild);
247
+ });
248
+ return promise;
249
+ }
231
250
  }
232
251
  function analyticsChannelType(channel) {
233
252
  switch (channel.type) {
@@ -246,6 +265,10 @@ function analyticsChannelType(channel) {
246
265
  }
247
266
  }
248
267
  async function threadParents(guild, parentChannelId) {
268
+ if (parentChannelId) {
269
+ const channel = await guild.channels.fetch(parentChannelId);
270
+ return channel && "threads" in channel ? [channel] : [];
271
+ }
249
272
  const channels = await guild.channels.fetch();
250
273
  return [...channels.values()]
251
274
  .filter((channel) => channel !== null &&
@@ -334,4 +357,25 @@ function snowflakeTimestamp(id) {
334
357
  function unavailable(code) {
335
358
  return { available: false, code };
336
359
  }
360
+ function abortable(promise, signal) {
361
+ if (!signal)
362
+ return promise;
363
+ return new Promise((resolve, reject) => {
364
+ const abort = () => reject(signal.reason instanceof Error
365
+ ? signal.reason
366
+ : new Error("Discord Analytics inspection was aborted"));
367
+ if (signal.aborted) {
368
+ abort();
369
+ return;
370
+ }
371
+ signal.addEventListener("abort", abort, { once: true });
372
+ void promise.then((value) => {
373
+ signal.removeEventListener("abort", abort);
374
+ resolve(value);
375
+ }, (error) => {
376
+ signal.removeEventListener("abort", abort);
377
+ reject(error instanceof Error ? error : new Error("Discord read failed"));
378
+ });
379
+ });
380
+ }
337
381
  //# sourceMappingURL=discord-analytics.js.map
@@ -0,0 +1,21 @@
1
+ import type { DiscordAutoModerationAction, DiscordAutoModerationMutationResult, DiscordAutoModerationRuleSnapshot, DiscordAutoModerationService } from "@helyx/sdk";
2
+ import { type AutoModerationAction, type AutoModerationRule, type Client } from "discord.js";
3
+ /** Discord REST adapter scoped to guilds owned by this client/shard. */
4
+ export declare class DiscordAutoModerationAdapter implements DiscordAutoModerationService {
5
+ #private;
6
+ private readonly client;
7
+ constructor(client: Client);
8
+ list(input: {
9
+ guildId: string;
10
+ }): Promise<readonly DiscordAutoModerationRuleSnapshot[]>;
11
+ read(input: {
12
+ guildId: string;
13
+ ruleId: string;
14
+ }): Promise<DiscordAutoModerationRuleSnapshot | null>;
15
+ create(input: Parameters<DiscordAutoModerationService["create"]>[0]): Promise<DiscordAutoModerationMutationResult>;
16
+ update(input: Parameters<DiscordAutoModerationService["update"]>[0]): Promise<DiscordAutoModerationMutationResult>;
17
+ delete(input: Parameters<DiscordAutoModerationService["delete"]>[0]): Promise<DiscordAutoModerationMutationResult>;
18
+ }
19
+ export declare function toRuleSnapshot(rule: AutoModerationRule): DiscordAutoModerationRuleSnapshot;
20
+ export declare function fromDiscordAction(action: AutoModerationAction): DiscordAutoModerationAction;
21
+ //# sourceMappingURL=discord-auto-moderation-service.d.ts.map