diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 757ca0979bd..1f5df23037e 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -44,6 +44,11 @@ "nativeSkillsAutoEnabled": true }, "configuredState": { + "env": { + "allOf": [ + "DISCORD_BOT_TOKEN" + ] + }, "specifier": "./configured-state", "exportName": "hasDiscordConfiguredState" } diff --git a/extensions/irc/package.json b/extensions/irc/package.json index 1247e15c755..bebe132ed68 100644 --- a/extensions/irc/package.json +++ b/extensions/irc/package.json @@ -27,6 +27,12 @@ ], "systemImage": "network", "configuredState": { + "env": { + "allOf": [ + "IRC_HOST", + "IRC_NICK" + ] + }, "specifier": "./configured-state", "exportName": "hasIrcConfiguredState" } diff --git a/extensions/slack/package.json b/extensions/slack/package.json index f7760ea4346..481e1941911 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -33,6 +33,13 @@ "nativeSkillsAutoEnabled": false }, "configuredState": { + "env": { + "anyOf": [ + "SLACK_APP_TOKEN", + "SLACK_BOT_TOKEN", + "SLACK_USER_TOKEN" + ] + }, "specifier": "./configured-state", "exportName": "hasSlackConfiguredState" } diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 9bde19a94aa..f2fbb2d0cb1 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -43,6 +43,11 @@ "nativeSkillsAutoEnabled": true }, "configuredState": { + "env": { + "allOf": [ + "TELEGRAM_BOT_TOKEN" + ] + }, "specifier": "./configured-state", "exportName": "hasTelegramConfiguredState" } diff --git a/src/channels/plugins/configured-state.test.ts b/src/channels/plugins/configured-state.test.ts index 0ba9d3957b2..c8856f8682b 100644 --- a/src/channels/plugins/configured-state.test.ts +++ b/src/channels/plugins/configured-state.test.ts @@ -1,9 +1,12 @@ +import { createRequire } from "node:module"; import { describe, expect, it } from "vitest"; import { hasBundledChannelConfiguredState, listBundledChannelIdsWithConfiguredState, } from "./configured-state.js"; +const nodeRequire = createRequire(import.meta.url); + describe("bundled channel configured-state metadata", () => { it("lists the shipped metadata-first configured-state channels", () => { expect(listBundledChannelIdsWithConfiguredState()).toEqual( @@ -41,4 +44,22 @@ describe("bundled channel configured-state metadata", () => { }), ).toBe(true); }); + + it("uses declarative env metadata without a TypeScript source require hook", () => { + const previousTsHook = nodeRequire.extensions[".ts"]; + delete nodeRequire.extensions[".ts"]; + try { + expect( + hasBundledChannelConfiguredState({ + channelId: "discord", + cfg: {}, + env: { DISCORD_BOT_TOKEN: "token" }, + }), + ).toBe(true); + } finally { + if (previousTsHook) { + nodeRequire.extensions[".ts"] = previousTsHook; + } + } + }); }); diff --git a/src/channels/plugins/package-state-probes.ts b/src/channels/plugins/package-state-probes.ts index 5ebdb9dc786..70a9a2059a9 100644 --- a/src/channels/plugins/package-state-probes.ts +++ b/src/channels/plugins/package-state-probes.ts @@ -16,12 +16,29 @@ type ChannelPackageStateChecker = (params: { type ChannelPackageStateMetadata = { specifier?: string; exportName?: string; + env?: { + allOf?: readonly string[]; + anyOf?: readonly string[]; + }; }; export type ChannelPackageStateMetadataKey = "configuredState" | "persistedAuthState"; const log = createSubsystemLogger("channels"); +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)); +} + +function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv | undefined, key: string): boolean { + return typeof env?.[key] === "string" && env[key].trim().length > 0; +} + function resolveChannelPackageStateMetadata( entry: PluginChannelCatalogEntry, metadataKey: ChannelPackageStateMetadataKey, @@ -32,10 +49,17 @@ function resolveChannelPackageStateMetadata( } const specifier = normalizeOptionalString(metadata.specifier) ?? ""; const exportName = normalizeOptionalString(metadata.exportName) ?? ""; - if (!specifier || !exportName) { + const allOf = normalizeStringList(metadata.env?.allOf); + const anyOf = normalizeStringList(metadata.env?.anyOf); + const env = allOf.length > 0 || anyOf.length > 0 ? { allOf, anyOf } : undefined; + if ((!specifier || !exportName) && !env) { return null; } - return { specifier, exportName }; + return { + ...(specifier ? { specifier } : {}), + ...(exportName ? { exportName } : {}), + ...(env ? { env } : {}), + }; } function listChannelPackageStateCatalog( @@ -55,6 +79,17 @@ function resolveChannelPackageStateChecker(params: { return null; } + if (metadata.env) { + return ({ env }) => { + const allOf = metadata.env?.allOf ?? []; + const anyOf = metadata.env?.anyOf ?? []; + return ( + (allOf.length === 0 || allOf.every((key) => hasNonEmptyEnvValue(env, key))) && + (anyOf.length === 0 || anyOf.some((key) => hasNonEmptyEnvValue(env, key))) + ); + }; + } + try { const moduleExport = loadChannelPluginModule({ modulePath: resolveExistingPluginModulePath(params.entry.rootDir, metadata.specifier!), diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 0bf9f9685bf..45fbfc698a8 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -325,6 +325,9 @@ describe("bundled plugin metadata", () => { { dir: "discord", configuredState: { + env: { + allOf: ["DISCORD_BOT_TOKEN"], + }, specifier: "./configured-state", exportName: "hasDiscordConfiguredState", }, @@ -332,6 +335,9 @@ describe("bundled plugin metadata", () => { { dir: "irc", configuredState: { + env: { + allOf: ["IRC_HOST", "IRC_NICK"], + }, specifier: "./configured-state", exportName: "hasIrcConfiguredState", }, @@ -339,6 +345,9 @@ describe("bundled plugin metadata", () => { { dir: "slack", configuredState: { + env: { + anyOf: ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"], + }, specifier: "./configured-state", exportName: "hasSlackConfiguredState", }, @@ -346,6 +355,9 @@ describe("bundled plugin metadata", () => { { dir: "telegram", configuredState: { + env: { + allOf: ["TELEGRAM_BOT_TOKEN"], + }, specifier: "./configured-state", exportName: "hasTelegramConfiguredState", }, diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 9c8df9a4dc6..956906ea87f 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -1481,6 +1481,10 @@ export type PluginPackageChannel = { configuredState?: { specifier?: string; exportName?: string; + env?: { + allOf?: readonly string[]; + anyOf?: readonly string[]; + }; }; persistedAuthState?: { specifier?: string;