@helyx/bot 0.4.36 → 0.4.38

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 (65) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +17 -2
  3. package/dist/cloud-management-module.js +275 -206
  4. package/dist/command-registration-policy.d.ts +8 -0
  5. package/dist/command-registration-policy.js +39 -0
  6. package/dist/command-sync.d.ts +5 -1
  7. package/dist/command-sync.js +67 -3
  8. package/dist/control-server.d.ts +15 -3
  9. package/dist/control-server.js +34 -48
  10. package/dist/dashboard-commands.js +5 -1
  11. package/dist/developer-operations-module.d.ts +2 -0
  12. package/dist/developer-operations-module.js +65 -4
  13. package/dist/discord-guild-health.d.ts +2 -2
  14. package/dist/discord-rest.d.ts +5 -0
  15. package/dist/discord-rest.js +9 -0
  16. package/dist/events.js +4 -2
  17. package/dist/first-time-command-setup.d.ts +25 -0
  18. package/dist/first-time-command-setup.js +22 -0
  19. package/dist/guild-command-control-routes.d.ts +5 -0
  20. package/dist/guild-command-control-routes.js +191 -0
  21. package/dist/guild-command-discord-client.d.ts +21 -0
  22. package/dist/guild-command-discord-client.js +50 -0
  23. package/dist/guild-command-projection.d.ts +46 -0
  24. package/dist/guild-command-projection.js +254 -0
  25. package/dist/guild-command-read-service.d.ts +201 -0
  26. package/dist/guild-command-read-service.js +272 -0
  27. package/dist/guild-command-release.d.ts +19 -0
  28. package/dist/guild-command-release.js +47 -0
  29. package/dist/guild-command-worker.d.ts +49 -0
  30. package/dist/guild-command-worker.js +443 -0
  31. package/dist/guilds.d.ts +1 -1
  32. package/dist/guilds.js +41 -26
  33. package/dist/index.js +75 -4
  34. package/dist/interactions.d.ts +15 -4
  35. package/dist/interactions.js +285 -70
  36. package/dist/management-module.js +117 -41
  37. package/dist/module-logging.d.ts +1 -0
  38. package/dist/module-logging.js +5 -5
  39. package/dist/module-status-control.d.ts +19 -0
  40. package/dist/module-status-control.js +69 -0
  41. package/dist/modules.d.ts +2 -0
  42. package/dist/modules.js +21 -2
  43. package/dist/nexus-commands.d.ts +1 -12
  44. package/dist/nexus-commands.js +1 -99
  45. package/dist/oauth-module.js +134 -88
  46. package/dist/permissions-module.js +112 -39
  47. package/dist/privacy-module.d.ts +2 -0
  48. package/dist/privacy-module.js +60 -0
  49. package/dist/scheduled-task-runner.js +36 -1
  50. package/dist/shard-contracts.d.ts +24 -0
  51. package/dist/shard-contracts.js +20 -0
  52. package/dist/shard-router.d.ts +1 -0
  53. package/dist/shard-router.js +7 -0
  54. package/dist/shard-supervisor.d.ts +2 -1
  55. package/dist/shard-supervisor.js +94 -16
  56. package/dist/shard-worker.d.ts +6 -1
  57. package/dist/shard-worker.js +9 -1
  58. package/dist/shared-commands.d.ts +13 -0
  59. package/dist/shared-commands.js +163 -0
  60. package/dist/support-module.js +51 -51
  61. package/dist/trivia-data-provider.d.ts +26 -0
  62. package/dist/trivia-data-provider.js +51 -0
  63. package/package.json +5 -4
  64. package/dist/oauth-module.test.d.ts +0 -2
  65. package/dist/oauth-module.test.js +0 -172
