{"record":{"id":"8e0c6e52f0a17d92","repo":"koala73/worldmonitor","slug":"webhookenvelope-required-for-slack-channel","errorCode":"webhookEnvelope required for slack channel","errorMessage":"webhookEnvelope required for slack channel","messagePattern":"webhookEnvelope required for slack channel","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/notificationChannels.ts","lineNumber":228,"sourceCode":"    scheduleWelcome: v.optional(v.boolean()),\n    // Internal-only: derived by the relay HTTP handler from Clerk, never the body.\n    verifiedAccountEmail: v.optional(v.string()),\n  },\n  handler: async (ctx, args) => {\n    const { userId, channelType, webhookEnvelope, email, webhookLabel } = args;\n    const existing = await ctx.db\n      .query(\"notificationChannels\")\n      .withIndex(\"by_user_channel\", (q) =>\n        q.eq(\"userId\", userId).eq(\"channelType\", channelType),\n      )\n      .unique();\n    const isNew = !existing;\n    let channelId = existing ? String(existing._id) : \"\";\n    const now = Date.now();\n    if (channelType === \"telegram\") {\n      throw new ConvexError(\"telegram channel must be linked through bot pairing\");\n    } else if (channelType === \"slack\") {\n      if (!webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for slack channel\");\n      const doc = { userId, channelType: \"slack\" as const, webhookEnvelope, verified: true, linkedAt: now };\n      if (existing) { await ctx.db.replace(existing._id, doc); } else { channelId = String(await ctx.db.insert(\"notificationChannels\", doc)); }\n    } else if (channelType === \"email\") {\n      await assertProEntitlement(ctx, userId);\n      const recipient = requireVerifiedAccountEmail(email, args.verifiedAccountEmail);\n      const doc = { userId, channelType: \"email\" as const, email: recipient, emailOwnership: \"verified_account\" as const, verified: true, linkedAt: now };\n      if (existing) { await ctx.db.replace(existing._id, doc); } else { channelId = String(await ctx.db.insert(\"notificationChannels\", doc)); }\n    } else if (channelType === \"webhook\") {\n      if (!webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for webhook channel\");\n      const doc = { userId, channelType: \"webhook\" as const, webhookEnvelope, verified: true, linkedAt: now, webhookLabel };\n      if (existing) { await ctx.db.replace(existing._id, doc); } else { channelId = String(await ctx.db.insert(\"notificationChannels\", doc)); }\n    } else {\n      throw new ConvexError(\"discord channel must be set via set-discord-oauth\");\n    }\n    if (isNew && args.scheduleWelcome === true) {\n      await ctx.scheduler.runAfter(\n        0,\n        (internal as any).notificationChannels.queueChannelWelcome,","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/convex/notificationChannels.ts#L210-L246","documentation":"Channel-shape validation in setChannel: channelType is 'slack' but args.webhookEnvelope is missing. Slack channels persist the webhook envelope as their payload, so it is required to construct the channel document.","triggerScenarios":"Thrown at convex/notificationChannels.ts:501 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Complete the Slack webhook capture step before saving the channel","Require the webhookEnvelope field in the UI when slack is the selected channel type"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}