hood-alerts 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.
Files changed (107) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +369 -0
  3. package/dist/bot/index.cjs +1008 -0
  4. package/dist/bot/index.cjs.map +1 -0
  5. package/dist/bot/index.d.cts +241 -0
  6. package/dist/bot/index.d.ts +241 -0
  7. package/dist/bot/index.js +30 -0
  8. package/dist/bot/index.js.map +1 -0
  9. package/dist/chunk-32GS6XVE.js +732 -0
  10. package/dist/chunk-32GS6XVE.js.map +1 -0
  11. package/dist/chunk-3LHCQA4Z.js +76 -0
  12. package/dist/chunk-3LHCQA4Z.js.map +1 -0
  13. package/dist/chunk-3NHLSKZE.js +1 -0
  14. package/dist/chunk-3NHLSKZE.js.map +1 -0
  15. package/dist/chunk-6JQK5G3Z.js +72 -0
  16. package/dist/chunk-6JQK5G3Z.js.map +1 -0
  17. package/dist/chunk-CBNZ6AZT.js +598 -0
  18. package/dist/chunk-CBNZ6AZT.js.map +1 -0
  19. package/dist/chunk-CIURPPOW.js +1 -0
  20. package/dist/chunk-CIURPPOW.js.map +1 -0
  21. package/dist/chunk-IUOEGBF6.js +49 -0
  22. package/dist/chunk-IUOEGBF6.js.map +1 -0
  23. package/dist/chunk-NBMINJ5E.js +230 -0
  24. package/dist/chunk-NBMINJ5E.js.map +1 -0
  25. package/dist/chunk-O2RY2SWN.js +1 -0
  26. package/dist/chunk-O2RY2SWN.js.map +1 -0
  27. package/dist/chunk-PLMVKTQM.js +1 -0
  28. package/dist/chunk-PLMVKTQM.js.map +1 -0
  29. package/dist/chunk-RSTLW7XH.js +130 -0
  30. package/dist/chunk-RSTLW7XH.js.map +1 -0
  31. package/dist/chunk-SOBZ2FPP.js +355 -0
  32. package/dist/chunk-SOBZ2FPP.js.map +1 -0
  33. package/dist/chunk-VC5MKEY2.js +170 -0
  34. package/dist/chunk-VC5MKEY2.js.map +1 -0
  35. package/dist/chunk-W2GUAWUR.js +147 -0
  36. package/dist/chunk-W2GUAWUR.js.map +1 -0
  37. package/dist/chunk-Y2M3USWZ.js +355 -0
  38. package/dist/chunk-Y2M3USWZ.js.map +1 -0
  39. package/dist/chunk-Z4DJPESQ.js +1216 -0
  40. package/dist/chunk-Z4DJPESQ.js.map +1 -0
  41. package/dist/chunk-ZDM5VNWB.js +1 -0
  42. package/dist/chunk-ZDM5VNWB.js.map +1 -0
  43. package/dist/events/index.cjs +820 -0
  44. package/dist/events/index.cjs.map +1 -0
  45. package/dist/events/index.d.cts +58 -0
  46. package/dist/events/index.d.ts +58 -0
  47. package/dist/events/index.js +69 -0
  48. package/dist/events/index.js.map +1 -0
  49. package/dist/format-6RG4OcyE.d.ts +57 -0
  50. package/dist/format-B7d40f65.d.cts +57 -0
  51. package/dist/index.cjs +4122 -0
  52. package/dist/index.cjs.map +1 -0
  53. package/dist/index.d.cts +21 -0
  54. package/dist/index.d.ts +21 -0
  55. package/dist/index.js +274 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/logger-3phvV_fl.d.cts +185 -0
  58. package/dist/logger-CYza_0eU.d.ts +185 -0
  59. package/dist/logs-D7sYBvSy.d.cts +66 -0
  60. package/dist/logs-D7sYBvSy.d.ts +66 -0
  61. package/dist/notifiers/index.cjs +620 -0
  62. package/dist/notifiers/index.cjs.map +1 -0
  63. package/dist/notifiers/index.d.cts +186 -0
  64. package/dist/notifiers/index.d.ts +186 -0
  65. package/dist/notifiers/index.js +70 -0
  66. package/dist/notifiers/index.js.map +1 -0
  67. package/dist/pricing-BumgLpvB.d.cts +79 -0
  68. package/dist/pricing-BumgLpvB.d.ts +79 -0
  69. package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
  70. package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
  71. package/dist/registry-D3We8Lg6.d.ts +50 -0
  72. package/dist/registry-DePA3IyS.d.cts +50 -0
  73. package/dist/rules/index.cjs +614 -0
  74. package/dist/rules/index.cjs.map +1 -0
  75. package/dist/rules/index.d.cts +166 -0
  76. package/dist/rules/index.d.ts +166 -0
  77. package/dist/rules/index.js +60 -0
  78. package/dist/rules/index.js.map +1 -0
  79. package/dist/schema-DuDMPqpq.d.ts +183 -0
  80. package/dist/schema-NnIdKFl2.d.cts +183 -0
  81. package/dist/service/index.cjs +3774 -0
  82. package/dist/service/index.cjs.map +1 -0
  83. package/dist/service/index.d.cts +262 -0
  84. package/dist/service/index.d.ts +262 -0
  85. package/dist/service/index.js +45 -0
  86. package/dist/service/index.js.map +1 -0
  87. package/dist/service/main.cjs +3759 -0
  88. package/dist/service/main.cjs.map +1 -0
  89. package/dist/service/main.d.cts +1 -0
  90. package/dist/service/main.d.ts +1 -0
  91. package/dist/service/main.js +60 -0
  92. package/dist/service/main.js.map +1 -0
  93. package/dist/sources-CAaQR6-N.d.ts +159 -0
  94. package/dist/sources-CpDOUlzx.d.cts +159 -0
  95. package/dist/telegram-CpodYJrP.d.cts +64 -0
  96. package/dist/telegram-pvphmBwn.d.ts +64 -0
  97. package/dist/tiers/index.cjs +471 -0
  98. package/dist/tiers/index.cjs.map +1 -0
  99. package/dist/tiers/index.d.cts +231 -0
  100. package/dist/tiers/index.d.ts +231 -0
  101. package/dist/tiers/index.js +34 -0
  102. package/dist/tiers/index.js.map +1 -0
  103. package/dist/types-C0ETog04.d.ts +109 -0
  104. package/dist/types-CcoKpStA.d.cts +109 -0
  105. package/dist/types-DFPpTMWo.d.cts +186 -0
  106. package/dist/types-DFPpTMWo.d.ts +186 -0
  107. package/package.json +114 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bot/index.ts","../../src/bot/commands.ts","../../src/notifiers/escape.ts","../../src/rules/schema.ts","../../src/events/types.ts","../../src/tiers/entitlements.ts","../../src/notifiers/http.ts","../../src/notifiers/telegram.ts","../../src/service/logger.ts","../../src/bot/telegram-bot.ts","../../src/bot/discord-bot.ts"],"sourcesContent":["/**\n * The bot: one command router, two front ends (Telegram long polling and\n * Discord slash commands over the HTTP interactions endpoint).\n *\n * @packageDocumentation\n */\n\nexport { COMMANDS, createCommandRouter } from './commands.js'\nexport type {\n CommandContext,\n CommandReply,\n CommandRouter,\n CommandRouterDeps,\n CommandSpec,\n} from './commands.js'\n\nexport { createTelegramBot } from './telegram-bot.js'\nexport type { TelegramBot, TelegramBotOptions } from './telegram-bot.js'\n\nexport {\n CALLBACK_TYPE,\n EPHEMERAL_FLAG,\n INTERACTION_TYPE,\n createDiscordInteractionHandler,\n discordCommandDefinitions,\n registerDiscordCommands,\n verifyDiscordSignature,\n} from './discord-bot.js'\nexport type {\n DiscordCommandDefinition,\n DiscordInteractionOptions,\n InteractionResponse,\n RegisterCommandsOptions,\n} from './discord-bot.js'\n","import { randomUUID } from 'node:crypto'\nimport { getAddress } from 'viem'\nimport { escapeHtml } from '../notifiers/escape.js'\nimport {\n defaultRules,\n formatValidationError,\n parseSubscription,\n ruleComplexity,\n safeParseRule,\n type Platform,\n type Rule,\n type Subscription,\n} from '../rules/schema.js'\nimport { linkMessage, verifyWalletLink } from '../tiers/entitlements.js'\nimport type { EntitlementGate } from '../tiers/enforce.js'\nimport type { AlertStore } from '../service/store.js'\n\n/**\n * The bot's command surface, written once and shared by Telegram and Discord.\n *\n * Both platforms deliver the same thing to this router: a command name, a\n * subscriber id, a default delivery target and a list of arguments. Keeping\n * the logic here means the two bots cannot drift apart in what `/rule add`\n * accepts or what `/tier` reports, and it means the command tests exercise the\n * real code path rather than a Telegram-shaped copy of it.\n *\n * Every command answers bad input with a specific, actionable message. There\n * is no silent failure and no raw validation dump: a mistyped threshold gets\n * \"usd must be a positive number, for example /threshold whales 5000\".\n */\n\n/** What a platform hands the router. */\nexport interface CommandContext {\n /** Stable identity for entitlements: `telegram:<user id>`, `discord:<guild or user id>`. */\n subscriberId: string\n platform: Platform\n /**\n * Where alerts would go if the user does not name a target: the Telegram\n * chat id, or the Discord channel id the command was used in.\n */\n defaultTarget: string\n /** Arguments after the command word. */\n args: string[]\n /** The whole argument string, for commands that take free-form JSON. */\n rest: string\n}\n\n/** A reply to render back to the user. */\nexport interface CommandReply {\n /** Telegram HTML, which Discord renders acceptably after tag stripping. */\n html: string\n /** Plain-text equivalent, used by Discord and by logs. */\n text: string\n}\n\n/** A command the bot exposes. */\nexport interface CommandSpec {\n name: string\n description: string\n /** Usage line shown by `/help` and in the README. */\n usage: string\n /** Discord slash-command options, when the command takes arguments. */\n options?: { name: string; description: string; required: boolean }[]\n}\n\n/** Everything the router needs. */\nexport interface CommandRouterDeps {\n store: AlertStore\n gate: EntitlementGate\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n /** Id factory for new subscriptions and nonces. @defaultValue `randomUUID` */\n newId?: () => string\n /** Shown by `/upgrade`: how to pay. Set from the service config. */\n upgradeInstructions?: string\n}\n\n/** The router. */\nexport interface CommandRouter {\n /** Every command, for `/help` and for Discord registration. */\n readonly commands: readonly CommandSpec[]\n /** Handle one command. Never throws: errors come back as a reply. */\n handle(command: string, context: CommandContext): Promise<CommandReply>\n}\n\nconst reply = (text: string): CommandReply => ({ html: escapeHtml(text), text })\n\nconst html = (htmlText: string, plain: string): CommandReply => ({ html: htmlText, text: plain })\n\n/** The command catalogue. */\nexport const COMMANDS: readonly CommandSpec[] = [\n { name: 'start', description: 'Set up alerts in this chat', usage: '/start' },\n { name: 'help', description: 'Show every command', usage: '/help' },\n {\n name: 'subscribe',\n description: 'Send alerts here, with a starter rule set',\n usage: '/subscribe [target]',\n options: [\n {\n name: 'target',\n description: 'Discord webhook URL or channel id. Defaults to this channel.',\n required: false,\n },\n ],\n },\n {\n name: 'unsubscribe',\n description: 'Stop alerts for a subscription',\n usage: '/unsubscribe [subscription id]',\n options: [{ name: 'id', description: 'Subscription id from /status', required: false }],\n },\n { name: 'rules', description: 'List your rules', usage: '/rules' },\n {\n name: 'rule',\n description: 'Add, remove, enable or disable a rule',\n usage: '/rule add <json> | /rule rm <id> | /rule on <id> | /rule off <id>',\n options: [{ name: 'input', description: 'add <json> | rm <id> | on <id> | off <id>', required: true }],\n },\n {\n name: 'threshold',\n description: 'Set a rule minimum USD value',\n usage: '/threshold <rule id> <usd>',\n options: [\n { name: 'rule', description: 'Rule id', required: true },\n { name: 'usd', description: 'Minimum USD value', required: true },\n ],\n },\n {\n name: 'watch',\n description: 'Add a token to a rule watchlist',\n usage: '/watch <token address> [rule id]',\n options: [\n { name: 'token', description: 'Token contract address', required: true },\n { name: 'rule', description: 'Rule id (defaults to the first rule)', required: false },\n ],\n },\n {\n name: 'unwatch',\n description: 'Remove a token from a rule watchlist',\n usage: '/unwatch <token address> [rule id]',\n options: [\n { name: 'token', description: 'Token contract address', required: true },\n { name: 'rule', description: 'Rule id (defaults to the first rule)', required: false },\n ],\n },\n { name: 'tier', description: 'Show your tier and its limits', usage: '/tier' },\n { name: 'status', description: 'Show your subscriptions and delivery state', usage: '/status' },\n {\n name: 'link',\n description: 'Link a paying wallet (signature verified)',\n usage: '/link [address] [signature]',\n options: [\n { name: 'address', description: 'Wallet address that pays', required: false },\n { name: 'signature', description: 'Signature of the link message', required: false },\n ],\n },\n { name: 'upgrade', description: 'How to go premium', usage: '/upgrade' },\n]\n\nfunction ruleSummary(rule: Rule): string {\n const parts = [\n `${rule.enabled ? 'on ' : 'off'} ${rule.id}`,\n rule.name ? `\"${rule.name}\"` : null,\n `kinds=${rule.kinds.join('/')}`,\n `pads=${rule.launchpads.join('/')}`,\n rule.minUsd !== undefined ? `minUsd=${rule.minUsd}` : null,\n rule.maxUsd !== undefined ? `maxUsd=${rule.maxUsd}` : null,\n rule.minLiquidityUsd !== undefined ? `minLiq=${rule.minLiquidityUsd}` : null,\n rule.maxLiquidityUsd !== undefined ? `maxLiq=${rule.maxLiquidityUsd}` : null,\n rule.side !== 'any' ? `side=${rule.side}` : null,\n rule.tokens.length > 0 ? `tokens=${rule.tokens.length}` : null,\n rule.deployers.length > 0 ? `deployers=${rule.deployers.length}` : null,\n rule.rateLimit ? `rate=${JSON.stringify(rule.rateLimit)}` : null,\n `filters=${ruleComplexity(rule)}`,\n ].filter((part): part is string => part !== null)\n return parts.join(' ')\n}\n\n/** Build the command router. */\nexport function createCommandRouter(deps: CommandRouterDeps): CommandRouter {\n const now = deps.now ?? Date.now\n const newId = deps.newId ?? randomUUID\n\n function subscriptionsOf(context: CommandContext): Subscription[] {\n return deps.store.listSubscriptionsFor(context.subscriberId)\n }\n\n function firstSubscription(context: CommandContext): Subscription | null {\n return subscriptionsOf(context)[0] ?? null\n }\n\n async function handleSubscribe(context: CommandContext): Promise<CommandReply> {\n const target = context.args[0]?.trim() || context.defaultTarget\n if (!target) {\n return reply(\n 'I could not work out where to send alerts. Pass a target: /subscribe <discord webhook URL or channel id>',\n )\n }\n const existing = subscriptionsOf(context)\n if (existing.some((sub) => sub.target === target)) {\n return reply(`This target is already subscribed. Use /rules to edit it, or /unsubscribe to stop.`)\n }\n\n const decision = await deps.gate.checkAddSubscription(context.subscriberId, existing.length)\n if (!decision.allowed) return reply(`Cannot subscribe: ${decision.reason}`)\n\n const { policy } = await deps.gate.resolve(context.subscriberId)\n const rules = defaultRules().filter((rule) =>\n rule.kinds.every((kind) => policy.allowedKinds.includes(kind)),\n )\n const subscription = parseSubscription({\n id: newId(),\n subscriberId: context.subscriberId,\n platform: context.platform,\n target,\n enabled: true,\n rules: rules.slice(0, policy.maxRulesPerSubscription),\n createdAtMs: now(),\n })\n deps.store.saveSubscription(subscription)\n\n return html(\n [\n `<b>Subscribed.</b> Alerts will arrive here.`,\n `Subscription id: <code>${escapeHtml(subscription.id)}</code>`,\n `Tier: <b>${policy.tier}</b> (delivery delay ${policy.deliveryDelayMs / 1000}s, ${policy.maxAlertsPerHour} alerts/hour)`,\n '',\n 'Starter rules:',\n ...subscription.rules.map((rule) => `• <code>${escapeHtml(ruleSummary(rule))}</code>`),\n '',\n 'Edit them with /threshold, /watch and /rule. See /help.',\n ].join('\\n'),\n `Subscribed. id=${subscription.id} tier=${policy.tier}`,\n )\n }\n\n async function handleUnsubscribe(context: CommandContext): Promise<CommandReply> {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('You have no subscriptions. Use /subscribe to start one.')\n\n const id = context.args[0]?.trim()\n if (!id) {\n if (subs.length > 1) {\n return html(\n [\n 'You have several subscriptions. Name the one to remove:',\n ...subs.map((sub) => `• <code>${escapeHtml(sub.id)}</code> → ${escapeHtml(sub.target)}`),\n '',\n 'Usage: /unsubscribe &lt;subscription id&gt;',\n ].join('\\n'),\n 'Name the subscription to remove: ' + subs.map((sub) => sub.id).join(', '),\n )\n }\n const only = subs[0] as Subscription\n deps.store.deleteSubscription(only.id)\n return reply('Unsubscribed. No more alerts will be delivered here.')\n }\n\n const match = subs.find((sub) => sub.id === id)\n if (!match) return reply(`No subscription with id \"${id}\". Use /status to list yours.`)\n deps.store.deleteSubscription(match.id)\n return reply(`Unsubscribed ${match.id}.`)\n }\n\n function handleRules(context: CommandContext): CommandReply {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('You have no subscriptions yet. Use /subscribe to start one.')\n const lines: string[] = []\n for (const sub of subs) {\n lines.push(`<b>${escapeHtml(sub.target)}</b> (<code>${escapeHtml(sub.id)}</code>)`)\n if (sub.rules.length === 0) lines.push(' no rules: nothing will be delivered')\n for (const rule of sub.rules) lines.push(` • <code>${escapeHtml(ruleSummary(rule))}</code>`)\n }\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleRule(context: CommandContext): Promise<CommandReply> {\n const action = context.args[0]?.toLowerCase()\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe, then add rules.')\n\n if (action === 'add') {\n const json = context.rest.slice(context.rest.indexOf('add') + 3).trim()\n if (!json) {\n return html(\n [\n 'Usage: <code>/rule add {\"id\":\"big-whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":25000}</code>',\n '',\n 'Fields: id, name, enabled, kinds, launchpads, minUsd, maxUsd, minLiquidityUsd,',\n 'maxLiquidityUsd, tokens, deployers, excludeDeployers, side, reputation, rateLimit.',\n ].join('\\n'),\n 'Usage: /rule add {\"id\":\"big-whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":25000}',\n )\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch {\n return reply('That is not valid JSON. Example: /rule add {\"id\":\"whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":5000}')\n }\n const result = safeParseRule(parsed)\n if (!result.success) return reply(`Rule rejected: ${formatValidationError(result.error)}`)\n const rule = result.data\n\n if (subscription.rules.some((existing) => existing.id === rule.id)) {\n return reply(`A rule with id \"${rule.id}\" already exists. Remove it first with /rule rm ${rule.id}.`)\n }\n const decision = await deps.gate.checkAddRule(context.subscriberId, rule, subscription.rules.length)\n if (!decision.allowed) return reply(`Rule rejected: ${decision.reason}`)\n\n subscription.rules.push(rule)\n deps.store.saveSubscription(subscription)\n return html(\n `Rule added: <code>${escapeHtml(ruleSummary(rule))}</code>`,\n `Rule added: ${ruleSummary(rule)}`,\n )\n }\n\n if (action === 'rm' || action === 'remove' || action === 'delete') {\n const id = context.args[1]?.trim()\n if (!id) return reply('Usage: /rule rm <rule id>. List them with /rules.')\n const index = subscription.rules.findIndex((rule) => rule.id === id)\n if (index === -1) return reply(`No rule with id \"${id}\". List them with /rules.`)\n subscription.rules.splice(index, 1)\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${id} removed.`)\n }\n\n if (action === 'on' || action === 'off') {\n const id = context.args[1]?.trim()\n if (!id) return reply(`Usage: /rule ${action} <rule id>. List them with /rules.`)\n const rule = subscription.rules.find((entry) => entry.id === id)\n if (!rule) return reply(`No rule with id \"${id}\". List them with /rules.`)\n rule.enabled = action === 'on'\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${id} is now ${rule.enabled ? 'enabled' : 'disabled'}.`)\n }\n\n return html(\n [\n 'Usage:',\n '• <code>/rule add {json}</code>',\n '• <code>/rule rm &lt;id&gt;</code>',\n '• <code>/rule on &lt;id&gt;</code>',\n '• <code>/rule off &lt;id&gt;</code>',\n ].join('\\n'),\n 'Usage: /rule add {json} | /rule rm <id> | /rule on <id> | /rule off <id>',\n )\n }\n\n async function handleThreshold(context: CommandContext): Promise<CommandReply> {\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe.')\n const [ruleId, rawUsd] = context.args\n if (!ruleId || !rawUsd) {\n return reply('Usage: /threshold <rule id> <usd>. Example: /threshold whales 5000')\n }\n const rule = subscription.rules.find((entry) => entry.id === ruleId)\n if (!rule) return reply(`No rule with id \"${ruleId}\". List them with /rules.`)\n const usd = Number(rawUsd)\n if (!Number.isFinite(usd) || usd < 0) {\n return reply(`\"${rawUsd}\" is not a valid amount. Use a positive number, for example /threshold ${ruleId} 5000`)\n }\n if (rule.maxUsd !== undefined && usd > rule.maxUsd) {\n return reply(`minUsd ${usd} would exceed this rule's maxUsd ${rule.maxUsd}. Lower it or raise maxUsd first.`)\n }\n rule.minUsd = usd\n const decision = await deps.gate.checkRule(context.subscriberId, rule)\n if (!decision.allowed) return reply(`Change rejected: ${decision.reason}`)\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${ruleId} now requires at least $${usd} of value.`)\n }\n\n async function handleWatch(context: CommandContext, add: boolean): Promise<CommandReply> {\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe.')\n const rawToken = context.args[0]?.trim()\n if (!rawToken) {\n return reply(`Usage: /${add ? 'watch' : 'unwatch'} <token address> [rule id]`)\n }\n if (!/^0x[0-9a-fA-F]{40}$/.test(rawToken)) {\n return reply(`\"${rawToken}\" is not a token address. Pass the 0x contract address from the alert.`)\n }\n const token = getAddress(rawToken as `0x${string}`).toLowerCase()\n\n const ruleId = context.args[1]?.trim()\n const rule = ruleId\n ? subscription.rules.find((entry) => entry.id === ruleId)\n : subscription.rules[0]\n if (!rule) {\n return reply(\n ruleId ? `No rule with id \"${ruleId}\". List them with /rules.` : 'This subscription has no rules yet. Add one with /rule add.',\n )\n }\n\n if (add) {\n if (rule.tokens.includes(token)) return reply(`${rawToken} is already on rule ${rule.id}.`)\n rule.tokens.push(token)\n const decision = await deps.gate.checkRule(context.subscriberId, rule)\n if (!decision.allowed) {\n rule.tokens.pop()\n return reply(`Cannot add: ${decision.reason}`)\n }\n deps.store.saveSubscription(subscription)\n return reply(`Watching ${rawToken} on rule ${rule.id} (${rule.tokens.length} tokens).`)\n }\n\n const index = rule.tokens.indexOf(token)\n if (index === -1) return reply(`${rawToken} is not on rule ${rule.id}.`)\n rule.tokens.splice(index, 1)\n deps.store.saveSubscription(subscription)\n return reply(`Stopped watching ${rawToken} on rule ${rule.id}.`)\n }\n\n async function handleTier(context: CommandContext): Promise<CommandReply> {\n const { entitlement, policy } = await deps.gate.resolve(context.subscriberId)\n const expiry =\n entitlement.expiresAtMs === null\n ? 'no expiry'\n : `expires ${new Date(entitlement.expiresAtMs).toISOString()}`\n const lines = [\n `<b>Tier: ${escapeHtml(policy.tier)}</b> (${escapeHtml(expiry)}, source ${escapeHtml(entitlement.source)})`,\n `Subscriptions: up to ${policy.maxSubscriptions}`,\n `Rules per subscription: up to ${policy.maxRulesPerSubscription}`,\n `Filters per rule: up to ${policy.maxRuleComplexity}`,\n `Watchlist size: up to ${policy.maxWatchlistTokens}`,\n `Alerts per hour: up to ${policy.maxAlertsPerHour}`,\n `Delivery delay: ${policy.deliveryDelayMs / 1000}s`,\n `Liquidity and reputation filters: ${policy.allowOnChainLookupFilters ? 'included' : 'premium only'}`,\n `Event kinds: ${policy.allowedKinds.join(', ')}`,\n ]\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleStatus(context: CommandContext): Promise<CommandReply> {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('No subscriptions. Use /subscribe to start one.')\n const delivered = deps.store.countDeliveriesForSubscriber(context.subscriberId, now() - 3_600_000)\n const { policy } = await deps.gate.resolve(context.subscriberId)\n const lines = [\n `<b>${subs.length} subscription${subs.length === 1 ? '' : 's'}</b>, ${delivered} alerts in the last hour (cap ${policy.maxAlertsPerHour})`,\n ...subs.map(\n (sub) =>\n `• <code>${escapeHtml(sub.id)}</code> → ${escapeHtml(sub.target)} (${sub.rules.length} rules, ${sub.enabled ? 'enabled' : 'disabled'})`,\n ),\n ]\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleLink(context: CommandContext): Promise<CommandReply> {\n const [address, signature] = context.args\n if (!address) {\n const nonce = deps.store.issueLinkNonce(context.subscriberId, newId())\n const message = linkMessage(context.subscriberId, nonce)\n return html(\n [\n 'Sign this exact message with the wallet that pays, then send',\n `<code>/link &lt;address&gt; &lt;signature&gt;</code>`,\n '',\n `<pre>${escapeHtml(message)}</pre>`,\n ].join('\\n'),\n `Sign this message and reply with /link <address> <signature>:\\n${message}`,\n )\n }\n if (!signature) {\n return reply('Usage: /link <address> <signature>. Send /link on its own to get the message to sign.')\n }\n const nonce = deps.store.getLinkNonce(context.subscriberId)\n if (!nonce) {\n return reply('No pending link request. Send /link on its own first to get a fresh message to sign.')\n }\n const verified = await verifyWalletLink(context.subscriberId, nonce, address, signature)\n if (!verified) {\n return reply('That signature does not match. Sign the exact message from /link with the wallet you named.')\n }\n deps.store.linkWallet(context.subscriberId, verified)\n deps.store.clearLinkNonce(context.subscriberId)\n return reply(`Wallet ${verified} linked. Payments from it now count towards your subscription.`)\n }\n\n function handleUpgrade(): CommandReply {\n if (!deps.upgradeInstructions) {\n return reply(\n 'This deployment does not sell premium access. The operator grants it through configuration; ask them.',\n )\n }\n return html(escapeHtml(deps.upgradeInstructions), deps.upgradeInstructions)\n }\n\n function handleHelp(): CommandReply {\n const header = 'hood-alerts: Robinhood Chain memecoin alerts.'\n const kinds = 'Event kinds: launch, curve_trade, graduation, whale_trade.'\n const lifecycle =\n 'NOXA lists instantly (no bonding curve), so it never produces curve_trade or graduation. The Odyssey produces all four.'\n return html(\n [\n `<b>${escapeHtml(header)}</b>`,\n '',\n ...COMMANDS.map(\n (command) => `<code>${escapeHtml(command.usage)}</code> - ${escapeHtml(command.description)}`,\n ),\n '',\n escapeHtml(kinds),\n escapeHtml(lifecycle),\n ].join('\\n'),\n [\n header,\n '',\n ...COMMANDS.map((command) => `${command.usage} - ${command.description}`),\n '',\n kinds,\n lifecycle,\n ].join('\\n'),\n )\n }\n\n return {\n commands: COMMANDS,\n async handle(command: string, context: CommandContext): Promise<CommandReply> {\n const name = command.replace(/^\\//, '').split('@')[0]?.toLowerCase() ?? ''\n try {\n switch (name) {\n case 'start':\n case 'help':\n return handleHelp()\n case 'subscribe':\n return await handleSubscribe(context)\n case 'unsubscribe':\n return await handleUnsubscribe(context)\n case 'rules':\n return handleRules(context)\n case 'rule':\n return await handleRule(context)\n case 'threshold':\n return await handleThreshold(context)\n case 'watch':\n return await handleWatch(context, true)\n case 'unwatch':\n return await handleWatch(context, false)\n case 'tier':\n return await handleTier(context)\n case 'status':\n return await handleStatus(context)\n case 'link':\n return await handleLink(context)\n case 'upgrade':\n return handleUpgrade()\n default:\n return reply(`Unknown command \"${name}\". Send /help for the list.`)\n }\n } catch (error) {\n // A handler throwing must still answer the user. Silence looks like a\n // dead bot, which is worse than an error message.\n const message = error instanceof Error ? error.message : String(error)\n return reply(`That command failed: ${message}. Try /help, or report this if it keeps happening.`)\n }\n },\n }\n}\n","/**\n * Escaping. The single most common way an alert bot breaks in production.\n *\n * Memecoin names are adversarial input by nature. A token called `WHO_LET_THE`\n * silently italicises half a Telegram message under MarkdownV2 and a token\n * called `<b>RUG` injects markup under HTML parse mode; either way Telegram\n * answers `400 Bad Request: can't parse entities` and the alert is lost. The\n * rules are not symmetric between platforms or even between contexts within a\n * platform, so each context gets its own function and its own tests.\n */\n\n/**\n * Telegram MarkdownV2 reserves exactly these characters in normal text.\n * Source: Telegram Bot API \"MarkdownV2 style\" section. All 18 of them must be\n * escaped with a preceding backslash anywhere they appear as literal text.\n */\nconst MARKDOWN_V2_RESERVED = '_*[]()~`>#+-=|{}.!'\n\n/** Escape literal text for Telegram MarkdownV2. */\nexport function escapeMarkdownV2(text: string): string {\n let out = ''\n for (const char of text) {\n if (MARKDOWN_V2_RESERVED.includes(char)) out += '\\\\'\n out += char\n }\n return out\n}\n\n/**\n * Escape the URL inside a MarkdownV2 inline link `[label](url)`.\n * Inside the parentheses only `)` and `\\` are special, and escaping the full\n * reserved set here would corrupt query strings and path separators.\n */\nexport function escapeMarkdownV2Url(url: string): string {\n return url.replace(/[\\\\)]/g, (char) => `\\\\${char}`)\n}\n\n/**\n * Escape text inside a MarkdownV2 code span or `pre` block. Only `` ` `` and\n * `\\` are special there. Escaping the full set would print literal backslashes\n * to the user, which is the classic over-correction of this bug.\n */\nexport function escapeMarkdownV2Code(text: string): string {\n return text.replace(/[\\\\`]/g, (char) => `\\\\${char}`)\n}\n\n/** Build a MarkdownV2 inline link with both halves escaped correctly. */\nexport function markdownV2Link(label: string, url: string): string {\n return `[${escapeMarkdownV2(label)}](${escapeMarkdownV2Url(url)})`\n}\n\n/**\n * Escape text for Telegram HTML parse mode. Telegram's HTML is a small subset\n * and requires exactly `&`, `<` and `>` to be replaced in text nodes.\n */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n/** Escape a value going into an HTML attribute (a link `href`). */\nexport function escapeHtmlAttribute(value: string): string {\n return escapeHtml(value).replace(/\"/g, '&quot;')\n}\n\n/** Build a Telegram HTML anchor with both halves escaped. */\nexport function htmlLink(label: string, url: string): string {\n return `<a href=\"${escapeHtmlAttribute(url)}\">${escapeHtml(label)}</a>`\n}\n\n/**\n * Discord markdown special characters. Discord renders markdown inside embed\n * titles, descriptions and field values, so untrusted token names need the\n * same treatment. `#` is not escaped: it is only special at the start of a\n * line for headings, and every place we interpolate a name is mid-line.\n */\nconst DISCORD_RESERVED = /[\\\\*_~`|>[\\]()]/g\n\n/** Escape literal text for Discord markdown rendering. */\nexport function escapeDiscordMarkdown(text: string): string {\n return text.replace(DISCORD_RESERVED, (char) => `\\\\${char}`)\n}\n\n/**\n * Truncate to a maximum length without splitting a surrogate pair, which is\n * what turns a long emoji-bearing token name into a replacement character.\n * The ellipsis is included in the budget.\n */\nexport function truncate(text: string, max: number): string {\n if (max <= 0) return ''\n const chars = [...text]\n if (chars.length <= max) return text\n if (max === 1) return '…'\n return `${chars.slice(0, max - 1).join('')}…`\n}\n\n/**\n * A display label for a token: its symbol when the contract exposed one,\n * otherwise a shortened address. Never returns an empty string, because an\n * empty Telegram entity is a 400.\n */\nexport function displayLabel(symbol: string | null, address: string): string {\n const trimmed = symbol?.trim()\n if (trimmed) return truncate(trimmed, 40)\n return shortAddress(address)\n}\n\n/** `0x1234…abcd`, the standard compact address rendering. */\nexport function shortAddress(address: string): string {\n if (address.length <= 12) return address\n return `${address.slice(0, 6)}…${address.slice(-4)}`\n}\n","import { z } from 'zod'\nimport { LAUNCHPAD_EVENT_KINDS, type AlertEventKind, type Launchpad } from '../events/types.js'\n\n/**\n * Rules are data, not code.\n *\n * Every filter a subscriber can set is expressed as a JSON document validated\n * by these schemas, which means a rule can be stored in SQLite, edited from a\n * Telegram or Discord command, diffed, exported and re-imported without any\n * of it going through `eval`, a DSL parser or a hand-rolled validator. The\n * schema is the single source of truth for what a valid rule is: the bot, the\n * service and the tier policy all validate through it.\n */\n\nconst addressPattern = /^0x[0-9a-fA-F]{40}$/\n\n/** An EVM address, stored lowercase so comparisons never depend on checksum casing. */\nexport const addressSchema = z\n .string()\n .regex(addressPattern, 'must be a 0x-prefixed 20-byte address')\n .transform((value) => value.toLowerCase())\n\n/** The four alert kinds. */\nexport const eventKindSchema = z.enum(['launch', 'curve_trade', 'graduation', 'whale_trade'])\n\n/** The two launchpads. */\nexport const launchpadSchema = z.enum(['noxa', 'odyssey'])\n\n/** Trade direction filter. `any` disables the filter. */\nexport const sideSchema = z.enum(['buy', 'sell', 'any'])\n\n/**\n * Deployer reputation filters, all derived from on-chain history (see\n * `hood-alerts/rules` `createRpcReputationProvider`). Every bound is inclusive.\n */\nexport const reputationFilterSchema = z\n .object({\n /** Require the deployer to have launched at least this many tokens before. */\n minPriorLaunches: z.number().int().min(0).max(10_000).optional(),\n /** Reject serial deployers above this prior-launch count. */\n maxPriorLaunches: z.number().int().min(0).max(10_000).optional(),\n /**\n * Reject a deployer with more than this many prior launches whose pool\n * liquidity has since been drained below the rug threshold.\n */\n maxRuggedLaunches: z.number().int().min(0).max(10_000).optional(),\n /**\n * Require this launch's own LP position to be held by the launchpad's\n * locker contract. Only meaningful for `launch` events that carry a pool\n * (NOXA); events without a pool never satisfy it.\n */\n requireLpLocked: z.boolean().optional(),\n })\n .strict()\n .refine(\n (value) =>\n value.minPriorLaunches === undefined ||\n value.maxPriorLaunches === undefined ||\n value.minPriorLaunches <= value.maxPriorLaunches,\n { message: 'minPriorLaunches must not exceed maxPriorLaunches' },\n )\n\n/** Per-rule delivery rate limits. */\nexport const rateLimitSchema = z\n .object({\n /** Hard cap on alerts this rule may deliver per rolling hour. */\n maxPerHour: z.number().int().min(1).max(10_000).optional(),\n /** Minimum gap between two alerts from this rule, in seconds. */\n minIntervalSeconds: z.number().int().min(1).max(86_400).optional(),\n })\n .strict()\n\n/** A single alert rule. */\nexport const ruleSchema = z\n .object({\n /** Stable id, unique within a subscription. Used in bot commands. */\n id: z\n .string()\n .min(1)\n .max(32)\n .regex(/^[a-z0-9][a-z0-9_-]*$/, 'must be lowercase letters, digits, hyphen or underscore'),\n /** Human label shown in `/rules`. */\n name: z.string().min(1).max(64).optional(),\n enabled: z.boolean().default(true),\n /** Event kinds this rule reacts to. Defaults to all four. */\n kinds: z.array(eventKindSchema).min(1).max(4).default(['launch', 'curve_trade', 'graduation', 'whale_trade']),\n /** Launchpads this rule reacts to. Defaults to both. */\n launchpads: z.array(launchpadSchema).min(1).max(2).default(['noxa', 'odyssey']),\n /** Minimum USD value of the event. Events with an unknown value never pass. */\n minUsd: z.number().min(0).max(1e12).optional(),\n /** Maximum USD value of the event. */\n maxUsd: z.number().min(0).max(1e12).optional(),\n /** Minimum USD liquidity of the token's deepest known pool. */\n minLiquidityUsd: z.number().min(0).max(1e12).optional(),\n /** Maximum USD liquidity of the token's deepest known pool. */\n maxLiquidityUsd: z.number().min(0).max(1e12).optional(),\n /** Watchlist. When non-empty, only these tokens match. */\n tokens: z.array(addressSchema).max(500).default([]),\n /** Deployer watchlist. When non-empty, only these actors match. */\n deployers: z.array(addressSchema).max(500).default([]),\n /** Deployers to always reject, applied after every other filter. */\n excludeDeployers: z.array(addressSchema).max(500).default([]),\n /** Trade direction. Ignored by `launch` and `graduation` events. */\n side: sideSchema.default('any'),\n reputation: reputationFilterSchema.optional(),\n rateLimit: rateLimitSchema.optional(),\n })\n .strict()\n .refine((rule) => rule.minUsd === undefined || rule.maxUsd === undefined || rule.minUsd <= rule.maxUsd, {\n message: 'minUsd must not exceed maxUsd',\n path: ['minUsd'],\n })\n .refine(\n (rule) =>\n rule.minLiquidityUsd === undefined ||\n rule.maxLiquidityUsd === undefined ||\n rule.minLiquidityUsd <= rule.maxLiquidityUsd,\n { message: 'minLiquidityUsd must not exceed maxLiquidityUsd', path: ['minLiquidityUsd'] },\n )\n .refine((rule) => rule.kinds.some((kind) => rule.launchpads.some((pad) => canEmit(pad, kind))), {\n message:\n 'this combination can never fire: NOXA is an instant launcher with no bonding curve, so it emits no curve_trade and no graduation',\n path: ['kinds'],\n })\n\n/** A validated rule. */\nexport type Rule = z.output<typeof ruleSchema>\n/** A rule as accepted on input, before defaults are applied. */\nexport type RuleInput = z.input<typeof ruleSchema>\n/** Validated reputation filter. */\nexport type ReputationFilter = z.output<typeof reputationFilterSchema>\n/** Validated rate limit block. */\nexport type RateLimitConfig = z.output<typeof rateLimitSchema>\n\n/** Delivery platforms a subscription can target. */\nexport const platformSchema = z.enum(['telegram', 'discord'])\n/** A delivery platform. */\nexport type Platform = z.output<typeof platformSchema>\n\n/** A subscription: one delivery target plus the rules that feed it. */\nexport const subscriptionSchema = z\n .object({\n id: z.string().min(1).max(64),\n /**\n * The account that owns this subscription, and the unit entitlements are\n * checked against. For Telegram this is the user id, for Discord the guild\n * id (a server pays once for its channels) or user id for a DM.\n */\n subscriberId: z.string().min(1).max(128),\n platform: platformSchema,\n /**\n * Where alerts go: a Telegram chat id, a Discord channel id, or a Discord\n * webhook URL. Validated per platform by the notifier, not here, because\n * the same subscription record serves all three shapes.\n */\n target: z.string().min(1).max(512),\n enabled: z.boolean().default(true),\n rules: z.array(ruleSchema).max(100).default([]),\n createdAtMs: z.number().int().nonnegative(),\n })\n .strict()\n .refine((sub) => new Set(sub.rules.map((rule) => rule.id)).size === sub.rules.length, {\n message: 'rule ids must be unique within a subscription',\n path: ['rules'],\n })\n\n/** A validated subscription. */\nexport type Subscription = z.output<typeof subscriptionSchema>\n/** A subscription as accepted on input. */\nexport type SubscriptionInput = z.input<typeof subscriptionSchema>\n\n/** Can a launchpad emit this event kind at all? */\nexport function canEmit(launchpad: Launchpad, kind: AlertEventKind): boolean {\n return LAUNCHPAD_EVENT_KINDS[launchpad].includes(kind)\n}\n\n/**\n * How many filters a rule actually uses. The tier policy caps this so free\n * subscribers get real filtering without turning the service into an unpaid\n * on-chain query engine.\n */\nexport function ruleComplexity(rule: Rule): number {\n let score = 0\n if (rule.minUsd !== undefined) score += 1\n if (rule.maxUsd !== undefined) score += 1\n if (rule.minLiquidityUsd !== undefined) score += 1\n if (rule.maxLiquidityUsd !== undefined) score += 1\n if (rule.tokens.length > 0) score += 1\n if (rule.deployers.length > 0) score += 1\n if (rule.excludeDeployers.length > 0) score += 1\n if (rule.side !== 'any') score += 1\n if (rule.reputation) score += Object.values(rule.reputation).filter((v) => v !== undefined).length\n if (rule.rateLimit) score += 1\n return score\n}\n\n/** Filters that cost an extra on-chain read per candidate event. */\nexport function usesOnChainLookups(rule: Rule): boolean {\n return (\n rule.minLiquidityUsd !== undefined ||\n rule.maxLiquidityUsd !== undefined ||\n (rule.reputation !== undefined &&\n Object.values(rule.reputation).some((value) => value !== undefined))\n )\n}\n\n/**\n * Parse and validate a rule, applying defaults.\n *\n * @throws {@link z.ZodError} with a field-level message the bot renders back\n * to the user verbatim.\n *\n * @example\n * ```ts\n * const rule = parseRule({ id: 'whales', kinds: ['whale_trade'], minUsd: 5000 })\n * console.log(rule.launchpads) // ['noxa', 'odyssey'] (defaulted)\n * ```\n */\nexport function parseRule(input: unknown): Rule {\n return ruleSchema.parse(input)\n}\n\n/** Non-throwing {@link parseRule}. */\nexport function safeParseRule(input: unknown): z.ZodSafeParseResult<Rule> {\n return ruleSchema.safeParse(input)\n}\n\n/** Parse and validate a subscription. */\nexport function parseSubscription(input: unknown): Subscription {\n return subscriptionSchema.parse(input)\n}\n\n/** Non-throwing {@link parseSubscription}. */\nexport function safeParseSubscription(input: unknown): z.ZodSafeParseResult<Subscription> {\n return subscriptionSchema.safeParse(input)\n}\n\n/**\n * Render a Zod error as one line of human-readable text, for bot replies.\n * A user who typed `/threshold abc` gets \"minUsd: expected number\", not a\n * stack trace and not a silent failure.\n */\nexport function formatValidationError(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.join('.')\n return path ? `${path}: ${issue.message}` : issue.message\n })\n .join('; ')\n}\n\n/** A ready-to-use starter rule set, used by `/subscribe` before any editing. */\nexport function defaultRules(): Rule[] {\n return [\n parseRule({ id: 'launches', name: 'New launches', kinds: ['launch'] }),\n parseRule({\n id: 'graduations',\n name: 'Odyssey graduations',\n kinds: ['graduation'],\n launchpads: ['odyssey'],\n }),\n parseRule({ id: 'whales', name: 'Whale trades over $5k', kinds: ['whale_trade'], minUsd: 5000 }),\n ]\n}\n","import type { Address, Hash } from 'viem'\n\n/**\n * The normalized alert event taxonomy for Robinhood Chain memecoins.\n *\n * Two launchpads run on mainnet (chain 4663) and they have genuinely\n * different lifecycles. The taxonomy models that difference instead of\n * pretending they are the same product:\n *\n * - **NOXA** (`fun.noxa.fi/robinhood`) is an *instant* launcher. One\n * transaction deploys the ERC-20, creates a Uniswap v3 pool, seeds\n * single-sided liquidity and locks the LP NFT. There is no bonding curve,\n * so NOXA emits `launch` and (through its pool) `whale_trade`, and it can\n * never emit `curve_trade` or `graduation`.\n * - **The Odyssey** (`theodyssey.fun`) is a pump.fun-style native-ETH bonding\n * curve with virtual reserves. It emits `launch` when a curve opens,\n * `curve_trade` for every buy/sell on the curve, and `graduation` when the\n * curve fills and liquidity migrates to a locked Uniswap v3 pool. After\n * graduation its tokens also produce `whale_trade` from the migrated pool.\n *\n * A `graduation` alert therefore covers The Odyssey only. Claiming NOXA\n * \"graduations\" would be describing an event that does not exist on chain.\n */\n\n/** Which launchpad an event originated from. */\nexport type Launchpad = 'noxa' | 'odyssey'\n\n/** The kinds of alert this package emits. */\nexport type AlertEventKind = 'launch' | 'curve_trade' | 'graduation' | 'whale_trade'\n\n/** Buy or sell, from the trader's perspective. */\nexport type TradeSide = 'buy' | 'sell'\n\n/** Explorer deep links attached to every event. */\nexport interface ExplorerLinks {\n /** The transaction that produced the event. */\n tx: string\n /** The token contract. */\n token: string\n /** The actor (deployer or trader). */\n actor: string\n /** The Uniswap v3 pool, when the event has one. */\n pool?: string\n}\n\n/** Fields every alert event carries. */\nexport interface AlertEventBase {\n /**\n * Stable, content-addressed identity for this event: `kind:txHash:logIndex`.\n * Reprocessing the same block range regenerates the identical id, which is\n * what makes the delivery dedupe table work across restarts and reorg\n * re-reads.\n */\n id: string\n kind: AlertEventKind\n launchpad: Launchpad\n /** The memecoin the event is about. */\n token: Address\n /** Token symbol read from the ERC-20, or `null` when the call reverts. */\n symbol: string | null\n /** Token name read from the ERC-20, or `null` when the call reverts. */\n name: string | null\n /** The wallet responsible: the deployer for launches, the trader for trades. */\n actor: Address\n blockNumber: bigint\n logIndex: number\n transactionHash: Hash\n /**\n * Block timestamp in milliseconds since epoch, or `null` when the event was\n * decoded without a block fetch (the source fills it whenever available).\n */\n timestampMs: number | null\n /**\n * USD value of the event, or `null` when it cannot be derived from a real\n * price source. Never guessed: launches carry the seeded/initial-buy value,\n * trades carry the traded notional, and `null` means \"no USDG or WETH leg to\n * price against\", which rules treat as \"does not satisfy a min-USD filter\".\n */\n usdValue: number | null\n explorer: ExplorerLinks\n}\n\n/**\n * A new token launch.\n *\n * NOXA: decoded from `TokenLaunched` on the launch factory. `pool` is present\n * immediately and `initialBuyAmount` is the deployer's own first buy.\n * Odyssey: decoded from `TokenCreated` on a bonding-curve factory. `pool` is\n * `null` because the curve holds the liquidity until graduation.\n */\nexport interface LaunchEvent extends AlertEventBase {\n kind: 'launch'\n /** The Uniswap v3 pool, or `null` for an Odyssey token still on its curve. */\n pool: Address | null\n /** The quote token of the pool (NOXA only, `null` on Odyssey). */\n pairToken: Address | null\n /** Raw initial buy in the pair token's units (NOXA), else `0n`. */\n initialBuyAmount: bigint\n /**\n * The Uniswap v3 LP position NFT minted for this launch (NOXA), or `null`\n * when the launchpad did not mint one at launch time (every Odyssey token,\n * whose LP appears at graduation). Reputation rules read its owner to prove\n * the LP is locked.\n */\n positionId: bigint | null\n /**\n * `true` when this launchpad lists instantly with no bonding curve, so no\n * `graduation` event will ever follow. Always `true` for NOXA. For Odyssey\n * it is `true` only for launches from the instant factory variant.\n */\n instantListing: boolean\n}\n\n/**\n * A bonding-curve buy or sell. The Odyssey only: NOXA has no curve.\n * Decoded from `Traded` on an Odyssey factory.\n */\nexport interface CurveTradeEvent extends AlertEventBase {\n kind: 'curve_trade'\n launchpad: 'odyssey'\n side: TradeSide\n /** Token amount bought or sold, raw (18 decimals). */\n tokenAmount: bigint\n /** Native ETH paid or received, in wei. */\n quoteAmountWei: bigint\n /** Protocol fee taken on this trade, in wei. */\n feeWei: bigint\n /** Curve virtual quote reserve after the trade, in wei. */\n virtualQuoteWei: bigint\n /** Curve virtual token reserve after the trade, raw. */\n virtualTokenAmount: bigint\n /** Price of one token in ETH implied by this fill, or `null` for a zero-size fill. */\n priceEth: number | null\n}\n\n/**\n * A curve that filled and migrated to a locked Uniswap v3 pool.\n * The Odyssey only. Decoded from `PoolMigrated`.\n */\nexport interface GraduationEvent extends AlertEventBase {\n kind: 'graduation'\n launchpad: 'odyssey'\n /** The Uniswap v3 pool liquidity migrated into. */\n pool: Address\n /** The position manager token id holding the migrated liquidity. */\n positionId: bigint\n /** Uniswap v3 liquidity units minted at migration. */\n liquidity: bigint\n /** Token side used to seed the pool, raw. */\n tokenUsed: bigint\n /** Quote side used to seed the pool, raw (6 decimals when the quote is USDG). */\n quoteUsed: bigint\n}\n\n/**\n * A Uniswap v3 swap on a memecoin pool above the configured USD threshold.\n * Both launchpads produce these: NOXA from block one, Odyssey after\n * graduation.\n */\nexport interface WhaleTradeEvent extends AlertEventBase {\n kind: 'whale_trade'\n pool: Address\n /** The pool's quote token (the non-memecoin side). */\n quoteToken: Address\n /** Quote token symbol, e.g. `WETH` or `USDG`. */\n quoteSymbol: string\n side: TradeSide\n /** Memecoin amount that moved, in human units. */\n tokenAmount: number\n /** Quote amount that moved, in human units. */\n quoteAmount: number\n /** Executed price of the memecoin in quote-token units. */\n price: number\n /** Uniswap v3 fee tier of the pool, in hundredths of a bip (3000 = 0.3%). */\n feeTier: number\n}\n\n/** Any normalized alert event. */\nexport type AlertEvent = LaunchEvent | CurveTradeEvent | GraduationEvent | WhaleTradeEvent\n\n/** Narrow an event to a kind. */\nexport function isKind<K extends AlertEventKind>(\n event: AlertEvent,\n kind: K,\n): event is Extract<AlertEvent, { kind: K }> {\n return event.kind === kind\n}\n\n/**\n * Human-readable one-line label for an event kind, used by the bot and the\n * notifier formatters.\n */\nexport const EVENT_KIND_LABELS: Record<AlertEventKind, string> = {\n launch: 'New launch',\n curve_trade: 'Curve trade',\n graduation: 'Graduation',\n whale_trade: 'Whale trade',\n}\n\n/**\n * Which event kinds each launchpad can actually produce. The rule engine uses\n * this to reject impossible rules (for example: Odyssey-only `graduation`\n * combined with a NOXA-only launchpad filter would never fire).\n */\nexport const LAUNCHPAD_EVENT_KINDS: Record<Launchpad, readonly AlertEventKind[]> = {\n noxa: ['launch', 'whale_trade'],\n odyssey: ['launch', 'curve_trade', 'graduation', 'whale_trade'],\n}\n","import { getAddress, verifyMessage, type Address } from 'viem'\nimport { USDG_DECIMALS, erc20Abi, type HoodClient } from 'hoodchain'\nimport { formatUnits } from 'viem'\nimport { fetchLogRange, type FetchLogRangeOptions } from '../events/logs.js'\nimport type { Tier } from './policy.js'\n\n/**\n * Entitlements: who is premium, and why.\n *\n * The service resolves a subscriber's tier through exactly one interface, so\n * there is a single place to audit and a single place to swap the commercial\n * model. Two implementations ship:\n *\n * 1. {@link createStaticEntitlementProvider} is the documented default. Premium\n * subscribers come from configuration (`PREMIUM_SUBSCRIBERS` in the\n * environment). It has no dependencies, works offline, and is what a\n * self-hoster running the bot for their own community wants.\n * 2. {@link createUsdgEntitlementProvider} is a working on-chain rail. It reads\n * USDG `Transfer` logs to the operator's receiving address from a wallet\n * the subscriber has linked, and accrues subscription time from those real\n * payments. It needs no facilitator, no card processor and no third-party\n * service: USDG is the chain's own dollar and the ledger is the chain.\n *\n * Wallet linking is signature-verified (EIP-191 `personal_sign` over a\n * server-issued nonce), so a subscriber cannot claim someone else's payments\n * by pasting their address.\n */\n\n/** A resolved entitlement. */\nexport interface Entitlement {\n subscriberId: string\n tier: Tier\n /** When premium lapses, ms since epoch. `null` for free or for a permanent grant. */\n expiresAtMs: number | null\n /** Which provider decided this, for support and for `/tier`. */\n source: string\n}\n\n/** The single entitlement chokepoint. */\nexport interface EntitlementProvider {\n /** Resolve a subscriber's tier. Must never throw: fall back to free. */\n get(subscriberId: string): Promise<Entitlement>\n}\n\nconst free = (subscriberId: string, source: string): Entitlement => ({\n subscriberId,\n tier: 'free',\n expiresAtMs: null,\n source,\n})\n\n/**\n * Configuration-driven entitlements: the documented default.\n *\n * @example\n * ```ts\n * const entitlements = createStaticEntitlementProvider({\n * premiumSubscribers: ['telegram:12345', 'discord:987654321'],\n * })\n * ```\n */\nexport function createStaticEntitlementProvider(options: {\n premiumSubscribers?: readonly string[]\n /** Treat everyone as premium. For a private self-hosted deployment. */\n allPremium?: boolean\n}): EntitlementProvider {\n const premium = new Set((options.premiumSubscribers ?? []).map((id) => id.trim()).filter(Boolean))\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n if (options.allPremium === true || premium.has(subscriberId)) {\n return { subscriberId, tier: 'premium', expiresAtMs: null, source: 'static' }\n }\n return free(subscriberId, 'static')\n },\n }\n}\n\n/** Lookup of the wallet a subscriber has verifiably linked. */\nexport interface WalletLinkStore {\n /** The linked, signature-verified wallet for a subscriber, or `null`. */\n walletOf(subscriberId: string): Promise<Address | null>\n}\n\n/** Options for {@link createUsdgEntitlementProvider}. */\nexport interface UsdgEntitlementOptions {\n client: HoodClient\n /** The operator's receiving address. Payments to any other address are ignored. */\n receiver: Address\n /** Price of one subscription period, in whole USDG. */\n pricePerPeriodUsdg: number\n /** Length of a period in days. @defaultValue `30` */\n periodDays?: number\n /** Where verified wallet links come from. */\n links: WalletLinkStore\n /** First block to scan for payments. @defaultValue `0n` */\n fromBlock?: bigint\n /** Cache lifetime per subscriber, in ms. @defaultValue `300_000` (5 minutes) */\n ttlMs?: number\n /** Retry/bisect options for the payment scan. */\n logOptions?: FetchLogRangeOptions\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\nconst transferEvent = {\n type: 'event',\n name: 'Transfer',\n inputs: [\n { name: 'from', type: 'address', indexed: true },\n { name: 'to', type: 'address', indexed: true },\n { name: 'value', type: 'uint256', indexed: false },\n ],\n} as const\n\n/**\n * Accrue an expiry from a payment history. Each payment buys\n * `floor(amount / price)` periods, starting from whenever the current\n * entitlement would have lapsed, so renewing early extends rather than\n * overwrites.\n *\n * Exported because it is the part worth testing on its own: everything else in\n * the provider is a log query.\n */\nexport function accrueExpiry(\n payments: readonly { atMs: number; amountUsdg: number }[],\n pricePerPeriodUsdg: number,\n periodDays: number,\n): number | null {\n if (pricePerPeriodUsdg <= 0) return null\n const periodMs = periodDays * 86_400_000\n const ordered = [...payments].sort((a, b) => a.atMs - b.atMs)\n let expiry: number | null = null\n for (const payment of ordered) {\n const periods = Math.floor(payment.amountUsdg / pricePerPeriodUsdg)\n if (periods < 1) continue\n const start: number = expiry !== null && expiry > payment.atMs ? expiry : payment.atMs\n expiry = start + periods * periodMs\n }\n return expiry\n}\n\n/**\n * On-chain USDG entitlements.\n *\n * @example\n * ```ts\n * const entitlements = createUsdgEntitlementProvider({\n * client: hood,\n * receiver: '0xYourReceivingAddress',\n * pricePerPeriodUsdg: 25,\n * links: store, // the service's SQLite store implements WalletLinkStore\n * })\n * const entitlement = await entitlements.get('telegram:12345')\n * ```\n */\nexport function createUsdgEntitlementProvider(options: UsdgEntitlementOptions): EntitlementProvider {\n const ttlMs = options.ttlMs ?? 300_000\n const now = options.now ?? Date.now\n const periodDays = options.periodDays ?? 30\n const usdg =\n options.client.network === 'testnet'\n ? '0x7E955252E15c84f5768B83c41a71F9eba181802F'\n : '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168'\n const cache = new Map<string, { entitlement: Entitlement; at: number }>()\n\n async function resolve(subscriberId: string): Promise<Entitlement> {\n const wallet = await options.links.walletOf(subscriberId)\n if (!wallet) return free(subscriberId, 'usdg:no-linked-wallet')\n\n const head = await options.client.public.getBlockNumber()\n const logs = await fetchLogRange(\n (from, to) =>\n options.client.public.getLogs({\n address: usdg as Address,\n event: transferEvent,\n args: { from: wallet, to: getAddress(options.receiver) },\n fromBlock: from,\n toBlock: to,\n }),\n options.fromBlock ?? 0n,\n head,\n options.logOptions ?? {},\n )\n if (logs.length === 0) return free(subscriberId, 'usdg:no-payments')\n\n const payments: { atMs: number; amountUsdg: number }[] = []\n for (const log of logs) {\n const value = log.args.value as bigint | undefined\n if (value === undefined || value === 0n) continue\n const block = await options.client.public.getBlock({\n blockNumber: log.blockNumber,\n includeTransactions: false,\n })\n payments.push({\n atMs: Number(block.timestamp) * 1000,\n amountUsdg: Number(formatUnits(value, USDG_DECIMALS)),\n })\n }\n\n const expiresAtMs = accrueExpiry(payments, options.pricePerPeriodUsdg, periodDays)\n if (expiresAtMs === null || expiresAtMs <= now()) {\n return { subscriberId, tier: 'free', expiresAtMs, source: 'usdg:lapsed' }\n }\n return { subscriberId, tier: 'premium', expiresAtMs, source: 'usdg' }\n }\n\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n const hit = cache.get(subscriberId)\n if (hit && now() - hit.at < ttlMs) return hit.entitlement\n try {\n const entitlement = await resolve(subscriberId)\n cache.set(subscriberId, { entitlement, at: now() })\n return entitlement\n } catch (error) {\n // An RPC failure must never silently downgrade a paying subscriber to\n // free with no explanation, and must never upgrade a free one. Serve\n // the last known answer if there is one, otherwise free with a source\n // that says exactly what happened.\n if (hit) return hit.entitlement\n const message = error instanceof Error ? error.message : String(error)\n return free(subscriberId, `usdg:unavailable (${message})`)\n }\n },\n }\n}\n\n/**\n * Chain several providers: the first one that returns premium wins. Lets an\n * operator grant comps by configuration while still selling on chain.\n */\nexport function chainEntitlementProviders(\n ...providers: readonly EntitlementProvider[]\n): EntitlementProvider {\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n let last: Entitlement = free(subscriberId, 'chain:empty')\n for (const provider of providers) {\n const entitlement = await provider.get(subscriberId)\n if (entitlement.tier === 'premium') return entitlement\n last = entitlement\n }\n return last\n },\n }\n}\n\n/** The message a subscriber signs to link a wallet. */\nexport function linkMessage(subscriberId: string, nonce: string): string {\n return `hood-alerts wallet link\\nsubscriber: ${subscriberId}\\nnonce: ${nonce}`\n}\n\n/**\n * Verify a wallet link signature (EIP-191 `personal_sign`).\n *\n * The subscriber signs {@link linkMessage} with the wallet that pays, and the\n * service checks the signature before storing the link. Without this step,\n * anyone could paste a paying subscriber's address and inherit their\n * entitlement.\n *\n * @returns the checksummed address on success, `null` on any failure (bad\n * signature shape, wrong signer, malformed address).\n *\n * @example\n * ```ts\n * const nonce = crypto.randomUUID()\n * // the user signs linkMessage('telegram:12345', nonce) in their wallet\n * const address = await verifyWalletLink('telegram:12345', nonce, wallet, signature)\n * if (address) await store.linkWallet('telegram:12345', address)\n * ```\n */\nexport async function verifyWalletLink(\n subscriberId: string,\n nonce: string,\n address: string,\n signature: string,\n): Promise<Address | null> {\n if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) return null\n if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return null\n try {\n const valid = await verifyMessage({\n address: getAddress(address),\n message: linkMessage(subscriberId, nonce),\n signature: signature as `0x${string}`,\n })\n return valid ? getAddress(address) : null\n } catch {\n return null\n }\n}\n","import type { FetchLike, FetchLikeResponse } from './types.js'\n\n/** A completed HTTP response with its body parsed as JSON when possible. */\nexport interface JsonResponse {\n status: number\n ok: boolean\n headers: { get(name: string): string | null }\n body: unknown\n raw: string\n}\n\n/** Default sleep. */\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))\n\n/** Resolve the `fetch` to use: the injected double in tests, the global otherwise. */\nexport function resolveFetch(injected?: FetchLike): FetchLike {\n if (injected) return injected\n if (typeof globalThis.fetch !== 'function') {\n throw new Error('hood-alerts: global fetch is unavailable (Node 20+ required), and none was injected')\n }\n return globalThis.fetch as unknown as FetchLike\n}\n\n/**\n * POST JSON and parse the response.\n *\n * A non-JSON body (an HTML error page from a proxy, an empty 204) is not an\n * error here: `body` becomes `null` and `raw` keeps the text, so callers can\n * report what actually came back instead of a `SyntaxError` from deep inside\n * the adapter.\n */\nexport async function postJson(\n fetchImpl: FetchLike,\n url: string,\n payload: unknown,\n headers: Record<string, string> = {},\n): Promise<JsonResponse> {\n const response: FetchLikeResponse = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(payload),\n })\n const raw = await response.text()\n let body: unknown = null\n if (raw) {\n try {\n body = JSON.parse(raw)\n } catch {\n body = null\n }\n }\n return { status: response.status, ok: response.ok, headers: response.headers, body, raw }\n}\n\n/** Read a numeric header, or `null` when absent or unparseable. */\nexport function numericHeader(\n headers: { get(name: string): string | null },\n name: string,\n): number | null {\n const value = headers.get(name)\n if (value === null) return null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/** Read a nested property from an unknown JSON body without casting blindly. */\nexport function pick(body: unknown, ...path: string[]): unknown {\n let current: unknown = body\n for (const key of path) {\n if (typeof current !== 'object' || current === null) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n","import { numericHeader, pick, postJson, resolveFetch, sleep as realSleep } from './http.js'\nimport {\n delivered,\n failed,\n type DeliveryResult,\n type FetchLike,\n type Notifier,\n type NotifierHttpOptions,\n type RenderedAlert,\n} from './types.js'\n\n/**\n * Telegram Bot API adapter.\n *\n * Rate limiting is the part everyone gets wrong. Telegram does not use a\n * header budget: it answers `429` with a JSON body\n * `{\"ok\":false,\"error_code\":429,\"description\":\"Too Many Requests: retry after 7\",\n * \"parameters\":{\"retry_after\":7}}` and expects the caller to wait exactly that\n * many *seconds* before retrying. This adapter honours `parameters.retry_after`\n * (falling back to the `Retry-After` header, then to exponential backoff), caps\n * the total wait at `maxWaitMs` so one throttled chat cannot stall the whole\n * dispatch loop, and treats `400`/`403` as permanent so a deleted chat or a\n * blocked bot stops being retried forever.\n */\n\n/** A raw Telegram Bot API response. */\nexport interface TelegramResponse {\n ok: boolean\n status: number\n /** `result` on success. */\n result: unknown\n /** `description` on failure. */\n description: string | null\n /** `parameters.retry_after` in seconds, when Telegram sent one. */\n retryAfterSeconds: number | null\n}\n\n/** Options for {@link createTelegramClient}. */\nexport interface TelegramClientOptions extends NotifierHttpOptions {\n /** Bot token from @BotFather, `<id>:<secret>`. */\n botToken: string\n /** API origin. Override for a local Bot API server. @defaultValue `https://api.telegram.org` */\n apiBase?: string\n}\n\n/** A thin, rate-limit-aware Telegram Bot API client. */\nexport interface TelegramClient {\n /** Call any Bot API method. Retries 429 and 5xx, never retries 4xx. */\n call(method: string, params: Record<string, unknown>): Promise<TelegramResponse>\n /** How many requests the last {@link TelegramClient.call} needed. */\n readonly lastAttempts: number\n}\n\nconst PERMANENT_STATUSES = new Set([400, 401, 403, 404])\n\n/** Build a Telegram Bot API client. */\nexport function createTelegramClient(options: TelegramClientOptions): TelegramClient {\n if (!/^\\d+:[A-Za-z0-9_-]+$/.test(options.botToken)) {\n throw new Error('hood-alerts: TELEGRAM_BOT_TOKEN must look like \"<bot id>:<secret>\"')\n }\n const fetchImpl: FetchLike = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://api.telegram.org').replace(/\\/+$/, '')\n const maxAttempts = options.maxAttempts ?? 4\n const backoffMs = options.backoffMs ?? 500\n const maxWaitMs = options.maxWaitMs ?? 60_000\n const sleep = options.sleep ?? realSleep\n\n let lastAttempts = 0\n\n return {\n get lastAttempts() {\n return lastAttempts\n },\n async call(method: string, params: Record<string, unknown>): Promise<TelegramResponse> {\n const url = `${apiBase}/bot${options.botToken}/${method}`\n let attempt = 0\n let last: TelegramResponse = {\n ok: false,\n status: 0,\n result: null,\n description: 'no attempt was made',\n retryAfterSeconds: null,\n }\n\n while (attempt < maxAttempts) {\n attempt += 1\n lastAttempts = attempt\n let response\n try {\n response = await postJson(fetchImpl, url, params)\n } catch (error) {\n // Transport failure: retry with backoff, it is not a Telegram verdict.\n last = {\n ok: false,\n status: 0,\n result: null,\n description: error instanceof Error ? error.message : String(error),\n retryAfterSeconds: null,\n }\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n continue\n }\n\n const description = typeof pick(response.body, 'description') === 'string'\n ? (pick(response.body, 'description') as string)\n : null\n const retryAfterField = pick(response.body, 'parameters', 'retry_after')\n const retryAfterSeconds =\n typeof retryAfterField === 'number'\n ? retryAfterField\n : numericHeader(response.headers, 'retry-after')\n\n last = {\n ok: response.status === 200 && pick(response.body, 'ok') === true,\n status: response.status,\n result: pick(response.body, 'result') ?? null,\n description,\n retryAfterSeconds,\n }\n if (last.ok) return last\n\n if (response.status === 429) {\n const waitMs = (retryAfterSeconds ?? backoffMs / 1000) * 1000\n if (waitMs > maxWaitMs || attempt >= maxAttempts) return last\n await sleep(waitMs)\n continue\n }\n if (PERMANENT_STATUSES.has(response.status)) return last\n if (attempt >= maxAttempts) return last\n await sleep(backoffMs * 2 ** (attempt - 1))\n }\n return last\n },\n }\n}\n\n/** Options for {@link createTelegramNotifier}. */\nexport interface TelegramNotifierOptions extends TelegramClientOptions {\n /** Suppress link previews. @defaultValue `true` (alerts carry several links). */\n disableWebPagePreview?: boolean\n /** Deliver silently, without a notification sound. @defaultValue `false` */\n disableNotification?: boolean\n /** Reuse an existing client instead of building one (the bot shares its client). */\n client?: TelegramClient\n}\n\n/**\n * Build the Telegram delivery adapter.\n *\n * @example\n * ```ts\n * const telegram = createTelegramNotifier({ botToken: process.env.TELEGRAM_BOT_TOKEN! })\n * const result = await telegram.send('-1001234567890', renderAlert(event))\n * if (!result.ok) console.error(result.error, 'retryable:', result.retryable)\n * ```\n */\nexport function createTelegramNotifier(options: TelegramNotifierOptions): Notifier {\n const client = options.client ?? createTelegramClient(options)\n const disableWebPagePreview = options.disableWebPagePreview ?? true\n const disableNotification = options.disableNotification ?? false\n\n return {\n platform: 'telegram',\n kind: 'bot',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n if (!/^-?\\d+$|^@[A-Za-z][A-Za-z0-9_]{4,}$/.test(target)) {\n return failed(\n null,\n 0,\n `invalid Telegram chat id \"${target}\": expected a numeric id or an @username`,\n false,\n )\n }\n const response = await client.call('sendMessage', {\n chat_id: /^-?\\d+$/.test(target) ? Number(target) : target,\n text: alert.text,\n parse_mode: alert.parseMode,\n link_preview_options: { is_disabled: disableWebPagePreview },\n disable_notification: disableNotification,\n })\n const attempts = client.lastAttempts\n\n if (response.ok) {\n const messageId = pick(response.result, 'message_id')\n return delivered(response.status, attempts, messageId === undefined ? null : String(messageId))\n }\n const retryable = response.status === 429 || response.status >= 500 || response.status === 0\n return failed(\n response.status || null,\n attempts,\n response.description ?? `Telegram sendMessage failed with status ${response.status}`,\n retryable,\n )\n },\n }\n}\n","/** Structured logging: one JSON object per line, machine-readable by default. */\n\n/** Log levels, ordered. */\nexport const LEVELS = ['debug', 'info', 'warn', 'error'] as const\n\n/** A log level. */\nexport type LogLevel = (typeof LEVELS)[number]\n\n/** Structured fields attached to a log line. */\nexport type LogFields = Record<string, unknown>\n\n/** The logger the service uses everywhere. */\nexport interface Logger {\n debug(message: string, fields?: LogFields): void\n info(message: string, fields?: LogFields): void\n warn(message: string, fields?: LogFields): void\n error(message: string, fields?: LogFields): void\n /** A logger that adds `fields` to every line, for per-component context. */\n child(fields: LogFields): Logger\n}\n\n/** Options for {@link createLogger}. */\nexport interface LoggerOptions {\n /** Minimum level emitted. @defaultValue `'info'` */\n level?: LogLevel\n /** Where lines go. @defaultValue `console.log` */\n write?: (line: string) => void\n /** Fields added to every line. */\n base?: LogFields\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\n/**\n * Serialize a value that may contain a bigint or an Error. Log lines must\n * never throw: a `TypeError: Do not know how to serialize a BigInt` inside an\n * error handler loses the error it was reporting.\n */\nfunction safeStringify(value: unknown): string {\n return JSON.stringify(value, (_key, raw: unknown) => {\n if (typeof raw === 'bigint') return raw.toString()\n if (raw instanceof Error) return { name: raw.name, message: raw.message, stack: raw.stack }\n return raw\n })\n}\n\n/** Build a structured logger. */\nexport function createLogger(options: LoggerOptions = {}): Logger {\n const minimum = LEVELS.indexOf(options.level ?? 'info')\n const write = options.write ?? ((line: string): void => console.log(line))\n const now = options.now ?? Date.now\n const base = options.base ?? {}\n\n const emit = (level: LogLevel, message: string, fields?: LogFields): void => {\n if (LEVELS.indexOf(level) < minimum) return\n write(\n safeStringify({\n time: new Date(now()).toISOString(),\n level,\n message,\n ...base,\n ...(fields ?? {}),\n }),\n )\n }\n\n return {\n debug: (message, fields) => emit('debug', message, fields),\n info: (message, fields) => emit('info', message, fields),\n warn: (message, fields) => emit('warn', message, fields),\n error: (message, fields) => emit('error', message, fields),\n child(fields: LogFields): Logger {\n return createLogger({ ...options, base: { ...base, ...fields } })\n },\n }\n}\n\n/** A logger that discards everything. For tests and for `--quiet`. */\nexport function createSilentLogger(): Logger {\n const silent: Logger = {\n debug: () => undefined,\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n child: () => silent,\n }\n return silent\n}\n","import { createTelegramClient, type TelegramClient, type TelegramClientOptions } from '../notifiers/telegram.js'\nimport type { Logger } from '../service/logger.js'\nimport { createSilentLogger } from '../service/logger.js'\nimport type { CommandContext, CommandRouter } from './commands.js'\n\n/**\n * The Telegram bot: long polling, because it needs no public URL, no TLS\n * certificate and no inbound firewall rule. A self-hoster runs the container\n * and the bot works; a webhook deployment would need all three before the\n * first `/start`.\n *\n * `getUpdates` is called with a long timeout, so the loop is idle rather than\n * busy between messages, and the offset is advanced past every update the loop\n * has handled so a restart never re-processes an old command.\n */\n\n/** Options for {@link createTelegramBot}. */\nexport interface TelegramBotOptions extends TelegramClientOptions {\n router: CommandRouter\n logger?: Logger\n /** Reuse the notifier's client so both share one rate-limit path. */\n client?: TelegramClient\n /** Long-poll timeout in seconds. @defaultValue `30` */\n pollTimeoutSeconds?: number\n /** Persist the update offset so a restart resumes cleanly. */\n offsetStore?: { get(): number | null; set(offset: number): void }\n}\n\n/** A running bot. */\nexport interface TelegramBot {\n /** Poll once and handle whatever arrived. Returns how many updates it handled. */\n pollOnce(): Promise<number>\n /** Publish the command list so Telegram shows the `/` menu. */\n publishCommands(): Promise<boolean>\n /** Start the loop. Resolves when {@link TelegramBot.stop} is called. */\n start(): Promise<void>\n /** Stop the loop after the current poll. */\n stop(): void\n}\n\ninterface TelegramUpdate {\n update_id: number\n message?: {\n message_id: number\n text?: string\n chat?: { id: number }\n from?: { id: number; is_bot?: boolean }\n }\n}\n\nfunction parseCommand(text: string): { command: string; args: string[]; rest: string } | null {\n const trimmed = text.trim()\n if (!trimmed.startsWith('/')) return null\n const [head, ...tail] = trimmed.split(/\\s+/)\n if (!head) return null\n return { command: head, args: tail, rest: trimmed.slice(head.length).trim() }\n}\n\n/**\n * Build the Telegram bot.\n *\n * @example\n * ```ts\n * const bot = createTelegramBot({ botToken: process.env.TELEGRAM_BOT_TOKEN!, router })\n * await bot.publishCommands()\n * await bot.start()\n * ```\n */\nexport function createTelegramBot(options: TelegramBotOptions): TelegramBot {\n const client = options.client ?? createTelegramClient(options)\n const logger = options.logger ?? createSilentLogger()\n const pollTimeout = options.pollTimeoutSeconds ?? 30\n let offset = options.offsetStore?.get() ?? 0\n let running = false\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n const message = update.message\n const text = message?.text\n const chatId = message?.chat?.id\n const fromId = message?.from?.id\n if (!text || chatId === undefined || fromId === undefined || message?.from?.is_bot === true) return\n\n const parsed = parseCommand(text)\n if (!parsed) return\n\n const context: CommandContext = {\n subscriberId: `telegram:${fromId}`,\n platform: 'telegram',\n defaultTarget: String(chatId),\n args: parsed.args,\n rest: parsed.rest,\n }\n const reply = await options.router.handle(parsed.command, context)\n const response = await client.call('sendMessage', {\n chat_id: chatId,\n text: reply.html,\n parse_mode: 'HTML',\n link_preview_options: { is_disabled: true },\n })\n if (!response.ok) {\n logger.warn('telegram reply failed', {\n chatId,\n command: parsed.command,\n error: response.description,\n })\n }\n }\n\n return {\n async publishCommands(): Promise<boolean> {\n const response = await client.call('setMyCommands', {\n commands: options.router.commands.map((command) => ({\n command: command.name,\n description: command.description.slice(0, 256),\n })),\n })\n if (!response.ok) logger.warn('setMyCommands failed', { error: response.description })\n return response.ok\n },\n\n async pollOnce(): Promise<number> {\n const response = await client.call('getUpdates', {\n ...(offset > 0 ? { offset } : {}),\n timeout: pollTimeout,\n allowed_updates: ['message'],\n })\n if (!response.ok) {\n logger.warn('getUpdates failed', { error: response.description })\n return 0\n }\n const updates = Array.isArray(response.result) ? (response.result as TelegramUpdate[]) : []\n for (const update of updates) {\n try {\n await handleUpdate(update)\n } catch (error) {\n // One bad command must not stall the loop or replay forever: log it\n // and still advance past it.\n logger.error('telegram update handler threw', {\n updateId: update.update_id,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n offset = update.update_id + 1\n options.offsetStore?.set(offset)\n }\n return updates.length\n },\n\n async start(): Promise<void> {\n running = true\n logger.info('telegram bot started', { offset })\n while (running) {\n try {\n await this.pollOnce()\n } catch (error) {\n logger.error('telegram poll failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n await new Promise((resolve) => setTimeout(resolve, 2_000))\n }\n }\n logger.info('telegram bot stopped')\n },\n\n stop(): void {\n running = false\n },\n }\n}\n","import { createPublicKey, verify as cryptoVerify } from 'node:crypto'\nimport { resolveFetch } from '../notifiers/http.js'\nimport type { FetchLike } from '../notifiers/types.js'\nimport { truncate } from '../notifiers/escape.js'\nimport type { CommandContext, CommandRouter, CommandSpec } from './commands.js'\n\n/**\n * The Discord bot: slash commands over the HTTP interactions endpoint.\n *\n * Discord offers two ways to receive commands: a persistent gateway\n * WebSocket, or an HTTPS interactions endpoint. This package uses the\n * endpoint, because the service already runs an HTTP server for `/health` and\n * `/metrics`, and an endpoint scales to zero and survives a restart with no\n * reconnect logic. The price is that every request must be signature-verified,\n * which Discord enforces: it sends deliberately invalid signatures during\n * setup and refuses to save an endpoint that answers them with anything other\n * than `401`.\n *\n * Verification is Ed25519 over `timestamp + rawBody`, using the application's\n * public key. Node's `crypto` verifies Ed25519 natively, so this needs no\n * dependency: the raw 32-byte key is wrapped in the fixed SPKI DER prefix for\n * `id-Ed25519` (RFC 8410) and handed to `createPublicKey`.\n */\n\n/** Discord's interaction type numbers, for the two kinds handled here. */\nexport const INTERACTION_TYPE = { PING: 1, APPLICATION_COMMAND: 2 } as const\n\n/** Discord's interaction callback types. */\nexport const CALLBACK_TYPE = { PONG: 1, CHANNEL_MESSAGE_WITH_SOURCE: 4 } as const\n\n/** Discord marks a message ephemeral (only the caller sees it) with this flag. */\nexport const EPHEMERAL_FLAG = 1 << 6\n\n/** RFC 8410 SPKI prefix for an Ed25519 public key. */\nconst ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')\n\n/**\n * Verify a Discord interaction signature.\n *\n * @param publicKey the application's hex public key from the developer portal.\n * @returns `true` only for a signature that verifies. Any malformed input is\n * `false`, never a throw: a crash here would answer Discord with a 500 and\n * fail its endpoint validation just as surely as a wrong answer.\n */\nexport function verifyDiscordSignature(\n publicKey: string,\n signature: string,\n timestamp: string,\n rawBody: string,\n): boolean {\n if (!/^[0-9a-fA-F]{64}$/.test(publicKey)) return false\n if (!/^[0-9a-fA-F]{128}$/.test(signature)) return false\n if (!timestamp) return false\n try {\n const key = createPublicKey({\n key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(publicKey, 'hex')]),\n format: 'der',\n type: 'spki',\n })\n return cryptoVerify(\n null,\n Buffer.from(timestamp + rawBody, 'utf8'),\n key,\n Buffer.from(signature, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n/** A Discord slash-command definition, as the API expects it. */\nexport interface DiscordCommandDefinition {\n name: string\n description: string\n /** Option type 3 is STRING, the only type these commands need. */\n options?: { type: 3; name: string; description: string; required: boolean }[]\n}\n\n/** Convert the shared command catalogue into Discord slash-command definitions. */\nexport function discordCommandDefinitions(\n commands: readonly CommandSpec[],\n): DiscordCommandDefinition[] {\n return commands.map((command) => ({\n name: command.name,\n // Discord caps descriptions at 100 characters and rejects the whole\n // registration if any one is longer.\n description: truncate(command.description, 100),\n ...(command.options && command.options.length > 0\n ? {\n options: command.options.map((option) => ({\n type: 3 as const,\n name: option.name,\n description: truncate(option.description, 100),\n required: option.required,\n })),\n }\n : {}),\n }))\n}\n\n/** Options for {@link registerDiscordCommands}. */\nexport interface RegisterCommandsOptions {\n applicationId: string\n botToken: string\n /** Register to one guild (instant) instead of globally (up to an hour to propagate). */\n guildId?: string\n apiBase?: string\n fetch?: FetchLike\n}\n\n/**\n * Register slash commands with Discord.\n *\n * @returns the number of commands Discord accepted.\n * @throws when Discord rejects the registration, with its own error message\n * attached: a silent failure here means a bot that appears installed and has\n * no commands.\n *\n * @example\n * ```bash\n * DISCORD_APPLICATION_ID=... DISCORD_BOT_TOKEN=... npm run register:discord\n * ```\n */\nexport async function registerDiscordCommands(\n commands: readonly CommandSpec[],\n options: RegisterCommandsOptions,\n): Promise<number> {\n const fetchImpl = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://discord.com/api/v10').replace(/\\/+$/, '')\n const url = options.guildId\n ? `${apiBase}/applications/${options.applicationId}/guilds/${options.guildId}/commands`\n : `${apiBase}/applications/${options.applicationId}/commands`\n\n const response = await fetchImpl(url, {\n method: 'PUT',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bot ${options.botToken}`,\n },\n body: JSON.stringify(discordCommandDefinitions(commands)),\n })\n const raw = await response.text()\n if (!response.ok) {\n throw new Error(`hood-alerts: Discord rejected the command registration (${response.status}): ${raw}`)\n }\n try {\n const parsed: unknown = JSON.parse(raw)\n return Array.isArray(parsed) ? parsed.length : 0\n } catch {\n return 0\n }\n}\n\n/** The shape of an incoming interaction, narrowed to what the router needs. */\ninterface Interaction {\n type: number\n guild_id?: string\n channel_id?: string\n member?: { user?: { id?: string } }\n user?: { id?: string }\n data?: {\n name?: string\n options?: { name: string; value?: unknown }[]\n }\n}\n\n/** What to answer Discord with. */\nexport interface InteractionResponse {\n status: number\n body: unknown\n}\n\n/** Options for {@link createDiscordInteractionHandler}. */\nexport interface DiscordInteractionOptions {\n router: CommandRouter\n /** The application's public key, for signature verification. */\n publicKey: string\n /** Reply only to the caller. @defaultValue `true` */\n ephemeral?: boolean\n}\n\n/**\n * Build the interactions handler. Mount it on the service's HTTP server at\n * whatever path is configured as the application's Interactions Endpoint URL.\n *\n * @example\n * ```ts\n * const interactions = createDiscordInteractionHandler({ router, publicKey })\n * // inside the HTTP server:\n * const answer = await interactions.handle({ signature, timestamp, rawBody })\n * res.writeHead(answer.status, { 'content-type': 'application/json' })\n * res.end(JSON.stringify(answer.body))\n * ```\n */\nexport function createDiscordInteractionHandler(options: DiscordInteractionOptions): {\n handle(request: { signature: string | null; timestamp: string | null; rawBody: string }): Promise<InteractionResponse>\n} {\n const ephemeral = options.ephemeral ?? true\n\n return {\n async handle(request): Promise<InteractionResponse> {\n if (\n !request.signature ||\n !request.timestamp ||\n !verifyDiscordSignature(options.publicKey, request.signature, request.timestamp, request.rawBody)\n ) {\n return { status: 401, body: { error: 'invalid request signature' } }\n }\n\n let interaction: Interaction\n try {\n interaction = JSON.parse(request.rawBody) as Interaction\n } catch {\n return { status: 400, body: { error: 'body is not JSON' } }\n }\n\n if (interaction.type === INTERACTION_TYPE.PING) {\n return { status: 200, body: { type: CALLBACK_TYPE.PONG } }\n }\n if (interaction.type !== INTERACTION_TYPE.APPLICATION_COMMAND) {\n return { status: 400, body: { error: `unsupported interaction type ${interaction.type}` } }\n }\n\n const name = interaction.data?.name\n if (!name) return { status: 400, body: { error: 'interaction carried no command name' } }\n\n const values = (interaction.data?.options ?? [])\n .map((option) => (option.value === undefined ? '' : String(option.value)))\n .filter((value) => value.length > 0)\n // A Discord option value can itself contain spaces (a JSON rule body, a\n // webhook URL), so the router receives the option values as separate\n // args and the joined string as `rest`.\n const rest = values.join(' ')\n const args = values.length === 1 ? (values[0] as string).split(/\\s+/) : values\n\n const userId = interaction.member?.user?.id ?? interaction.user?.id ?? ''\n const context: CommandContext = {\n // A guild pays once for all its channels; a DM is billed to the user.\n subscriberId: `discord:${interaction.guild_id ?? userId}`,\n platform: 'discord',\n defaultTarget: interaction.channel_id ?? '',\n args,\n rest: `${name} ${rest}`.trim(),\n }\n\n const reply = await options.router.handle(name, context)\n return {\n status: 200,\n body: {\n type: CALLBACK_TYPE.CHANNEL_MESSAGE_WITH_SOURCE,\n data: {\n // Discord hard-caps message content at 2000 characters.\n content: truncate(reply.text, 1_900),\n ...(ephemeral ? { flags: EPHEMERAL_FLAG } : {}),\n },\n },\n }\n },\n }\n}\n"],"mappings":"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","names":["import_viem","import_viem","nonce","sleep","reply","import_node_crypto","cryptoVerify","reply"]}
