@helyx/bot 0.1.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.
@@ -0,0 +1,10 @@
1
+ type CatalogueLocation = string | URL;
2
+ type CatalogueReader = (path: CatalogueLocation) => Promise<string>;
3
+ /**
4
+ * Combines the reviewed catalogue shipped by a hosted deployment with the
5
+ * optional runtime allowlist. A package being installed is never sufficient
6
+ * to make it executable: it must be named by one of these explicit sources.
7
+ */
8
+ export declare function resolveModulePackageSpecifiers(configuredPackages: readonly string[], bundledCatalogueUrl?: URL, reader?: CatalogueReader, selectedCataloguePath?: string): Promise<string[]>;
9
+ export {};
10
+ //# sourceMappingURL=module-configuration.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { z } from "zod";
4
+ const officialModulePackage = /^@helyx\/module-[a-z0-9][a-z0-9-]*$/u;
5
+ const modulePackageCatalogueSchema = z
6
+ .object({
7
+ schemaVersion: z.literal(1),
8
+ packages: z
9
+ .array(z.string().regex(officialModulePackage))
10
+ .superRefine((packages, context) => {
11
+ if (new Set(packages).size !== packages.length) {
12
+ context.addIssue({
13
+ code: "custom",
14
+ message: "package names must be unique",
15
+ });
16
+ }
17
+ }),
18
+ })
19
+ .strict();
20
+ const defaultCatalogueUrl = new URL("../hosted-modules.json", import.meta.url);
21
+ /**
22
+ * Combines the reviewed catalogue shipped by a hosted deployment with the
23
+ * optional runtime allowlist. A package being installed is never sufficient
24
+ * to make it executable: it must be named by one of these explicit sources.
25
+ */
26
+ export async function resolveModulePackageSpecifiers(configuredPackages, bundledCatalogueUrl = defaultCatalogueUrl, reader = (path) => readFile(path, "utf8"), selectedCataloguePath) {
27
+ const bundledPackages = await readModuleCatalogue(bundledCatalogueUrl, reader, true, "hosted");
28
+ const selectedPackages = selectedCataloguePath
29
+ ? await readModuleCatalogue(resolve(selectedCataloguePath), reader, false, "selected")
30
+ : [];
31
+ return [
32
+ ...new Set([
33
+ ...bundledPackages,
34
+ ...selectedPackages,
35
+ ...configuredPackages,
36
+ ]),
37
+ ];
38
+ }
39
+ async function readModuleCatalogue(location, reader, missingAllowed, label) {
40
+ try {
41
+ const source = await reader(location);
42
+ const parsed = modulePackageCatalogueSchema.safeParse(JSON.parse(source));
43
+ if (!parsed.success)
44
+ throw new Error("schema validation failed");
45
+ return parsed.data.packages;
46
+ }
47
+ catch (error) {
48
+ if (missingAllowed && isMissingFileError(error))
49
+ return [];
50
+ throw new Error(`Invalid ${label} module catalogue`, { cause: error });
51
+ }
52
+ }
53
+ function isMissingFileError(error) {
54
+ return (error instanceof Error &&
55
+ "code" in error &&
56
+ error.code === "ENOENT");
57
+ }
58
+ //# sourceMappingURL=module-configuration.js.map
@@ -0,0 +1,21 @@
1
+ import { type DatabasePool } from "@helyx/database";
2
+ import type { ModuleDefinition, ModuleLogDetail, ModuleLoggingService } from "@helyx/sdk";
3
+ import type { Client } from "discord.js";
4
+ import { type InteractionLogger } from "./interactions.js";
5
+ export declare class DiscordModuleLoggingService implements ModuleLoggingService {
6
+ #private;
7
+ private readonly client;
8
+ private readonly database;
9
+ private readonly logger;
10
+ constructor(client: Client, database: DatabasePool, modules: readonly ModuleDefinition[], logger: InteractionLogger);
11
+ emit(input: {
12
+ guildId: string;
13
+ moduleId: string;
14
+ eventId: string;
15
+ summary: string;
16
+ details?: readonly ModuleLogDetail[];
17
+ }): Promise<{
18
+ delivered: boolean;
19
+ }>;
20
+ }
21
+ //# sourceMappingURL=module-logging.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { InstallationRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, } from "@helyx/database";
2
+ import { toReplyOptions } from "./interactions.js";
3
+ const LOGGING_MODULE_ID = "helyx.logging";
4
+ const VIOLET = 0x7c3aed;
5
+ export class DiscordModuleLoggingService {
6
+ client;
7
+ database;
8
+ logger;
9
+ #modules;
10
+ #loggingInstalled;
11
+ constructor(client, database, modules, logger) {
12
+ this.client = client;
13
+ this.database = database;
14
+ this.logger = logger;
15
+ this.#modules = new Map(modules.map((module) => [module.manifest.id, module]));
16
+ this.#loggingInstalled = this.#modules.has(LOGGING_MODULE_ID);
17
+ }
18
+ async emit(input) {
19
+ try {
20
+ if (!this.#loggingInstalled)
21
+ return { delivered: false };
22
+ const source = this.#modules.get(input.moduleId);
23
+ const event = source?.logging?.events.find(({ id }) => id === input.eventId);
24
+ if (!source || !event) {
25
+ throw new Error("Module emitted an undeclared logging event");
26
+ }
27
+ const installations = new InstallationRepository(this.database);
28
+ if (!(await installations.isModuleEnabled(input.guildId, LOGGING_MODULE_ID))) {
29
+ return { delivered: false };
30
+ }
31
+ const [configuration, policy] = await Promise.all([
32
+ new ModuleConfigurationRepository(this.database).get(input.guildId, LOGGING_MODULE_ID),
33
+ new ModuleLoggingPolicyRepository(this.database).get(input.guildId, input.moduleId, input.eventId),
34
+ ]);
35
+ if (!(policy?.enabled ?? event.defaultEnabled)) {
36
+ return { delivered: false };
37
+ }
38
+ const channelId = configuration?.configuration.channelId;
39
+ if (typeof channelId !== "string" || !/^\d{17,20}$/u.test(channelId)) {
40
+ return { delivered: false };
41
+ }
42
+ const summary = normalizeText(input.summary, 500, "summary");
43
+ const details = (input.details ?? []).map(({ label, value }) => ({
44
+ label: normalizeText(label, 80, "detail label"),
45
+ value: normalizeText(value, 500, "detail value"),
46
+ }));
47
+ if (details.length > 10) {
48
+ throw new Error("Module logging details exceed the supported limit");
49
+ }
50
+ const guild = await this.client.guilds.fetch(input.guildId);
51
+ const channel = await guild.channels.fetch(channelId);
52
+ if (!channel?.isSendable()) {
53
+ throw new Error("Configured logging channel is not sendable");
54
+ }
55
+ const response = toReplyOptions({
56
+ allowedUserMentionIds: [],
57
+ componentsV2: {
58
+ accentColor: VIOLET,
59
+ text: [
60
+ { content: "## Activity log" },
61
+ { content: event.name, markdown: false },
62
+ { content: summary, markdown: false },
63
+ ...details.map(({ label, value }) => ({
64
+ content: `${label}: ${value}`,
65
+ markdown: false,
66
+ })),
67
+ { content: `Module: ${source.manifest.name}`, markdown: false },
68
+ ],
69
+ },
70
+ });
71
+ await channel.send(response);
72
+ return { delivered: true };
73
+ }
74
+ catch (error) {
75
+ this.logger.warn({
76
+ event: "module_logging.delivery_failed",
77
+ moduleId: input.moduleId,
78
+ logEventId: input.eventId,
79
+ guildId: input.guildId,
80
+ error: safeError(error),
81
+ }, "Module activity log delivery failed");
82
+ return { delivered: false };
83
+ }
84
+ }
85
+ }
86
+ function normalizeText(value, maximum, label) {
87
+ const normalized = value.normalize("NFKC").trim();
88
+ if (normalized.length === 0 || normalized.length > maximum) {
89
+ throw new Error(`Module logging ${label} is invalid`);
90
+ }
91
+ return normalized;
92
+ }
93
+ function safeError(error) {
94
+ const value = error instanceof Error ? error : new Error(String(error));
95
+ return { name: value.name, message: value.message };
96
+ }
97
+ //# sourceMappingURL=module-logging.js.map
@@ -0,0 +1,16 @@
1
+ import type { ModuleDefinition } from "@helyx/sdk";
2
+ type ModuleImporter = (specifier: string) => Promise<unknown>;
3
+ type PackageJsonResolver = (specifier: string) => string;
4
+ type PackageJsonReader = (path: string) => Promise<string>;
5
+ interface LoadedDefinition {
6
+ specifier: string;
7
+ definition: ModuleDefinition;
8
+ }
9
+ export interface ConfiguredModulePackage extends LoadedDefinition {
10
+ packageRoot: string;
11
+ }
12
+ export declare function loadConfiguredModules(specifiers: readonly string[], importer?: ModuleImporter): Promise<ModuleDefinition[]>;
13
+ export declare function loadConfiguredModulePackages(specifiers: readonly string[], importer?: ModuleImporter, resolvePackageJson?: PackageJsonResolver, readPackageJson?: PackageJsonReader): Promise<ConfiguredModulePackage[]>;
14
+ export declare function validateContributionParity(definition: ModuleDefinition): void;
15
+ export {};
16
+ //# sourceMappingURL=modules.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { orderModuleManifests, parseModuleManifest, } from "@helyx/module-manifest";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { dirname } from "node:path";
5
+ const officialModulePackage = /^@helyx\/module-[a-z0-9][a-z0-9-]*$/u;
6
+ function isModuleDefinition(value) {
7
+ if (typeof value !== "object" || value === null)
8
+ return false;
9
+ const candidate = value;
10
+ return (typeof candidate.start === "function" &&
11
+ typeof candidate.stop === "function" &&
12
+ candidate.manifest !== undefined);
13
+ }
14
+ export async function loadConfiguredModules(specifiers, importer = (specifier) => import(specifier)) {
15
+ return (await loadDefinitions(specifiers, importer)).map(({ definition }) => definition);
16
+ }
17
+ export async function loadConfiguredModulePackages(specifiers, importer = (specifier) => import(specifier), resolvePackageJson = (specifier) => createRequire(import.meta.url).resolve(`${specifier}/package.json`), readPackageJson = (path) => readFile(path, "utf8")) {
18
+ const loaded = await loadDefinitions(specifiers, importer);
19
+ return Promise.all(loaded.map(async ({ specifier, definition }) => {
20
+ const packageJsonPath = resolvePackageJson(specifier);
21
+ const packageJson = JSON.parse(await readPackageJson(packageJsonPath));
22
+ if (typeof packageJson !== "object" ||
23
+ packageJson === null ||
24
+ packageJson.name !== specifier ||
25
+ packageJson.version !==
26
+ definition.manifest.version) {
27
+ throw new Error(`Module package metadata mismatch: ${specifier}`);
28
+ }
29
+ return {
30
+ specifier,
31
+ definition,
32
+ packageRoot: dirname(packageJsonPath),
33
+ };
34
+ }));
35
+ }
36
+ async function loadDefinitions(specifiers, importer) {
37
+ const loaded = [];
38
+ for (const specifier of specifiers) {
39
+ if (!officialModulePackage.test(specifier)) {
40
+ throw new Error(`Untrusted or invalid module package specifier: ${specifier}`);
41
+ }
42
+ const namespace = await importer(specifier);
43
+ const definition = typeof namespace === "object" && namespace !== null
44
+ ? namespace.default
45
+ : undefined;
46
+ if (!isModuleDefinition(definition)) {
47
+ throw new Error(`Module package has no valid default export: ${specifier}`);
48
+ }
49
+ definition.manifest = parseModuleManifest(definition.manifest);
50
+ validateContributionParity(definition);
51
+ loaded.push({ specifier, definition });
52
+ }
53
+ const orderedManifests = orderModuleManifests(loaded.map(({ definition }) => definition.manifest));
54
+ const byId = new Map(loaded.map((item) => [item.definition.manifest.id, item]));
55
+ return orderedManifests.map((manifest) => byId.get(manifest.id));
56
+ }
57
+ export function validateContributionParity(definition) {
58
+ const executableCommands = (definition.interactions?.commands ?? []).map(({ name }) => name);
59
+ const executableEvents = [
60
+ ...(definition.events?.messageReactionAdd ?? []),
61
+ ...(definition.events?.guildMemberAdd ?? []),
62
+ ].map(({ type }) => type);
63
+ const executableLogs = (definition.logging?.events ?? []).map(({ id }) => id);
64
+ const executableDashboardActions = (definition.dashboardActions ?? []).map(({ id }) => id);
65
+ assertSameContributionIds(definition.manifest.contributions.commands, executableCommands, "commands");
66
+ assertSameContributionIds(definition.manifest.contributions.events, executableEvents, "events");
67
+ assertSameContributionIds(definition.manifest.contributions.logs ?? [], executableLogs, "logs");
68
+ assertSameContributionIds(definition.manifest.dashboard?.actions?.map(({ id }) => id) ?? [], executableDashboardActions, "dashboard actions");
69
+ }
70
+ function assertSameContributionIds(declared, executable, label) {
71
+ if (new Set(executable).size !== executable.length) {
72
+ throw new Error(`Module has duplicate executable ${label}`);
73
+ }
74
+ const declaredSorted = [...declared].sort();
75
+ const executableSorted = [...executable].sort();
76
+ if (declaredSorted.length !== executableSorted.length ||
77
+ declaredSorted.some((value, index) => value !== executableSorted[index])) {
78
+ throw new Error(`Module manifest ${label} do not match executable contributions`);
79
+ }
80
+ }
81
+ //# sourceMappingURL=modules.js.map
@@ -0,0 +1,3 @@
1
+ import { type ModuleDefinition } from "@helyx/sdk";
2
+ export declare function createPermissionsModule(configurableModules: readonly ModuleDefinition[]): ModuleDefinition;
3
+ //# sourceMappingURL=permissions-module.d.ts.map