Skip to content

Repository files navigation

NarnixVPN

A Telegram shop bot for selling V2Ray subscriptions, deployed as a single Cloudflare Worker.

NarnixVPN is built on the Narnix framework — the Worker-side bot toolkit this project shares with the framework's other bots. The bot itself runs in one Worker: the storefront, the wallet, the 3X-UI provisioning client, the support desk, the administration surface and the cron jobs. The AI support assistant module also runs in that Worker, relaying user messages to a separate AI backend that runs in Docker over signed callbacks.

What it does

Area Behaviour
Storefront Category → package → inbound cart, live pricing, mandatory inbounds, per-package extras
Checkout Wallet balance or card-to-card top-up, discount coupons, one-time free trial
Delivery Config created on the 3X-UI panel; subscription URL and QR code delivered in chat
Accounts Balance, deposits with approval, transaction history, renewals that re-price and reset traffic
Support Tickets relayed into the owner's private forum topics, with a delivery ledger and backlog recovery
AI assistant A temporary forum topic per user, relayed to a separate agent backend over signed callbacks
Administration Admins, panels, inbounds, categories, packages, coupons, broadcast, user lookup, manual balance adjust
Automation Cron-triggered broadcast drain, expiry/quota warnings, panel reachability
Languages Persian and English, with compile-time-checked translation keys

Modules

The bot is assembled from 12 pluggable modules. A module contributes a grammY composer and, optionally, conversation flows (wizards); the registry mounts them in this order:

Module Responsibility
start Main menu, /start, referral links, user bootstrap
free_test One-time free trial subscription
language Language switch and per-user locale
wallet Balance, card-to-card deposits and approvals, transaction history
assistant AI support assistant: temporary topic, message relay, signed callback dispatch
ticket Owner-only support tickets over dual forum topics
user Subscription list, config delivery, renewals
coupon Discount codes with usage tracking
shop Package purchase flow, pricing, checkout, provisioning
category Category CRUD and the package wizard
packages Package catalogue and inbound pricing
admin Panels, inbounds, admins, lookup, broadcast, manual balance adjust

Architecture

  • Module registry. src/core/module.ts defines the BotModule contract; src/core/registry.ts registers the modules and mounts them through attachToBot(). The registry refuses duplicate module ids and duplicate conversation ids — the latter because ctx.conversation.enter(id) resolves through one flat map, so a collision would silently shadow a wizard.
  • Deliberate middleware order (src/core/bot.ts): session → env + i18n binder → channel lock → conversations plugin → module composers. An active wizard must consume updates before any message:text handler can steal them.
  • Wizards on conversations v2. src/core/wizard.ts is a small toolkit over @grammyjs/conversations (ask, askText, askInt, askNumber, askChoice, askPhoto, confirm, /cancel handling, Persian/Arabic digit normalisation). Replay state lives in D1, not KV (src/core/conversationStorage.ts): KV is eventually consistent, and a stale replay log is corruption rather than a missed step.
  • HTML in one place. src/core/parseMode.ts injects parse_mode: HTML on the formatted Bot API methods. Because the conversations plugin builds a fresh Api per replay, the transformer is re-installed per conversation (src/core/conversations.ts).
  • Pluggable payment gateways. src/modules/wallet/gateways/ holds BasePaymentGateway, a CardToCardGateway and a registry that self-heals from the database on read (register/get/ensureRegistered/syncWithDB) — the documented Workers pattern, since a Worker has no safe startup hook.
  • Typed i18n derived from the locale file. src/types/i18n.ts derives every dotted leaf path of src/locales/fa.json, so translation keys are checked at compile time and there is no generated file to drift. check.i18n.ts audits parity, unused keys and inline Persian.
  • Money primitives. src/utils/balance.ts owns the only two ways a balance may move. Both return a boolean instead of throwing (a refund runs inside a catch block that is already explaining a failure) and both reject amounts that would corrupt the row arithmetically.
  • 3X-UI client. src/utils/v2ray.ts creates, updates and removes clients, reads traffic and builds the subscription URL; src/utils/subDelivery.ts and src/utils/configTag.ts handle delivery and config naming.
  • Background jobs (src/jobs/): a keyset-cursor broadcast drain with a lease, a panel reachability probe with a permanent-failure classifier, and an expiry/quota warnings pass. Two Cron Triggers are declared; the dispatcher matches event.cron as a literal string, which is why the trigger stays declared even while the warnings pass is switched off.
  • AI assistant (src/modules/assistant/): a temporary forum topic per conversation, message relay into the backend, and signed callback dispatch on POST /ai/callback. The module is registered before the ticket relay on purpose — both claim threaded private-chat messages, and the ticket relay's unlinked-topic branch would otherwise answer messages in AI topics.

Stack

Area Choice
Runtime Cloudflare Workers (compatibility_date 2026-06-14)
Language TypeScript, strict, noEmit + skipLibCheck
Bot framework grammY with @grammyjs/conversations v2
Storage Cloudflare D1 (binding DB, database narnix-db) and KV namespace SESSION_KV (24 h session TTL)
Tooling pnpm workspace, wrangler, tsx, vitest
Panel 3X-UI over its HTTP API

Layout

src/core/        bot assembly, module contract, registry, wizard toolkit, conversations,
                 conversation storage, D1 model layer, env assertions, error handler