@@ -0,0 +1,241 @@
1
+ import { P as Platform } from '../schema-NnIdKFl2.cjs';
2
+ import { EntitlementGate } from '../tiers/index.cjs';
3
+ import { A as AlertStore, c as Logger } from '../logger-3phvV_fl.cjs';
4
+ import { a as TelegramClientOptions, T as TelegramClient } from '../telegram-CpodYJrP.cjs';
5
+ import { F as FetchLike } from '../types-CcoKpStA.cjs';
6
+ import 'zod';
7
+ import '../types-DFPpTMWo.cjs';
8
+ import 'viem';
9
+ import 'hoodchain';
10
+ import '../logs-D7sYBvSy.cjs';
11
+ import '../registry-DePA3IyS.cjs';
12
+ import '../ratelimit-BKVQG2oY.cjs';
13
+
14
+ /**
15
+ * The bot's command surface, written once and shared by Telegram and Discord.
16
+ *
17
+ * Both platforms deliver the same thing to this router: a command name, a
18
+ * subscriber id, a default delivery target and a list of arguments. Keeping
19
+ * the logic here means the two bots cannot drift apart in what `/rule add`
20
+ * accepts or what `/tier` reports, and it means the command tests exercise the
21
+ * real code path rather than a Telegram-shaped copy of it.
22
+ *
23
+ * Every command answers bad input with a specific, actionable message. There
24
+ * is no silent failure and no raw validation dump: a mistyped threshold gets
25
+ * "usd must be a positive number, for example /threshold whales 5000".
26
+ */
27
+ /** What a platform hands the router. */
28
+ interface CommandContext {
29
+ /** Stable identity for entitlements: `telegram:<user id>`, `discord:<guild or user id>`. */
30
+ subscriberId: string;
31
+ platform: Platform;
32
+ /**
33
+ * Where alerts would go if the user does not name a target: the Telegram
34
+ * chat id, or the Discord channel id the command was used in.
35
+ */
36
+ defaultTarget: string;
37
+ /** Arguments after the command word. */
38
+ args: string[];
39
+ /** The whole argument string, for commands that take free-form JSON. */
40
+ rest: string;
41
+ }
42
+ /** A reply to render back to the user. */
43
+ interface CommandReply {
44
+ /** Telegram HTML, which Discord renders acceptably after tag stripping. */
45
+ html: string;
46
+ /** Plain-text equivalent, used by Discord and by logs. */
47
+ text: string;
48
+ }
49
+ /** A command the bot exposes. */
50
+ interface CommandSpec {
51
+ name: string;
52
+ description: string;
53
+ /** Usage line shown by `/help` and in the README. */
54
+ usage: string;
55
+ /** Discord slash-command options, when the command takes arguments. */
56
+ options?: {
57
+ name: string;
58
+ description: string;
59
+ required: boolean;
60
+ }[];
61
+ }
62
+ /** Everything the router needs. */
63
+ interface CommandRouterDeps {
64
+ store: AlertStore;
65
+ gate: EntitlementGate;
66
+ /** Clock injection point for tests. @defaultValue `Date.now` */
67
+ now?: () => number;
68
+ /** Id factory for new subscriptions and nonces. @defaultValue `randomUUID` */
69
+ newId?: () => string;
70
+ /** Shown by `/upgrade`: how to pay. Set from the service config. */
71
+ upgradeInstructions?: string;
72
+ }
73
+ /** The router. */
74
+ interface CommandRouter {
75
+ /** Every command, for `/help` and for Discord registration. */
76
+ readonly commands: readonly CommandSpec[];
77
+ /** Handle one command. Never throws: errors come back as a reply. */
78
+ handle(command: string, context: CommandContext): Promise<CommandReply>;
79
+ }
80
+ /** The command catalogue. */
81
+ declare const COMMANDS: readonly CommandSpec[];
82
+ /** Build the command router. */
83
+ declare function createCommandRouter(deps: CommandRouterDeps): CommandRouter;
84
+
85
+ /**
86
+ * The Telegram bot: long polling, because it needs no public URL, no TLS
87
+ * certificate and no inbound firewall rule. A self-hoster runs the container
88
+ * and the bot works; a webhook deployment would need all three before the
89
+ * first `/start`.
90
+ *
91
+ * `getUpdates` is called with a long timeout, so the loop is idle rather than
92
+ * busy between messages, and the offset is advanced past every update the loop
93
+ * has handled so a restart never re-processes an old command.
94
+ */
95
+ /** Options for {@link createTelegramBot}. */
96
+ interface TelegramBotOptions extends TelegramClientOptions {
97
+ router: CommandRouter;
98
+ logger?: Logger;
99
+ /** Reuse the notifier's client so both share one rate-limit path. */
100
+ client?: TelegramClient;
101
+ /** Long-poll timeout in seconds. @defaultValue `30` */
102
+ pollTimeoutSeconds?: number;
103
+ /** Persist the update offset so a restart resumes cleanly. */
104
+ offsetStore?: {
105
+ get(): number | null;
106
+ set(offset: number): void;
107
+ };
108
+ }
109
+ /** A running bot. */
110
+ interface TelegramBot {
111
+ /** Poll once and handle whatever arrived. Returns how many updates it handled. */
112
+ pollOnce(): Promise<number>;
113
+ /** Publish the command list so Telegram shows the `/` menu. */
114
+ publishCommands(): Promise<boolean>;
115
+ /** Start the loop. Resolves when {@link TelegramBot.stop} is called. */
116
+ start(): Promise<void>;
117
+ /** Stop the loop after the current poll. */
118
+ stop(): void;
119
+ }
120
+ /**
121
+ * Build the Telegram bot.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const bot = createTelegramBot({ botToken: process.env.TELEGRAM_BOT_TOKEN!, router })
126
+ * await bot.publishCommands()
127
+ * await bot.start()
128
+ * ```
129
+ */
130
+ declare function createTelegramBot(options: TelegramBotOptions): TelegramBot;
131
+
132
+ /**
133
+ * The Discord bot: slash commands over the HTTP interactions endpoint.
134
+ *
135
+ * Discord offers two ways to receive commands: a persistent gateway
136
+ * WebSocket, or an HTTPS interactions endpoint. This package uses the
137
+ * endpoint, because the service already runs an HTTP server for `/health` and
138
+ * `/metrics`, and an endpoint scales to zero and survives a restart with no
139
+ * reconnect logic. The price is that every request must be signature-verified,
140
+ * which Discord enforces: it sends deliberately invalid signatures during
141
+ * setup and refuses to save an endpoint that answers them with anything other
142
+ * than `401`.
143
+ *
144
+ * Verification is Ed25519 over `timestamp + rawBody`, using the application's
145
+ * public key. Node's `crypto` verifies Ed25519 natively, so this needs no
146
+ * dependency: the raw 32-byte key is wrapped in the fixed SPKI DER prefix for
147
+ * `id-Ed25519` (RFC 8410) and handed to `createPublicKey`.
148
+ */
149
+ /** Discord's interaction type numbers, for the two kinds handled here. */
150
+ declare const INTERACTION_TYPE: {
151
+ readonly PING: 1;
152
+ readonly APPLICATION_COMMAND: 2;
153
+ };
154
+ /** Discord's interaction callback types. */
155
+ declare const CALLBACK_TYPE: {
156
+ readonly PONG: 1;
157
+ readonly CHANNEL_MESSAGE_WITH_SOURCE: 4;
158
+ };
159
+ /** Discord marks a message ephemeral (only the caller sees it) with this flag. */
160
+ declare const EPHEMERAL_FLAG: number;
161
+ /**
162
+ * Verify a Discord interaction signature.
163
+ *
164
+ * @param publicKey the application's hex public key from the developer portal.
165
+ * @returns `true` only for a signature that verifies. Any malformed input is
166
+ * `false`, never a throw: a crash here would answer Discord with a 500 and
167
+ * fail its endpoint validation just as surely as a wrong answer.
168
+ */
169
+ declare function verifyDiscordSignature(publicKey: string, signature: string, timestamp: string, rawBody: string): boolean;
170
+ /** A Discord slash-command definition, as the API expects it. */
171
+ interface DiscordCommandDefinition {
172
+ name: string;
173
+ description: string;
174
+ /** Option type 3 is STRING, the only type these commands need. */
175
+ options?: {
176
+ type: 3;
177
+ name: string;
178
+ description: string;
179
+ required: boolean;
180
+ }[];
181
+ }
182
+ /** Convert the shared command catalogue into Discord slash-command definitions. */
183
+ declare function discordCommandDefinitions(commands: readonly CommandSpec[]): DiscordCommandDefinition[];
184
+ /** Options for {@link registerDiscordCommands}. */
185
+ interface RegisterCommandsOptions {
186
+ applicationId: string;
187
+ botToken: string;
188
+ /** Register to one guild (instant) instead of globally (up to an hour to propagate). */
189
+ guildId?: string;
190
+ apiBase?: string;
191
+ fetch?: FetchLike;
192
+ }
193
+ /**
194
+ * Register slash commands with Discord.
195
+ *
196
+ * @returns the number of commands Discord accepted.
197
+ * @throws when Discord rejects the registration, with its own error message
198
+ * attached: a silent failure here means a bot that appears installed and has
199
+ * no commands.
200
+ *
201
+ * @example
202
+ * ```bash
203
+ * DISCORD_APPLICATION_ID=... DISCORD_BOT_TOKEN=... npm run register:discord
204
+ * ```
205
+ */
206
+ declare function registerDiscordCommands(commands: readonly CommandSpec[], options: RegisterCommandsOptions): Promise<number>;
207
+ /** What to answer Discord with. */
208
+ interface InteractionResponse {
209
+ status: number;
210
+ body: unknown;
211
+ }
212
+ /** Options for {@link createDiscordInteractionHandler}. */
213
+ interface DiscordInteractionOptions {
214
+ router: CommandRouter;
215
+ /** The application's public key, for signature verification. */
216
+ publicKey: string;
217
+ /** Reply only to the caller. @defaultValue `true` */
218
+ ephemeral?: boolean;
219
+ }
220
+ /**
221
+ * Build the interactions handler. Mount it on the service's HTTP server at
222
+ * whatever path is configured as the application's Interactions Endpoint URL.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * const interactions = createDiscordInteractionHandler({ router, publicKey })
227
+ * // inside the HTTP server:
228
+ * const answer = await interactions.handle({ signature, timestamp, rawBody })
229
+ * res.writeHead(answer.status, { 'content-type': 'application/json' })
230
+ * res.end(JSON.stringify(answer.body))
231
+ * ```
232
+ */
233
+ declare function createDiscordInteractionHandler(options: DiscordInteractionOptions): {
234
+ handle(request: {
235
+ signature: string | null;
236
+ timestamp: string | null;
237
+ rawBody: string;
238
+ }): Promise<InteractionResponse>;
239
+ };
240
+
241
+ export { CALLBACK_TYPE, COMMANDS, type CommandContext, type CommandReply, type CommandRouter, type CommandRouterDeps, type CommandSpec, type DiscordCommandDefinition, type DiscordInteractionOptions, EPHEMERAL_FLAG, INTERACTION_TYPE, type InteractionResponse, type RegisterCommandsOptions, type TelegramBot, type TelegramBotOptions, createCommandRouter, createDiscordInteractionHandler, createTelegramBot, discordCommandDefinitions, registerDiscordCommands, verifyDiscordSignature };