@@ -0,0 +1,191 @@
1
+ import { z } from "zod";
2
+ const snowflake = z.string().regex(/^\d{17,20}$/u);
3
+ const reconciliationBody = z
4
+ .object({
5
+ actorUserId: snowflake,
6
+ correlationId: z.string().min(1).max(200),
7
+ expectedRevision: z.string().regex(/^\d+$/u),
8
+ source: z.enum(["customer_rebuild", "owner_repair"]),
9
+ budgetClass: z.enum(["customer", "owner"]),
10
+ delayMs: z.number().int().min(0).max(86_400_000),
11
+ summary: z.string().min(1).max(500),
12
+ })
13
+ .strict();
14
+ const profileBody = z
15
+ .object({
16
+ actorUserId: snowflake,
17
+ correlationId: z.string().min(1).max(200),
18
+ expectedRevision: z.string().regex(/^\d+$/u),
19
+ profile: z.enum(["enabled_only", "all_released", "all_loaded"]),
20
+ reasonSummary: z.string().min(1).max(500),
21
+ })
22
+ .strict();
23
+ const migrationHoldBody = z
24
+ .object({
25
+ actorUserId: snowflake,
26
+ correlationId: z.string().min(1).max(200),
27
+ expectedRevision: z.string().regex(/^\d+$/u),
28
+ enabled: z.boolean(),
29
+ })
30
+ .strict();
31
+ const refreshBody = z
32
+ .object({
33
+ actorUserId: snowflake,
34
+ correlationId: z.string().min(1).max(200),
35
+ })
36
+ .strict();
37
+ const moduleAvailabilityBody = z
38
+ .object({
39
+ moduleId: z.string().regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u),
40
+ correlationId: z.string().min(1).max(200),
41
+ })
42
+ .strict();
43
+ export function registerGuildCommandControlRoutes(server, commands, setup) {
44
+ server.get("/v1/commands/health", async (_request, reply) => {
45
+ reply.header("Cache-Control", "no-store");
46
+ if (!commands)
47
+ return reply.code(503).send({ message: "Command state is unavailable" });
48
+ return commands.getHealth();
49
+ });
50
+ server.post("/v1/commands/reconcile-module", async (request, reply) => {
51
+ reply.header("Cache-Control", "no-store");
52
+ if (!commands)
53
+ return reply.code(503).send({ message: "Command state is unavailable" });
54
+ return commands.reconcileModuleAvailability(moduleAvailabilityBody.parse(request.body));
55
+ });
56
+ server.get("/v1/guilds/:guildId/commands/status", async (request, reply) => {
57
+ reply.header("Cache-Control", "no-store");
58
+ if (!commands)
59
+ return reply.code(503).send({ message: "Command state is unavailable" });
60
+ const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
61
+ return commands.getStatus(guildId);
62
+ });
63
+ server.get("/v1/guilds/:guildId/commands/history", async (request, reply) => {
64
+ reply.header("Cache-Control", "no-store");
65
+ if (!commands)
66
+ return reply.code(503).send({ message: "Command state is unavailable" });
67
+ const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
68
+ const query = z
69
+ .object({
70
+ before: z
71
+ .string()
72
+ .regex(/^[1-9]\d*$/u)
73
+ .optional(),
74
+ since: z.iso.datetime().optional(),
75
+ limit: z.coerce.number().int().min(1).max(10).default(10),
76
+ })
77
+ .strict()
78
+ .parse(request.query);
79
+ return commands.listHistory({
80
+ guildId,
81
+ ...(query.before ? { beforeEventId: BigInt(query.before) } : {}),
82
+ ...(query.since ? { since: new Date(query.since) } : {}),
83
+ limit: query.limit,
84
+ });
85
+ });
86
+ server.post("/v1/guilds/:guildId/commands/first-time-setup/preview", async (request, reply) => {
87
+ reply.header("Cache-Control", "no-store");
88
+ if (!commands)
89
+ return reply
90
+ .code(503)
91
+ .send({ message: "Command state is unavailable" });
92
+ z.object({ guildId: snowflake }).parse(request.params);
93
+ const body = z
94
+ .object({
95
+ moduleIds: z
96
+ .array(z.string().regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u))
97
+ .min(1)
98
+ .max(100),
99
+ })
100
+ .strict()
101
+ .parse(request.body);
102
+ return commands.previewFirstTimeSetup(body.moduleIds);
103
+ });
104
+ server.post("/v1/guilds/:guildId/commands/reconcile", async (request, reply) => {
105
+ reply.header("Cache-Control", "no-store");
106
+ if (!commands)
107
+ return reply
108
+ .code(503)
109
+ .send({ message: "Command state is unavailable" });
110
+ const { guildId } = z
111
+ .object({ guildId: snowflake })
112
+ .parse(request.params);
113
+ const body = reconciliationBody.parse(request.body);
114
+ return commands.requestReconciliation({
115
+ guildId,
116
+ ...body,
117
+ expectedRevision: BigInt(body.expectedRevision),
118
+ });
119
+ });
120
+ server.post("/v1/guilds/:guildId/commands/refresh", async (request, reply) => {
121
+ reply.header("Cache-Control", "no-store");
122
+ if (!commands)
123
+ return reply
124
+ .code(503)
125
+ .send({ message: "Command state is unavailable" });
126
+ const { guildId } = z
127
+ .object({ guildId: snowflake })
128
+ .parse(request.params);
129
+ return commands.refreshStatus({
130
+ guildId,
131
+ ...refreshBody.parse(request.body),
132
+ });
133
+ });
134
+ server.patch("/v1/guilds/:guildId/commands/profile", async (request, reply) => {
135
+ reply.header("Cache-Control", "no-store");
136
+ if (!commands)
137
+ return reply
138
+ .code(503)
139
+ .send({ message: "Command state is unavailable" });
140
+ const { guildId } = z
141
+ .object({ guildId: snowflake })
142
+ .parse(request.params);
143
+ const body = profileBody.parse(request.body);
144
+ return commands.changeProfile({
145
+ guildId,
146
+ ...body,
147
+ expectedRevision: BigInt(body.expectedRevision),
148
+ });
149
+ });
150
+ server.patch("/v1/guilds/:guildId/commands/migration-hold", async (request, reply) => {
151
+ reply.header("Cache-Control", "no-store");
152
+ if (!commands)
153
+ return reply
154
+ .code(503)
155
+ .send({ message: "Command state is unavailable" });
156
+ const { guildId } = z
157
+ .object({ guildId: snowflake })
158
+ .parse(request.params);
159
+ const body = migrationHoldBody.parse(request.body);
160
+ return commands.setMigrationHold({
161
+ guildId,
162
+ ...body,
163
+ expectedRevision: BigInt(body.expectedRevision),
164
+ });
165
+ });
166
+ server.post("/v1/guilds/:guildId/commands/first-time-setup", async (request, reply) => {
167
+ reply.header("Cache-Control", "no-store");
168
+ const { guildId } = z
169
+ .object({ guildId: snowflake })
170
+ .parse(request.params);
171
+ const body = z
172
+ .object({
173
+ moduleIds: z
174
+ .array(z.string().regex(/^helyx\.[a-z0-9][a-z0-9-]*$/u))
175
+ .min(1)
176
+ .max(100),
177
+ expectedRevision: z.string().regex(/^\d+$/u),
178
+ idempotencyKey: z.string().min(1).max(160),
179
+ actorUserId: snowflake,
180
+ correlationId: z.string().min(1).max(200),
181
+ })
182
+ .strict()
183
+ .parse(request.body);
184
+ return setup.completeFirstTimeCommandSetup({
185
+ guildId,
186
+ ...body,
187
+ expectedRevision: BigInt(body.expectedRevision),
188
+ });
189
+ });
190
+ }
191
+ //# sourceMappingURL=guild-command-control-routes.js.map
@@ -0,0 +1,21 @@
1
+ import type { ObservedGuildCommand, ProjectedGuildCommand } from "./guild-command-projection.js";
2
+ export interface GuildCommandDiscordRest {
3
+ get(route: `/${string}`): Promise<unknown>;
4
+ put(route: `/${string}`, options: {
5
+ body: unknown;
6
+ }): Promise<unknown>;
7
+ }
8
+ export declare class GuildCommandDiscordClient {
9
+ #private;
10
+ constructor(input: {
11
+ applicationId: string;
12
+ token: string;
13
+ writeGuildIds: readonly string[];
14
+ writeAllGuilds?: boolean;
15
+ rest?: GuildCommandDiscordRest;
16
+ });
17
+ canWrite(guildId: string): boolean;
18
+ observe(guildId: string): Promise<ObservedGuildCommand[]>;
19
+ replace(guildId: string, definitions: readonly ProjectedGuildCommand[]): Promise<ObservedGuildCommand[]>;
20
+ }
21
+ //# sourceMappingURL=guild-command-discord-client.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { REST, Routes } from "discord.js";
2
+ const SNOWFLAKE = /^\d{17,20}$/u;
3
+ export class GuildCommandDiscordClient {
4
+ #applicationId;
5
+ #writeGuildIds;
6
+ #writeAllGuilds;
7
+ #rest;
8
+ constructor(input) {
9
+ validateSnowflake(input.applicationId, "Application ID");
10
+ if (!input.token)
11
+ throw new Error("Discord token is required");
12
+ for (const guildId of input.writeGuildIds)
13
+ validateSnowflake(guildId, "Write-allowlisted guild ID");
14
+ this.#applicationId = input.applicationId;
15
+ this.#writeGuildIds = new Set(input.writeGuildIds);
16
+ this.#writeAllGuilds = input.writeAllGuilds ?? false;
17
+ this.#rest =
18
+ input.rest ?? new REST({ version: "10" }).setToken(input.token);
19
+ }
20
+ canWrite(guildId) {
21
+ return this.#writeAllGuilds || this.#writeGuildIds.has(guildId);
22
+ }
23
+ async observe(guildId) {
24
+ validateSnowflake(guildId, "Guild ID");
25
+ return parseCommandCollection(await this.#rest.get(Routes.applicationGuildCommands(this.#applicationId, guildId)));
26
+ }
27
+ async replace(guildId, definitions) {
28
+ validateSnowflake(guildId, "Guild ID");
29
+ if (!this.canWrite(guildId))
30
+ throw new Error("Guild command write target is not allowlisted");
31
+ return parseCommandCollection(await this.#rest.put(Routes.applicationGuildCommands(this.#applicationId, guildId), { body: definitions.map(({ definition }) => definition) }));
32
+ }
33
+ }
34
+ function parseCommandCollection(value) {
35
+ if (!Array.isArray(value))
36
+ throw new Error("Discord returned an invalid command collection");
37
+ return value.map((command) => {
38
+ if (!isRecord(command) || typeof command.name !== "string" || !command.name)
39
+ throw new Error("Discord returned an invalid command definition");
40
+ return command;
41
+ });
42
+ }
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ function validateSnowflake(value, label) {
47
+ if (!SNOWFLAKE.test(value))
48
+ throw new Error(`${label} is invalid`);
49
+ }
50
+ //# sourceMappingURL=guild-command-discord-client.js.map
@@ -0,0 +1,46 @@
1
+ import type { GuildCommandVisibilityProfile } from "@helyx/database";
2
+ import type { ModuleDefinition } from "@helyx/sdk";
3
+ export { commandRegistrationPolicy } from "./command-registration-policy.js";
4
+ export interface ProjectedGuildCommand {
5
+ name: string;
6
+ type: number;
7
+ ownerModuleId: string;
8
+ ownerModuleName: string;
9
+ definition: Readonly<Record<string, unknown>>;
10
+ executablePaths: readonly string[];
11
+ }
12
+ export interface GuildCommandProjection {
13
+ definitions: readonly ProjectedGuildCommand[];
14
+ definitionHash: string;
15
+ rootCount: number;
16
+ subcommandCount: number;
17
+ selectedModuleIds: readonly string[];
18
+ }
19
+ export interface ObservedGuildCommand {
20
+ id?: string;
21
+ application_id?: string;
22
+ guild_id?: string;
23
+ version?: string;
24
+ name: string;
25
+ type?: number;
26
+ [key: string]: unknown;
27
+ }
28
+ export interface GuildCommandDiff {
29
+ unchanged: readonly string[];
30
+ create: readonly ProjectedGuildCommand[];
31
+ update: readonly ProjectedGuildCommand[];
32
+ remove: readonly ObservedGuildCommand[];
33
+ conflicts: readonly string[];
34
+ observationRequired: boolean;
35
+ }
36
+ export declare function projectGuildCommands(input: {
37
+ modules: readonly ModuleDefinition[];
38
+ enabledModuleIds: ReadonlySet<string>;
39
+ globallyDisabledModuleIds?: ReadonlySet<string>;
40
+ profile: GuildCommandVisibilityProfile;
41
+ includeInternalCommands?: boolean;
42
+ }): GuildCommandProjection;
43
+ export declare function diffGuildCommands(desired: GuildCommandProjection, observedInput: readonly ObservedGuildCommand[]): GuildCommandDiff;
44
+ export declare function guildCommandDefinitionHash(definitions: readonly Readonly<Record<string, unknown>>[]): string;
45
+ export declare function diffDryRunGuildCommands(desired: GuildCommandProjection, previousDesiredDefinitionHash: string | null): GuildCommandDiff;
46
+ //# sourceMappingURL=guild-command-projection.d.ts.map
@@ -0,0 +1,254 @@
1
+ import { isCustomerSelectableModule } from "@helyx/module-manifest";
2
+ import { createHash } from "node:crypto";
3
+ import { collectCommandRegistrations, } from "./shared-commands.js";
4
+ import { toCommandDefinition } from "./interactions.js";
5
+ import { commandRegistrationPolicy, contributionRegistrationPolicy, } from "./command-registration-policy.js";
6
+ const MAX_GUILD_ROOTS = 75;
7
+ const MAX_IMMEDIATE_CHILDREN = 20;
8
+ export { commandRegistrationPolicy } from "./command-registration-policy.js";
9
+ export function projectGuildCommands(input) {
10
+ const byId = new Map(input.modules.map((module) => [module.manifest.id, module]));
11
+ if (byId.size !== input.modules.length)
12
+ throw new Error("Command projection received duplicate module IDs");
13
+ const globallyDisabled = input.globallyDisabledModuleIds ?? new Set();
14
+ const selected = new Set();
15
+ for (const module of input.modules) {
16
+ if (input.includeInternalCommands &&
17
+ commandRegistrationPolicy(module) === "internal-guild") {
18
+ selectWithDependencies(module, byId, selected);
19
+ continue;
20
+ }
21
+ if (!isCustomerSelectableModule(module.manifest))
22
+ continue;
23
+ if (globallyDisabled.has(module.manifest.id))
24
+ continue;
25
+ const visible = input.profile === "all_loaded" ||
26
+ (input.profile === "all_released" &&
27
+ (module.manifest.releaseStatus === "live" ||
28
+ module.manifest.releaseStatus === "beta")) ||
29
+ (input.profile === "enabled_only" &&
30
+ input.enabledModuleIds.has(module.manifest.id));
31
+ if (visible)
32
+ selectWithDependencies(module, byId, selected);
33
+ }
34
+ const selectedModules = [...selected]
35
+ .map((moduleId) => byId.get(moduleId))
36
+ .filter(Boolean)
37
+ .sort((left, right) => left.manifest.id.localeCompare(right.manifest.id));
38
+ const registrations = collectCommandRegistrations(selectedModules);
39
+ const definitions = [...registrations.values()]
40
+ .filter(({ moduleId, contribution }) => {
41
+ const owner = byId.get(moduleId);
42
+ if (!owner)
43
+ throw new Error(`Command owner is not loaded: ${moduleId}`);
44
+ const policy = projectionRegistrationPolicy(owner, contribution);
45
+ return (policy === "enabled-guild" ||
46
+ (input.includeInternalCommands && policy === "internal-guild"));
47
+ })
48
+ .map(projectRegistration)
49
+ .sort(compareProjectedCommands);
50
+ validateProjectedCommands(definitions);
51
+ const canonicalDefinitions = definitions.map(({ definition }) => definition);
52
+ return {
53
+ definitions,
54
+ definitionHash: guildCommandDefinitionHash(canonicalDefinitions),
55
+ rootCount: definitions.length,
56
+ subcommandCount: definitions.reduce((total, definition) => total + definition.executablePaths.length, 0),
57
+ selectedModuleIds: [...selected].sort(),
58
+ };
59
+ }
60
+ function projectionRegistrationPolicy(module, contribution) {
61
+ return contributionRegistrationPolicy(module, contribution, {
62
+ allowLegacyCustomerGlobals: false,
63
+ });
64
+ }
65
+ export function diffGuildCommands(desired, observedInput) {
66
+ const observed = [...observedInput].sort(compareObservedCommands);
67
+ const conflicts = [];
68
+ const observedByKey = new Map();
69
+ for (const command of observed) {
70
+ const key = commandKey(command);
71
+ if (observedByKey.has(key))
72
+ conflicts.push(`duplicate_observed:${key}`);
73
+ else
74
+ observedByKey.set(key, command);
75
+ }
76
+ const desiredByKey = new Map(desired.definitions.map((command) => [
77
+ commandKey(command.definition),
78
+ command,
79
+ ]));
80
+ const unchanged = [];
81
+ const create = [];
82
+ const update = [];
83
+ for (const [key, command] of desiredByKey) {
84
+ const current = observedByKey.get(key);
85
+ if (!current) {
86
+ create.push(command);
87
+ continue;
88
+ }
89
+ if (canonicalStringify(normalizeGuildCommandDefinition(current)) ===
90
+ canonicalStringify(normalizeGuildCommandDefinition(command.definition)))
91
+ unchanged.push(command.name);
92
+ else
93
+ update.push(command);
94
+ }
95
+ const remove = observed.filter((command) => !desiredByKey.has(commandKey(command)));
96
+ return {
97
+ unchanged,
98
+ create,
99
+ update,
100
+ remove,
101
+ conflicts,
102
+ observationRequired: false,
103
+ };
104
+ }
105
+ export function guildCommandDefinitionHash(definitions) {
106
+ const normalized = [...definitions]
107
+ .sort((left, right) => commandKey(left).localeCompare(commandKey(right)))
108
+ .map((definition) => normalizeGuildCommandDefinition(definition));
109
+ return createHash("sha256")
110
+ .update(canonicalStringify(normalized))
111
+ .digest("hex");
112
+ }
113
+ export function diffDryRunGuildCommands(desired, previousDesiredDefinitionHash) {
114
+ if (previousDesiredDefinitionHash === desired.definitionHash) {
115
+ return {
116
+ unchanged: desired.definitions.map(({ name }) => name),
117
+ create: [],
118
+ update: [],
119
+ remove: [],
120
+ conflicts: [],
121
+ observationRequired: false,
122
+ };
123
+ }
124
+ return {
125
+ unchanged: [],
126
+ create: [],
127
+ update: [],
128
+ remove: [],
129
+ conflicts: [],
130
+ observationRequired: true,
131
+ };
132
+ }
133
+ function selectWithDependencies(module, byId, selected) {
134
+ if (selected.has(module.manifest.id))
135
+ return;
136
+ selected.add(module.manifest.id);
137
+ for (const dependency of module.manifest.dependencies) {
138
+ const definition = byId.get(dependency.id);
139
+ if (!definition)
140
+ throw new Error(`Command projection is missing dependency ${dependency.id} for ${module.manifest.id}`);
141
+ selectWithDependencies(definition, byId, selected);
142
+ }
143
+ }
144
+ function projectRegistration(registration) {
145
+ const definition = toCommandDefinition(registration);
146
+ return {
147
+ name: registration.contribution.name,
148
+ type: Number(definition.type ?? 1),
149
+ ownerModuleId: registration.moduleId,
150
+ ownerModuleName: registration.moduleName,
151
+ definition: canonicalValue(definition),
152
+ executablePaths: executablePaths(registration),
153
+ };
154
+ }
155
+ function executablePaths(registration) {
156
+ if (registration.routeOwners)
157
+ return [...registration.routeOwners.keys()].sort();
158
+ const command = registration.contribution;
159
+ if (command.subcommands?.length)
160
+ return command.subcommands.map(({ name }) => name).sort();
161
+ if (command.subcommandGroups?.length)
162
+ return command.subcommandGroups
163
+ .flatMap((group) => group.subcommands.map((subcommand) => `${group.name}.${subcommand.name}`))
164
+ .sort();
165
+ return [];
166
+ }
167
+ function validateProjectedCommands(definitions) {
168
+ if (definitions.length > MAX_GUILD_ROOTS)
169
+ throw new Error("Guild command projection exceeds 75 roots");
170
+ const keys = new Set();
171
+ for (const command of definitions) {
172
+ const key = `${command.type}:${command.name}`;
173
+ if (keys.has(key))
174
+ throw new Error(`Duplicate projected command: ${key}`);
175
+ keys.add(key);
176
+ const options = command.definition.options;
177
+ if (Array.isArray(options) && options.length > MAX_IMMEDIATE_CHILDREN)
178
+ throw new Error(`Command ${command.name} exceeds 20 immediate children`);
179
+ const contributionChildren = command.executablePaths.filter((path) => !path.includes(".")).length;
180
+ const groups = new Map();
181
+ for (const path of command.executablePaths) {
182
+ const [group, child] = path.split(".");
183
+ if (child)
184
+ groups.set(group, (groups.get(group) ?? 0) + 1);
185
+ }
186
+ if (contributionChildren + groups.size > MAX_IMMEDIATE_CHILDREN)
187
+ throw new Error(`Command ${command.name} exceeds 20 immediate children`);
188
+ for (const [group, count] of groups) {
189
+ if (count > MAX_IMMEDIATE_CHILDREN)
190
+ throw new Error(`Command ${command.name} group ${group} exceeds 20 children`);
191
+ }
192
+ }
193
+ }
194
+ function compareProjectedCommands(left, right) {
195
+ return left.name.localeCompare(right.name) || left.type - right.type;
196
+ }
197
+ function compareObservedCommands(left, right) {
198
+ return (left.name.localeCompare(right.name) ||
199
+ Number(left.type ?? 1) - Number(right.type ?? 1));
200
+ }
201
+ function commandKey(command) {
202
+ if (typeof command.name !== "string" || !command.name)
203
+ throw new Error("Observed command has no valid name");
204
+ const type = command.type === undefined ? 1 : Number(command.type);
205
+ if (!Number.isInteger(type) || type < 1 || type > 3)
206
+ throw new Error(`Observed command ${command.name} has an invalid type`);
207
+ return `${type}:${command.name}`;
208
+ }
209
+ function normalizeGuildCommandDefinition(value, root = true) {
210
+ if (Array.isArray(value))
211
+ return value.map((item) => normalizeGuildCommandDefinition(item, false));
212
+ if (value === null || typeof value !== "object")
213
+ return value;
214
+ const source = value;
215
+ const normalized = Object.entries(source)
216
+ .filter(([key, item]) => {
217
+ if (["id", "application_id", "guild_id", "version"].includes(key))
218
+ return false;
219
+ if (key === "nsfw" && item === false)
220
+ return false;
221
+ if (key === "default_permission" && item === true)
222
+ return false;
223
+ if (key === "default_member_permissions" && item === null)
224
+ return false;
225
+ if (key === "required" && item === false)
226
+ return false;
227
+ if (key.endsWith("_localizations") && item === null)
228
+ return false;
229
+ if (key === "options" && Array.isArray(item) && item.length === 0)
230
+ return false;
231
+ return item !== undefined;
232
+ })
233
+ .map(([key, item]) => [key, normalizeGuildCommandDefinition(item, false)]);
234
+ const result = Object.fromEntries(normalized);
235
+ if (root) {
236
+ result.contexts ??= [0];
237
+ result.integration_types ??= [0];
238
+ }
239
+ return result;
240
+ }
241
+ function canonicalStringify(value) {
242
+ return JSON.stringify(canonicalValue(value));
243
+ }
244
+ function canonicalValue(value) {
245
+ if (Array.isArray(value))
246
+ return value.map(canonicalValue);
247
+ if (value === null || typeof value !== "object")
248
+ return value;
249
+ return Object.fromEntries(Object.entries(value)
250
+ .filter(([, item]) => item !== undefined)
251
+ .sort(([left], [right]) => left.localeCompare(right))
252
+ .map(([key, item]) => [key, canonicalValue(item)]));
253
+ }
254
+ //# sourceMappingURL=guild-command-projection.js.map