{"record":{"id":"83010dc54d89fc79","repo":"koala73/worldmonitor","slug":"discord-channel-must-be-set-via-set-discord-oauth","errorCode":null,"errorMessage":"discord channel must be set via set-discord-oauth","messagePattern":"discord channel must be set via set-discord-oauth","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/notificationChannels.ts","lineNumber":558,"sourceCode":"      }\n    } else if (args.channelType === \"email\") {\n      const email = requireVerifiedAccountEmail(args.email, identity.emailVerified === true ? identity.email : undefined);\n      const doc = { userId, channelType: \"email\" as const, email, emailOwnership: \"verified_account\" as const, verified: true, linkedAt: now };\n      if (existing) {\n        await ctx.db.replace(existing._id, doc);\n      } else {\n        await ctx.db.insert(\"notificationChannels\", doc);\n      }\n    } else if (args.channelType === \"webhook\") {\n      if (!args.webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for webhook channel\");\n      const doc = { userId, channelType: \"webhook\" as const, webhookEnvelope: args.webhookEnvelope, verified: true, linkedAt: now, webhookLabel: args.webhookLabel };\n      if (existing) {\n        await ctx.db.replace(existing._id, doc);\n      } else {\n        await ctx.db.insert(\"notificationChannels\", doc);\n      }\n    } else {\n      throw new ConvexError(\"discord channel must be set via set-discord-oauth\");\n    }\n  },\n});\n\n// Called by the notification relay via /relay/deactivate HTTP action\n// when Telegram returns 403 or Slack returns 404/410. Ungated by design —\n// delivery-side deactivation must work after Pro lapses (see #8430).\nexport const deactivateChannelForUser = internalMutation({\n  args: { userId: v.string(), channelType: channelTypeValidator },\n  handler: async (ctx, args) => {\n    await deactivateChannelRow(ctx, args.userId, args.channelType);\n  },\n});\n\n// INTERNAL ONLY — #8430. Formerly public `mutation`s with a Pro gate that\n// contradicted the ungated relay path. Kept as identity-scoped internals so\n// authenticated deploy-key callers and Sentry probe filters still resolve the\n// historical names; production traffic uses `*ForUser` above.","sourceCodeStart":540,"sourceCodeEnd":576,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/convex/notificationChannels.ts#L540-L576","documentation":"Routing guard in setChannel: channelType is 'discord', which deliberately has no direct payload path — Discord channels must be created through the separate set-discord-oauth flow because they require an OAuth grant (bot/guild binding), not pasted credentials.","triggerScenarios":"Thrown at convex/notificationChannels.ts:525 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Use the set-discord-oauth mutation or flow to link a Discord channel","Hide Discord from the generic setChannel UI and route users to the OAuth flow directly"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}