@junando/webhook 0.15.1 → 0.16.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.
Files changed (2) hide show
  1. package/dist/handler.cjs +66 -9
  2. package/package.json +2 -2
package/dist/handler.cjs CHANGED
@@ -19666,6 +19666,12 @@ let RuleEvaluationPhase = /* @__PURE__ */ function(RuleEvaluationPhase) {
19666
19666
  RuleEvaluationPhase["PostLlm"] = "post-llm";
19667
19667
  return RuleEvaluationPhase;
19668
19668
  }({});
19669
+ /** Notifier backend a logical channel resolves to */
19670
+ let ChannelType = /* @__PURE__ */ function(ChannelType) {
19671
+ ChannelType["Slack"] = "slack";
19672
+ ChannelType["Teams"] = "teams";
19673
+ return ChannelType;
19674
+ }({});
19669
19675
  const AlertCountSchema = object({
19670
19676
  min: number$1().optional(),
19671
19677
  max: number$1().optional()
@@ -19715,9 +19721,19 @@ const RuleSchema = object({
19715
19721
  requiresRollback: boolean$1().optional()
19716
19722
  });
19717
19723
  const RuleSectionSchema = object({ rules: array(RuleSchema).default([]) });
19724
+ const SLACK_CHANNEL_SCHEMA = object({
19725
+ type: literal("slack"),
19726
+ channel: string$1().startsWith("#")
19727
+ });
19728
+ const TEAMS_CHANNEL_SCHEMA = object({
19729
+ type: literal("teams"),
19730
+ webhookUrlEnv: string$1().min(1)
19731
+ });
19732
+ const ChannelConfigSchema = discriminatedUnion("type", [SLACK_CHANNEL_SCHEMA, TEAMS_CHANNEL_SCHEMA]);
19718
19733
  const RuleConfigurationSchema = object({
19719
19734
  ["pre-llm"]: RuleSectionSchema,
19720
- ["post-llm"]: RuleSectionSchema
19735
+ ["post-llm"]: RuleSectionSchema,
19736
+ channels: record(string$1(), ChannelConfigSchema).default({})
19721
19737
  });
19722
19738
 
19723
19739
  //#endregion
@@ -73526,15 +73542,52 @@ function buildNotifierRegistry(config) {
73526
73542
  return registry;
73527
73543
  }
73528
73544
  /**
73545
+ * Builds the notifier for a single named channel entry from the rules YAML's
73546
+ * `channels:` section.
73547
+ *
73548
+ * @throws {Error} if the channel's backend cannot actually be reached (e.g.
73549
+ * `type: slack` with no SLACK_BOT_TOKEN configured, or `type: teams` whose
73550
+ * `webhookUrlEnv` is unset) — fails fast at startup rather than at delivery
73551
+ * time, mid-incident.
73552
+ */
73553
+ function buildChannelNotifier(name, channelConfig, config) {
73554
+ if (channelConfig.type === "slack") {
73555
+ if (!config.slackBotToken) {
73556
+ throw new Error(`Channel "${name}" (type: slack) requires SLACK_BOT_TOKEN to be set`);
73557
+ }
73558
+ return new SlackNotifier(config.slackBotToken, channelConfig.channel);
73559
+ }
73560
+ const webhookUrl = process.env[channelConfig.webhookUrlEnv];
73561
+ if (!webhookUrl) {
73562
+ throw new Error(`Channel "${name}" (type: teams) references env var "${channelConfig.webhookUrlEnv}", which is not set`);
73563
+ }
73564
+ let parsedUrl;
73565
+ try {
73566
+ parsedUrl = new URL(webhookUrl);
73567
+ } catch {
73568
+ throw new Error(`Channel "${name}": ${channelConfig.webhookUrlEnv} is not a valid URL`);
73569
+ }
73570
+ if (!parsedUrl.searchParams.has("api-version")) {
73571
+ throw new Error(`Channel "${name}": ${channelConfig.webhookUrlEnv} must include api-version= as a query parameter`);
73572
+ }
73573
+ return new TeamsNotifier(webhookUrl);
73574
+ }
73575
+ /**
73529
73576
  * Creates the notifier for the application.
73530
73577
  *
73531
73578
  * When `config.rulesConfigPath` is set:
73532
73579
  * - Reads and validates the rules YAML config
73533
- * - Creates a ChannelRegistry with the default notifier as fallback
73580
+ * - Builds a ChannelRegistry from its `channels:` section, with the default
73581
+ * notifier as fallback
73534
73582
  * - Wraps the default notifier with a RoutingNotifier for multi-channel dispatch
73535
73583
  *
73536
73584
  * When `config.rulesConfigPath` is NOT set:
73537
73585
  * - Returns the default notifier directly (backward-compatible)
73586
+ *
73587
+ * @throws {Error} at startup if a rule's route/escalate action references a
73588
+ * channel with no matching entry under `channels:` — an operator must fix
73589
+ * the rules config rather than have the alert silently fall back to the
73590
+ * default channel mid-incident.
73538
73591
  */
73539
73592
  function createNotifier(config) {
73540
73593
  const registry = buildNotifierRegistry(config);
@@ -73543,21 +73596,24 @@ function createNotifier(config) {
73543
73596
  logger$3.debug("RULES_CONFIG_PATH not set — rule engine disabled, using default notifier");
73544
73597
  return defaultNotifier;
73545
73598
  }
73599
+ const yamlContent = (0, node_fs.readFileSync)(config.rulesConfigPath, "utf-8");
73600
+ const ruleConfig = parseRuleConfig(yamlContent);
73546
73601
  const channelRegistry = new ChannelRegistry();
73547
73602
  channelRegistry.setDefault(defaultNotifier);
73603
+ for (const [name, channelConfig] of Object.entries(ruleConfig.channels)) {
73604
+ channelRegistry.register(name, buildChannelNotifier(name, channelConfig, config));
73605
+ }
73548
73606
  const unresolved = collectUnresolvedChannels(config, channelRegistry);
73549
73607
  if (unresolved.length > 0) {
73550
- logger$3.warn({
73551
- channels: unresolved,
73552
- rulesConfigPath: config.rulesConfigPath
73553
- }, "Rules reference channels with no registered notifier — these will be delivered to the default channel");
73608
+ throw new Error(`Rules reference undefined channels: ${unresolved.join(", ")}. Define them under ` + `"channels:" in ${config.rulesConfigPath}, or remove the reference.`);
73554
73609
  }
73555
73610
  return new RoutingNotifier(channelRegistry, defaultNotifier);
73556
73611
  }
73557
73612
  /**
73558
73613
  * Collect the channel names referenced by Route/Escalate actions in the rules
73559
- * config that have no notifier registered, and would therefore fall back to the
73560
- * default channel.
73614
+ * config that have no notifier registered against `registry` i.e. no
73615
+ * matching entry under the rules YAML's `channels:` section. `createNotifier`
73616
+ * treats a non-empty result as a fail-fast startup error.
73561
73617
  *
73562
73618
  * Returns an empty list when no rules config is set.
73563
73619
  */
@@ -73568,7 +73624,7 @@ function collectUnresolvedChannels(config, registry) {
73568
73624
  const yamlContent = (0, node_fs.readFileSync)(config.rulesConfigPath, "utf-8");
73569
73625
  const ruleConfig = parseRuleConfig(yamlContent);
73570
73626
  const referenced = new Set();
73571
- for (const section of Object.values(ruleConfig)) {
73627
+ for (const section of [ruleConfig["pre-llm"], ruleConfig["post-llm"]]) {
73572
73628
  for (const rule of section.rules) {
73573
73629
  for (const action of rule.actions) {
73574
73630
  if ("channel" in action) {
@@ -95154,6 +95210,7 @@ var src_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
95154
95210
  AlertType: () => AlertType,
95155
95211
  AlertmanagerPayloadSchema: () => AlertmanagerPayloadSchema,
95156
95212
  CIRCUIT_BREAKER: () => CIRCUIT_BREAKER,
95213
+ ChannelConfigSchema: () => ChannelConfigSchema,
95157
95214
  DEDUP_TTL_MS_MULTIPLIER: () => DEDUP_TTL_MS_MULTIPLIER,
95158
95215
  DEV_SERVER_PORT: () => DEV_SERVER_PORT,
95159
95216
  HOUR_MS: () => HOUR_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junando/webhook",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "AWS Lambda webhook handler — receives and enqueues external alert payloads for Junando",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@aws-sdk/client-sqs": "^3.1127.0",
22
22
  "zod": "^4.5.4",
23
- "@junando/core": "0.15.1"
23
+ "@junando/core": "0.16.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/aws-lambda": "^8.10.163",