src/modules/     start, free_test, language, wallet, assistant, ticket, user, coupon,
                 shop, category, packages, admin
src/jobs/        broadcast drain, warnings, reachability, cron dispatcher
src/middlewares/ channel lock
src/utils/       3X-UI client, subscription delivery, balance, i18n, KV sessions, dates,
                 randomness, formatting, config tags
src/locales/     fa.json, en.json
src/types/       ambient Env bindings, context, i18n, database, coupon types
src/scripts/     setWebhook.ts
migrations/      0001_baseline … 0010_ai_chats

Getting started

Requirements: Node.js with pnpm, a Cloudflare account (Workers, D1, KV) and a bot token from @BotFather.

pnpm install
cp .dev.vars.example .dev.vars   # then fill in the values
pnpm db:migrate                  # apply the migrations to the local D1 database
pnpm dev                         # wrangler dev

pnpm poll runs the same bot over long polling instead of a webhook, through getPlatformProxy() against the local D1/KV bindings — useful when Telegram cannot reach your machine. Set LOCAL_PROXY to a SOCKS proxy if the Telegram API is blocked on your network.

Configuration

Local development reads .dev.vars (gitignored); production reads Worker secrets. src/core/env.ts logs the names of any required variable that is missing at the top of every invocation, so a forgotten wrangler secret put cannot fail silently.

Variable Required Purpose
BOT_TOKEN yes Telegram bot token
OWNER yes Owner user id — a super-admin regardless of the admins table
ADMIN_GROUP_ID no Supergroup for admin notifications and support-ticket topics
CHANNEL_LOCK, CHANNEL_LOCK_LINK no Mandatory-join channel and the link shown on the "join first" screen
QR_SERVICE_URL no Self-hosted QR renderer (falls back to a public one)
AI_BACKEND_URL, AI_CALLBACK_SECRET no Assistant backend and the HMAC key shared with it
LOCAL_PROXY no SOCKS proxy, read only by src/poll.ts
WEBHOOK_URL, SECRET_TOKEN no Used by src/scripts/setWebhook.ts to register the webhook

Deploy

wrangler secret put BOT_TOKEN
wrangler secret put OWNER
wrangler secret put ADMIN_GROUP_ID      # optional
wrangler secret put CHANNEL_LOCK        # optional
wrangler secret put CHANNEL_LOCK_LINK   # optional
wrangler secret put QR_SERVICE_URL      # optional
wrangler secret put AI_BACKEND_URL      # optional
wrangler secret put AI_CALLBACK_SECRET  # optional
wrangler secret put UPDATE_SECRET       # the webhook secret the Worker verifies

pnpm deploy             # wrangler deploy
pnpm webhook            # register the webhook with Telegram
pnpm db:migrate:remote  # apply the migrations to the remote D1 database

Scripts

Script What it runs
pnpm dev wrangler dev
pnpm poll / pnpm poll:watch long polling through getPlatformProxy() (nodemon in watch mode)
pnpm test vitest run
pnpm typecheck tsc --noEmit
pnpm check:i18n locale parity, unused-key and inline-Persian audit
pnpm smoke:wizards wizard smoke harness
pnpm deploy / pnpm webhook deploy the Worker / register the webhook
pnpm db:migrate, pnpm db:migrate:remote, pnpm db:migrations apply and list D1 migrations

Cron Triggers

Expression Job
* * * * * Broadcast drain — one cursor batch per invocation
*/15 * * * * Expiry and quota warnings

Both are declared in wrangler.jsonc and dispatched in src/jobs/index.ts by matching the expression string. The warnings pass ships disabled (WARNINGS_ENABLED = false in that file — nothing was removed, flipping the boolean is the whole change) and its trigger is deliberately left in place, so that re-enabling it cannot silently fall through to the broadcast drain.

Assistant backend protocol

The AI assistant module is the Workers-side half of a separate support-agent service that runs in Docker. A user's messages are relayed into a temporary topic of their own, and the backend answers through:

POST /ai/callback
x-narnix-signature: t=<unix>,v1=<hex>

The signature is HMAC-SHA256 over the timestamp and body with AI_CALLBACK_SECRET, and requests outside a five-minute window are rejected as replays. The Worker handles these actions: answer, typing, create_ticket, close_chat, reset_chat, send_qr, send_config, draft and stage. With AI_BACKEND_URL / AI_CALLBACK_SECRET unset the feature is simply off.

Notes

  • The repository contains no CI configuration; verification is the pnpm scripts and the two root harnesses, run locally.
  • smoke.wizards.ts has two wallet checks that fail against the current harness data; the drift is in the harness, not in the module.
  • Webhook registration reads SECRET_TOKEN while webhook verification reads UPDATE_SECRET (src/index.ts). Set both to the same value, or registration will succeed and updates will be rejected.
  • pnpm poll:watch expects nodemon to be available; it is not a declared dependency.

License

NarnixVPN is released under the GNU Affero General Public License v3 (AGPL-3.0-only), the copyleft license designed for software that runs on servers: anyone operating a modified version must offer its full source code to the users interacting with it. The complete license text is in LICENSE.

The Narnix framework and the separate AI assistant backend are independent projects and carry their own licensing.

About

A Telegram shop bot for selling V2Ray subscriptions, deployed as a single Cloudflare Worker.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages