@adriangalilea/utils 7.1.0 → 8.0.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 (113) hide show
  1. package/README.md +13 -23
  2. package/dist/bot/access-control.d.ts +89 -31
  3. package/dist/bot/access-control.d.ts.map +1 -1
  4. package/dist/bot/access-control.js +10 -28
  5. package/dist/bot/access-control.js.map +1 -1
  6. package/dist/bot/callbacks.d.ts +0 -13
  7. package/dist/bot/callbacks.d.ts.map +1 -1
  8. package/dist/bot/callbacks.js +0 -16
  9. package/dist/bot/callbacks.js.map +1 -1
  10. package/dist/bot/kit.d.ts +4 -9
  11. package/dist/bot/kit.d.ts.map +1 -1
  12. package/dist/bot/kit.js +0 -1
  13. package/dist/bot/kit.js.map +1 -1
  14. package/dist/bot/lang.d.ts +46 -0
  15. package/dist/bot/lang.d.ts.map +1 -0
  16. package/dist/bot/lang.js +29 -0
  17. package/dist/bot/lang.js.map +1 -0
  18. package/dist/bot/language.d.ts +0 -2
  19. package/dist/bot/language.d.ts.map +1 -1
  20. package/dist/bot/language.js +0 -12
  21. package/dist/bot/language.js.map +1 -1
  22. package/dist/bot/llm.d.ts +1 -2
  23. package/dist/bot/llm.d.ts.map +1 -1
  24. package/dist/bot/llm.js +1 -2
  25. package/dist/bot/llm.js.map +1 -1
  26. package/dist/bot/menu.d.ts +7 -0
  27. package/dist/bot/menu.d.ts.map +1 -1
  28. package/dist/bot/menu.js +8 -2
  29. package/dist/bot/menu.js.map +1 -1
  30. package/dist/bot/payments/callbacks.d.ts +1 -1
  31. package/dist/bot/payments/callbacks.d.ts.map +1 -1
  32. package/dist/bot/payments/callbacks.js +9 -11
  33. package/dist/bot/payments/callbacks.js.map +1 -1
  34. package/dist/bot/payments/commands.d.ts +1 -1
  35. package/dist/bot/payments/commands.d.ts.map +1 -1
  36. package/dist/bot/payments/commands.js +2 -2
  37. package/dist/bot/payments/commands.js.map +1 -1
  38. package/dist/bot/payments/derive.d.ts.map +1 -1
  39. package/dist/bot/payments/derive.js +7 -6
  40. package/dist/bot/payments/derive.js.map +1 -1
  41. package/dist/bot/payments/handlers.d.ts +1 -5
  42. package/dist/bot/payments/handlers.d.ts.map +1 -1
  43. package/dist/bot/payments/handlers.js +1 -2
  44. package/dist/bot/payments/handlers.js.map +1 -1
  45. package/dist/bot/payments/invoice.d.ts.map +1 -1
  46. package/dist/bot/payments/invoice.js +1 -2
  47. package/dist/bot/payments/invoice.js.map +1 -1
  48. package/dist/bot/payments/menu-item.d.ts.map +1 -1
  49. package/dist/bot/payments/menu-item.js +2 -2
  50. package/dist/bot/payments/menu-item.js.map +1 -1
  51. package/dist/bot/payments/payouts.d.ts +3 -3
  52. package/dist/bot/payments/payouts.js +3 -3
  53. package/dist/bot/payments/plugin.d.ts +87 -29
  54. package/dist/bot/payments/plugin.d.ts.map +1 -1
  55. package/dist/bot/payments/refund.d.ts +1 -5
  56. package/dist/bot/payments/refund.d.ts.map +1 -1
  57. package/dist/bot/payments/refund.js +6 -32
  58. package/dist/bot/payments/refund.js.map +1 -1
  59. package/dist/bot/payments/types.d.ts +2 -9
  60. package/dist/bot/payments/types.d.ts.map +1 -1
  61. package/dist/bot/payments/types.js +0 -7
  62. package/dist/bot/payments/types.js.map +1 -1
  63. package/dist/bot/payments/waiver.d.ts +1 -17
  64. package/dist/bot/payments/waiver.d.ts.map +1 -1
  65. package/dist/bot/payments/waiver.js +2 -30
  66. package/dist/bot/payments/waiver.js.map +1 -1
  67. package/dist/bot/reactions.d.ts +0 -2
  68. package/dist/bot/reactions.d.ts.map +1 -1
  69. package/dist/bot/reactions.js +0 -16
  70. package/dist/bot/reactions.js.map +1 -1
  71. package/dist/bot/session.d.ts +1 -21
  72. package/dist/bot/session.d.ts.map +1 -1
  73. package/dist/bot/session.js +1 -29
  74. package/dist/bot/session.js.map +1 -1
  75. package/dist/bot/storage.d.ts +1 -1
  76. package/dist/bot/storage.js +1 -1
  77. package/dist/bot/worker.d.ts.map +1 -1
  78. package/dist/bot/worker.js +1 -4
  79. package/dist/bot/worker.js.map +1 -1
  80. package/dist/cli/text.d.ts.map +1 -1
  81. package/dist/cli/text.js +2 -3
  82. package/dist/cli/text.js.map +1 -1
  83. package/dist/llm/index.js +5 -5
  84. package/dist/llm/index.js.map +1 -1
  85. package/dist/runtime.d.ts +0 -4
  86. package/dist/runtime.d.ts.map +1 -1
  87. package/dist/runtime.js +0 -17
  88. package/dist/runtime.js.map +1 -1
  89. package/dist/tg-html/index.d.ts +11 -4
  90. package/dist/tg-html/index.d.ts.map +1 -1
  91. package/dist/tg-html/index.js +13 -8
  92. package/dist/tg-html/index.js.map +1 -1
  93. package/dist/tg-md/index.d.ts +6 -6
  94. package/dist/tg-md/index.d.ts.map +1 -1
  95. package/dist/tg-md/index.js +9 -19
  96. package/dist/tg-md/index.js.map +1 -1
  97. package/dist/universal/time.d.ts +11 -2
  98. package/dist/universal/time.d.ts.map +1 -1
  99. package/dist/universal/time.js +22 -2
  100. package/dist/universal/time.js.map +1 -1
  101. package/package.json +9 -15
  102. package/dist/bot/index.d.ts +0 -31
  103. package/dist/bot/index.d.ts.map +0 -1
  104. package/dist/bot/index.js +0 -31
  105. package/dist/bot/index.js.map +0 -1
  106. package/dist/universal/currency/download-crypto-list.d.ts +0 -10
  107. package/dist/universal/currency/download-crypto-list.d.ts.map +0 -1
  108. package/dist/universal/currency/download-crypto-list.js +0 -69
  109. package/dist/universal/currency/download-crypto-list.js.map +0 -1
  110. package/dist/universal/url/update-rules.d.ts +0 -15
  111. package/dist/universal/url/update-rules.d.ts.map +0 -1
  112. package/dist/universal/url/update-rules.js +0 -122
  113. package/dist/universal/url/update-rules.js.map +0 -1
package/README.md CHANGED
@@ -77,19 +77,6 @@ Enable it only with a deletion and retention policy; no identity is inferred.
77
77
  `overlaps: [["a", "b"]]` declares unordered audience overlap, not an ordered funnel.
78
78
  Daily totals support counts and averages, not percentile latency or event ordering.
79
79
 
80
- **7.0 migration:** `defineMetrics` takes `store: metricsStore(driver, project)` instead
81
- of `write: metricsWriter(...)`; a custom store implements `declare` and `write`, and
82
- `Measurement` no longer carries `spec`. Existing tables gain a column and a table:
83
- `ALTER TABLE metric_definition ADD COLUMN per_user INTEGER NOT NULL DEFAULT 0` and the
84
- `metric_project` table from `METRICS_SCHEMA` (one row per project, its overlaps as
85
- JSON; a 6.0 `metric_overlap` table is dropped); the first sample after the deploy
86
- declares the real flags, labels and overlaps. Ingestion credentials need add and
87
- update on every metric table and never delete: the store reconciles nothing. `readAudience` takes the schema
88
- (`readSchema(reader, project)`) instead of an overlap list and returns a row per
89
- declared per-user key. `readReport` replaces hand-rolled window + rows + audience
90
- composition. Metric keys and dimensions are storage identifiers: introduce a new key
91
- when changing meaning rather than relabelling history.
92
-
93
80
  ## Installation
94
81
 
95
82
  ```bash
@@ -600,7 +587,7 @@ await ctx.say.answer({ en: 'OK', es: 'OK' }) // → ctx.answer (callback o
600
587
 
601
588
  ### Telegram HTML (`tg-html`)
602
589
 
603
- Transform arbitrary (LLM-emitted) HTML into Telegram-compatible `parse_mode=HTML` with opinionated, consistent spacing. Telegram's HTML subset has no headings, lists, or block layout — send it `<h1>`/`<ul>` and the API rejects the whole message. `transform()` accepts the HTML a model naturally writes and renders the structure typographically: `h1` → bold+underline + blank line, `h2…h6` → bold + blank line, `ul/li` → `• ` bullets, blocks get blank lines, blockquotes keep their internal line breaks, allowed tags pass through with attributes filtered, unknown tags drop their markup (content kept), and stray `<`/`>` escape instead of vanishing. Zero dependencies, no DOM, Worker-safe. Successor of the standalone `tghtml` package (jsr, archived).
590
+ Transform arbitrary (LLM-emitted) HTML into Telegram-compatible `parse_mode=HTML` with opinionated, consistent spacing. Telegram's HTML subset has no headings, lists, or block layout — send it `<h1>`/`<ul>` and the API rejects the whole message. `transform()` accepts the HTML a model naturally writes and renders the structure typographically: `h1` → bold+underline + blank line, `h2…h6` → bold + blank line, `ul/li` → `• ` bullets, blocks get blank lines, blockquotes keep their internal line breaks, allowed tags pass through with attributes filtered, unknown tags drop their markup (content kept), and stray `<`/`>` escape instead of vanishing. Zero dependencies, no DOM, Worker-safe.
604
591
 
605
592
  ```typescript
606
593
  import { transform } from '@adriangalilea/utils/tg-html'
@@ -626,7 +613,7 @@ Zero dependencies, Worker-safe. Typography (heading air, bullet glyph, title und
626
613
 
627
614
  ### Telegram bot plugins (GramIO)
628
615
 
629
- Plugins for personal Telegram bots built on [GramIO](https://gramio.dev). Each plugin lives at its own subpath; peer deps (`gramio`, `@gramio/storage`, `@gramio/session`, `@gramio/format`, `marked`) are **all optional** — install only what you import.
616
+ Plugins for personal Telegram bots built on [GramIO](https://gramio.dev). Each plugin lives at its own subpath; peer deps (`gramio`, `@gramio/storage`, `@gramio/session`, `@gramio/format`) are **all optional** — install only what you import.
630
617
 
631
618
  ```bash
632
619
  pnpm add @adriangalilea/utils gramio @gramio/storage @gramio/session
@@ -677,14 +664,15 @@ Cloudflare is one adapter, not the architecture: `bot/worker` and `bot/storage-d
677
664
 
678
665
  Telegram's [Threaded Mode](https://telegram.org/blog/threaded-conversations) for private chats (BotFather → Bot Settings → Threaded Mode) auto-threads out of the box: `@gramio/contexts` ≥0.9 (what gramio ≥0.12 resolves) forwards `message_thread_id` on every `ctx.send` / `ctx.sendDocument` / `ctx.reply` — including from callback handlers, which carry `threadId` too. No fork, no override.
679
666
 
680
- **Runs on Cloudflare Workers / bun / anywhere.** Every bot subpath below is import-safe off Node — no filesystem access, no `node:*` modules, no import-time side effects anywhere in its graph — **except `bot/kit`**, which deliberately owns the Node-only pieces (process signal handling in `gracefulStart`, kev-backed env reading in `adminContext`). `pnpm test:worker-safe` walks every graph and screams on regression.
667
+ **Runs on Cloudflare Workers / bun / anywhere.** Every bot subpath below is import-safe off Node — no filesystem access, no `node:*` modules, no import-time side effects anywhere in its graph — **except `bot/kit`**, whose `gracefulStart` handles process signals. `pnpm test:worker-safe` walks every graph and screams on regression.
681
668
 
682
669
  | Subpath | What it does |
683
670
  |---|---|
684
671
  | `@adriangalilea/utils/bot/ctx` | Structural ctx types (`BotMessageCtx`, `BotCallbackCtx`, …) and the `narrow<T>(ctx)` cast helper. Pure typing — no runtime state. |
685
672
  | `@adriangalilea/utils/bot/keys` | The bot-id key namespace, a **persisted contract**: `botId(ctx)`, `botStorageKey(ctx, userId)` → `bot-<id>:<userId>`, `botSubKey(ctx, sub)` → `bot-<id>:<sub>`. Every storage key the library writes derives from here, and those keys live in YOUR Redis/D1 rows — the shape never changes. Pure functions of `ctx.bot.info.id` (no env/fs), which is what keeps the whole bot surface Worker-safe. |
686
- | `@adriangalilea/utils/bot/kit` | The Node-only corner. `gracefulStart(bot, opts?)` — SIGINT/SIGTERM → `bot.stop()` → exit; force-kills if shutdown hangs. DMs the admin `@<bot> started.` / `@<bot> shutting down.` by default when `KEV.TELEGRAM_ADMIN_ID` is set (graceful only — crashes don't trigger `onStop`); pass `notifyAdmin: false` to disable or `notifyAdmin: 12345` for an explicit chat id.<br>`adminContext({ adminId? })` — reads `TELEGRAM_ADMIN_ID` from `kev` (with optional hardcoded fallback), decorates `ctx.adminId` + `ctx.isAdmin`.<br>Also re-exports `botSession` / `prefixStorage` from `bot/session` so Node consumers keep one import. |
687
- | `@adriangalilea/utils/bot/session` | `botSession(opts)` — **drop-in replacement for `@gramio/session`'s `session()`** that auto-namespaces every key as `bot-<id>:<senderId>` using `ctx.bot.info.id` (populated by `getMe()` at startup). Use this instead of `session()` — full stop. Multiple bots sharing one Redis/D1 stay isolated by construction; every plugin in this package derives the same prefix internally via `botStorageKey(ctx, userId)` / `botSubKey(ctx, sub)` (from `bot/keys`). No regex, no manual prefix argument, no way to forget.<br>`prefixStorage(storage, prefix)` — escape hatch for adding a top-level prefix on top of the bot-id namespace; almost never needed. Worker-safe. |
673
+ | `@adriangalilea/utils/bot/kit` | The Node-only corner, one function: `gracefulStart(bot, opts?)` — SIGINT/SIGTERM → `bot.stop()` → exit; force-kills if shutdown hangs; calls `bot.syncCommands()` before `bot.start()`. DMs the admin `@<bot> started.` / `@<bot> shutting down.` by default when `KEV.TELEGRAM_ADMIN_ID` is set (graceful only — crashes don't trigger `onStop`); pass `notifyAdmin: false` to disable or `notifyAdmin: 12345` for an explicit chat id. |
674
+ | `@adriangalilea/utils/bot/admin` | `adminContext(admins)` — decorates `ctx.adminId` (the primary admin: the approve/deny and notification target) and `ctx.isAdmin` (any admin) on every event. Ids arrive POSITIONALLY as a number, an array, or a FUNCTION consulted per update, so a db-backed resolver changes who is admin with no restart. Reads no env and touches no OS, which is what keeps it Worker-safe; a Node bot composes env in at the call site (`adminContext(kev.int('TELEGRAM_ADMIN_ID', 123))`). `parseIdList("123, 456 789")` turns an env string into clean ids. `accessControl` and the payments refund flow declare a gramio dependency on this plugin, so gramio throws at `bot.start()` if you forget it. |
675
+ | `@adriangalilea/utils/bot/session` | `botSession(opts)` — **drop-in replacement for `@gramio/session`'s `session()`** that auto-namespaces every key as `bot-<id>:<senderId>` using `ctx.bot.info.id` (populated by `getMe()` at startup). Use this instead of `session()` — full stop. Multiple bots sharing one Redis/D1 stay isolated by construction; every plugin in this package derives the same prefix internally via `botStorageKey(ctx, userId)` / `botSubKey(ctx, sub)` (from `bot/keys`). No regex, no manual prefix argument, no way to forget. Worker-safe. |
688
676
  | `@adriangalilea/utils/bot/notify` | Best-effort admin DMs, worker-safe (you pass the ids; no env, no process). `notifyAdmins(bot, adminIds, text, extra?)` — DM each admin, failures logged and swallowed (a notification must never take the bot down). `alertAdminError(bot, adminIds, label, error, throttle?)` — truncated `🚨 label\nName: message`, rate-limited through a caller-owned `alertThrottle(ms?)` so a failure storm sends one alert per window. `gracefulStart`'s start/stop DMs are built on this. |
689
677
  | `@adriangalilea/utils/bot/profile` | `syncBotProfile(bot, { name?, description?, about?, photo?, commands?, expects?, adminIds? })` — the bot's Telegram-facing identity as CODE, never BotFather: localized name / description / About / public command list, reconciled idempotently on every boot (get → compare → set per field per language; unchanged values cost one read, so cold-start firing is rate-limit-free). `expects: { inline: true }` declares the BotFather-only capabilities the code assumes — a mismatch (inline mode off on an inline-dependent bot) DMs the admins, since the API can only detect it, not fix it. Never throws; failures log and the bot keeps running. |
690
678
  | `@adriangalilea/utils/bot/access-control` | Personal-bot ACL — gates non-admin/non-default users; admin gets DM with `[✅ Aprobar][❌ Denegar]` on first attempt; `/access` opens a persistent menu (revoke / reapprove / list pending). Backed by `@gramio/session` per-user + a small index. **Native alternative**: BotFather → Bot Settings → Access → "Restrict bot usage" — flat allow-list at Telegram. Use this plugin when you want in-bot approval flow instead of a BotFather round-trip; both can coexist. |
@@ -699,10 +687,10 @@ Telegram's [Threaded Mode](https://telegram.org/blog/threaded-conversations) for
699
687
  | `@adriangalilea/utils/bot/coalesce` | Joins client-split inbound messages back into one. When a user pastes >4096 chars, Telegram clients fragment it into separate `message` updates with no marker. Middleware detects the burst and emits one combined event. |
700
688
  | `@adriangalilea/utils/llm` | Multi-provider LLM caller on the Vercel AI SDK — the policy layer the SDK doesn't ship. `createLlm({ providers, health? })` → `.stream(req)` (typed events: `delta` / `reasoning` / `reset` / `tool-call` / `end`) and `.complete(req)`. **Failover:** priority-ordered across OpenAI-compatible, Anthropic-compatible, and OpenRouter endpoints; multiple keys per provider (health tracked per key fingerprint); per-key circuit breaker with exponential cool-down persisted in any KV-shaped `HealthStore`; an attempt that dies after emitting yields `reset` (discard, next candidate regenerates); empty completions count as failures. **Accounting:** tokens summed across billed attempts; `costUsd` is the ACTUAL charge (OpenRouter usage accounting via `providerMetadata`, or a `cost` field on the raw usage frame) — never a price-table estimate. **Knobs:** per-model temperature, maxTokens caps, `disableThinking` mapped to each dialect. **Tools:** AI SDK `tool()` + `toolChoice` pass through (re-exported). Worker-safe; peers: `ai`, `@ai-sdk/openai-compatible`, `@ai-sdk/anthropic`, `@openrouter/ai-sdk-provider`. |
701
689
  | `@adriangalilea/utils/bot/llm` | The Telegram side of an LLM chatbot; the model side is `@adriangalilea/utils/llm`. **Output:** `streamChatReply(ctx, events, opts?)` consumes an `AsyncIterable<LlmStreamEvent>` and paints it with Telegram's native message-draft streaming (`sendMessageDraft` full-frame repaints, throttled ~1/s, keepalive under the ~30s draft TTL), then persists the finished markdown via `ctx.send`, entity-split across 4096 by `@gramio/split`. Reasoning models get a thinking phase — streams into the ephemeral draft and evaporates (`reasoning: 'preview'`, default), persists as an expandable blockquote (`'message'`), or never renders at all (`'hidden'`). Upstream `reset` (provider failover) repaints the draft from scratch. Drafts are private-chat-only (+ BotFather forum-topic mode); elsewhere the preview phase is skipped and only the final send happens. Returns `{ content, reasoning, toolCalls, usage, messages }`. The painter underneath is `@adriangalilea/utils/bot/draft` (`createDraftPreview(ctx, { render, throttleMs? })`): dependency-light (no markdown machinery), for producers that push deltas or own their per-frame rendering (plain or rich-message frames) and persist path, with `streamForensics` (render-regression + reset warnings) built in and exported standalone. **History:** `llmHistory({...})` returns `.plugin` (decorates `ctx.llm` with `.add() / .get() / .clear() / .all() / .clearAll()`, per-(user, thread) OpenAI `ChatMessage` shape, persisted in the shared session record so the menu's 🗑 Forget wipes it automatically) AND `.menuItem` (drop-in "🗑 Delete this thread" for `botMenu` — wipes the LLM history AND calls `deleteForumTopic` so the Telegram thread + all its messages disappear from the chat; falls back to history-only clear when no `threadId` is present). |
702
- | `@adriangalilea/utils/metrics` | Declared counters and timings, bounded dimensions, optional actor IDs, and a schema for generic reporting. Inject a writer; measurement failures are reported without rejecting calls. See Product metrics below. |
703
- | `@adriangalilea/utils/metrics/sqlite` | Atomic SQLite/libSQL writer, explicit provisioning schema, and inclusive UTC daily reports. Connections and credentials belong to the application. |
690
+ | `@adriangalilea/utils/metrics` | Declared counters and timings, bounded dimensions, optional actor IDs, and a schema for generic reporting. Inject a store (`store: metricsStore(driver, project)`); measurement failures are reported without rejecting calls. See Product metrics below. |
691
+ | `@adriangalilea/utils/metrics/sqlite` | Atomic SQLite driver, explicit provisioning schema, and inclusive UTC daily reports; libSQL is its own module (`metrics/libsql`) over the same driver shape. Connections and credentials belong to the application. |
704
692
  | `@adriangalilea/utils/bot/menu` | `botMenu({ command, description, items, privacy?, personalData?, adminContact })` — `/settings` command + InlineKeyboard router. Root view always renders a `🛡️ Privacy & data` submenu button that wraps the privacy policy link plus (if `personalData: { storage }`) 🗑 Forget + 📥 Export buttons. Items take `keepRow` (render on the same row as the next item — e.g. a two-per-row language picker) and `rootExtra` (render at the bottom of the root menu, below Privacy & data). `label` / `header` / `style` / `visible` resolvers may be **async** (read your db at render time — never cache render strings in the session); `parseMode: 'HTML'` renders the header formatted (you own escaping); `personalData.onForget(ctx, userId)` wipes YOUR tables inside the same try as the session delete, so Forget either forgets everything or reports failure. `toggleMenuItem({ id, read, write, label: { off, on }, toast? })` — convenience factory for boolean-toggle items with dynamic label + optional toast, storage-agnostic via `read`/`write` closures. |
705
- | `@adriangalilea/utils/bot/payments` | `botPayments({ session, storage, paysupport, paysupportHint?, legal, waiver, vip?, credits?, perks? })` — Telegram Stars monetization in one drop-in plugin. **Three axes, all optional:** `vip` (positional tier ladder — single rung in v1 is just `vip: [{...}]`, ladder is `vip: [{...}, {...}]`; ids are `vip.1`, `vip.2`, …), `credits` (consumable balance + top-up packs `credits.1`, `credits.2`, …), `perks` (orthogonal one-shot unlocks `perks.<key>`). **Surface:** `ctx.payments.atLeast('vip')` / `atLeast('vip.2')` (typed rank check), `ctx.payments.tier()` / `.tier.level()` / `.tier.label()`, `ctx.payments.credits.{balance, consume, tryConsume}` (throws `InsufficientCredits`), `ctx.payments.has(perkId)`, `await ctx.payments.require('vip', { feature? })` (gate that sends a localized upgrade prompt deep-linked to `/settings → 💎 VIP`), `await ctx.payments.invoice(productKey)` (threads Art. 103(m) TRLGDCU consent inline before `sendInvoice`). **Owns:** waiver consent flow (versioned text → forces re-consent on bump, snapshotted on every charge for audit), `/paysupport` slash command (Telegram ToS §6.5; `paysupportHint` overrides the where-to-manage-charges line when your menu isn't `/settings`), idempotent `successful_payment` fulfillment via `pay:idempotency:{chargeId}` sentinel, lazy subscription expiry (no cron needed), tier upgrade auto-cancel of the lower rung's renewal, and admin-DM refund approval (mirror of `accessControl`'s [✅ Aprobar][❌ Denegar] pattern). **Returns:** `{ plugin, menuItem, payouts, onFulfilled }` — `menuItem` is the drop-in `💎 VIP` entry for `botMenu`; `payouts.{record, list, export, exportForUsers}` is the Fragment payout ledger (you receive TON, log the EUR conversion, export time-windowed CSV/JSON for your gestor); `onFulfilled(productKey \| '*', handler)` / `onRefunded(...)` register fire-and-forget hooks (purchase applied / admin-approved refund — a revenue ledger writes on one, reverses on the other). **Stars-only by design** — Telegram ToS §6.2 forbids third-party payment providers for digital goods. Crypto Pay deferred (MiCA risk); Stripe-outside-Telegram is a future v2 channel. Full compliance memo (Spanish-autónomo seller-of-record analysis, Verifactu vs Crea y Crece, MiCA, Art. 103(m) waiver text, GDPR retention) in `src/bot/payments/CLAUDE.md`. |
693
+ | `@adriangalilea/utils/bot/payments` | `botPayments({ session, storage, paysupport, paysupportHint?, legal, waiver, vip?, credits?, perks? })` — Telegram Stars monetization in one drop-in plugin. **Three axes, all optional:** `vip` (positional tier ladder — single rung in v1 is just `vip: [{...}]`, ladder is `vip: [{...}, {...}]`; ids are `vip.1`, `vip.2`, …), `credits` (consumable balance + top-up packs `credits.1`, `credits.2`, …), `perks` (orthogonal one-shot unlocks `perks.<key>`). **Surface:** `ctx.payments.atLeast('vip')` / `atLeast('vip.2')` (typed rank check), `ctx.payments.tier()` / `.tier.level()` / `.tier.label()`, `ctx.payments.credits.{balance, consume, tryConsume}` (throws `InsufficientCredits`), `ctx.payments.has(perkId)`, `await ctx.payments.require('vip', { feature? })` (gate that sends a localized upgrade prompt deep-linked to `/settings → 💎 VIP`), `await ctx.payments.invoice(productKey)` (threads Art. 103(m) TRLGDCU consent inline before `sendInvoice`). **Owns:** waiver consent flow (versioned text → forces re-consent on bump, snapshotted on every charge for audit), `/paysupport` slash command (Telegram ToS §6.5; `paysupportHint` overrides the where-to-manage-charges line when your menu isn't `/settings`), idempotent `successful_payment` fulfillment via `pay:idempotency:{chargeId}` sentinel, lazy subscription expiry (no cron needed), tier upgrade auto-cancel of the lower rung's renewal, and admin-DM refund approval (mirror of `accessControl`'s [✅ Aprobar][❌ Denegar] pattern). **Returns:** `{ plugin, menuItem, payouts, admin, onFulfilled, onRefunded }` — `menuItem` is the drop-in `💎 VIP` entry for `botMenu`; `admin.{listCharges, getCharge}` backs custom admin commands; `payouts.{record, list, export, exportForUsers}` is the Fragment payout ledger (you receive TON, log the EUR conversion, export time-windowed CSV/JSON for your gestor); `onFulfilled(productKey \| '*', handler)` / `onRefunded(...)` register fire-and-forget hooks (purchase applied / admin-approved refund — a revenue ledger writes on one, reverses on the other). **Stars-only by design** — Telegram ToS §6.2 forbids third-party payment providers for digital goods. Crypto Pay deferred (MiCA risk); Stripe-outside-Telegram is a future v2 channel. Full compliance memo (Spanish-autónomo seller-of-record analysis, Verifactu vs Crea y Crece, MiCA, Art. 103(m) waiver text, GDPR retention) in `src/bot/payments/CLAUDE.md`. |
706
694
  | `@adriangalilea/utils/bot/create` | `createBot<S>({ token?, storage?, initial?, admins?, language?, menu?, access?, payments?, handlers?, worker? })` — the composer (see "One bot file, ideation → production" above). Returns `{ build, session, poll, isMain, fetch }`: `poll()` long-polls, `export default app` is a complete Worker, `app.session(ctx)` / the `handlers` callback's `session(ctx)` is the TYPED accessor for your `S` fields. Owns storage+session wiring; resolves storage per environment (D1 binding → `bot/storage-d1`; `BOT_PERSIST` path → sqlite, `redis://` → redis, lazily-imported optional peers; else announced-ephemeral memory) — or pass `storage: (env) => Storage` when the choice is env-dependent (e.g. a D1 binding not named `DB`). Boot NARRATES the composition (`session: memory …`, `features: language(en,es) · menu(/settings)`) so every implicit decision is visible where you're looking. Runnable demo: `pnpm demo:bot`. |
707
695
  | `@adriangalilea/utils/bot/worker` | `botWorkerFetch(resolve)` — the Cloudflare Worker cap: secret-checked webhook (ack fast, work + storage flush ride `ctx.waitUntil`, errors DM admins throttled), `POST /setup` (webhook registration with handler-derived `allowed_updates` + a commit-narrating 🚀 deploy DM from the request body), `POST /deploy-started` (the 🛳 "what is shipping" DM — curl it from your deploy script on the still-live version, body `{sha, author, message, etaSeconds?}`), operator-authed `/pause` `/resume` `/webhook-status` (+ `statusExtra`), and a `routes` escape hatch tried before the built-ins. Structural bot type — no workers-types dependency. |
708
696
  | `@adriangalilea/utils/bot/flags` | `defineFlags(spec, { read, write? })` — feature flags declared ONCE in code, resolved LIVE from the bot's operator-config record (D1 json row, Redis hash — storage-agnostic, Worker-safe, zero deps). Each flag: `{ kind: 'bool'\|'number'\|'string', label, help?, default }`, where `default` is a scalar or a **tier map** `{ free, vip?, 'vip.N'? }` resolved off `ctx.payments.tier()` — "free users get this limit, premium users get that model" is one declaration, zero branching at the call site (ladder walks down: exact rung → lower rungs → `vip` → `free`). Read sites: `await flags.<key>(ctx)` — stored override wins over the code default, per-ctx reads coalesce into one config fetch. `flags.describe()` is the JSON schema admin panels render generically, so a new flag appears in every panel with zero panel edits; `flags.set(ctx, key, value)` writes a kind-checked live override (`null` clears it — RFC 7386 null-delete, matching SQLite `json_patch`); `flags.overrides(ctx)` lists what's overridden. Corrupt stored values scream (`SourcedError`); bad specs panic at construction. |
@@ -713,11 +701,13 @@ Standard wiring:
713
701
  ```typescript
714
702
  import { Bot } from 'gramio'
715
703
  import { redisStorage } from '@gramio/storage-redis'
716
- import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
704
+ import { adminContext } from '@adriangalilea/utils/bot/admin'
705
+ import { gracefulStart } from '@adriangalilea/utils/bot/kit'
717
706
  import { botSession } from '@adriangalilea/utils/bot/session'
718
707
  import { accessControl } from '@adriangalilea/utils/bot/access-control'
719
708
  import { createLlm } from '@adriangalilea/utils/llm'
720
709
  import { streamChatReply, llmHistory, toModelMessages } from '@adriangalilea/utils/bot/llm'
710
+ import { kev } from '@adriangalilea/utils'
721
711
 
722
712
  // Raw redis is fine — bot-id namespacing happens inside botSession +
723
713
  // every plugin via ctx.bot.info.id. Multiple bots sharing this Redis
@@ -733,7 +723,7 @@ const llm = createLlm({
733
723
  })
734
724
 
735
725
  const bot = new Bot(process.env.BOT_TOKEN!)
736
- .extend(adminContext({ adminId: 190202471 })) // KEV.TELEGRAM_ADMIN_ID overrides
726
+ .extend(adminContext(kev.int('TELEGRAM_ADMIN_ID', 190202471))) // or a live resolver
737
727
  .extend(userSession)
738
728
  .extend(accessControl({ session: userSession, storage, defaults: [] }))
739
729
  .extend(chat.plugin)
@@ -138,6 +138,7 @@
138
138
  import type { session } from "@gramio/session";
139
139
  import type { Storage } from "@gramio/storage";
140
140
  import { type AnyBot, type DeriveDefinitions, Plugin } from "gramio";
141
+ import { type LangSession } from "./lang.js";
141
142
  export type AccessStatus = "unknown" | "pending" | "approved" | "denied";
142
143
  export type AccessUser = {
143
144
  id: number;
@@ -230,8 +231,7 @@ export type AccessInfo = {
230
231
  */
231
232
  type SessionLike = {
232
233
  access?: AccessRecord;
233
- language?: string;
234
- };
234
+ } & LangSession;
235
235
  /** @internal — kept unexported so it doesn't clash with peers' refs. */
236
236
  type AcSessionPluginRef = ReturnType<typeof session<SessionLike, "session">>;
237
237
  export type AccessControlOptions = {
@@ -312,147 +312,205 @@ export declare const accessControl: (opts: AccessControlOptions) => Plugin<Recor
312
312
  global: AdminDerives & AccessDerives & SessionDerives;
313
313
  } & {
314
314
  message: {
315
- session: SessionLike & {
315
+ session: {
316
+ access?: AccessRecord;
317
+ } & LangSession & {
316
318
  $clear: () => Promise<void>;
317
319
  };
318
320
  };
319
321
  channel_post: {
320
- session: SessionLike & {
322
+ session: {
323
+ access?: AccessRecord;
324
+ } & LangSession & {
321
325
  $clear: () => Promise<void>;
322
326
  };
323
327
  };
324
328
  inline_query: {
325
- session: SessionLike & {
329
+ session: {
330
+ access?: AccessRecord;
331
+ } & LangSession & {
326
332
  $clear: () => Promise<void>;
327
333
  };
328
334
  };
329
335
  chosen_inline_result: {
330
- session: SessionLike & {
336
+ session: {
337
+ access?: AccessRecord;
338
+ } & LangSession & {
331
339
  $clear: () => Promise<void>;
332
340
  };
333
341
  };
334
342
  callback_query: {
335
- session: SessionLike & {
343
+ session: {
344
+ access?: AccessRecord;
345
+ } & LangSession & {
336
346
  $clear: () => Promise<void>;
337
347
  };
338
348
  };
339
349
  shipping_query: {
340
- session: SessionLike & {
350
+ session: {
351
+ access?: AccessRecord;
352
+ } & LangSession & {
341
353
  $clear: () => Promise<void>;
342
354
  };
343
355
  };
344
356
  pre_checkout_query: {
345
- session: SessionLike & {
357
+ session: {
358
+ access?: AccessRecord;
359
+ } & LangSession & {
346
360
  $clear: () => Promise<void>;
347
361
  };
348
362
  };
349
363
  poll_answer: {
350
- session: SessionLike & {
364
+ session: {
365
+ access?: AccessRecord;
366
+ } & LangSession & {
351
367
  $clear: () => Promise<void>;
352
368
  };
353
369
  };
354
370
  chat_join_request: {
355
- session: SessionLike & {
371
+ session: {
372
+ access?: AccessRecord;
373
+ } & LangSession & {
356
374
  $clear: () => Promise<void>;
357
375
  };
358
376
  };
359
377
  new_chat_members: {
360
- session: SessionLike & {
378
+ session: {
379
+ access?: AccessRecord;
380
+ } & LangSession & {
361
381
  $clear: () => Promise<void>;
362
382
  };
363
383
  };
364
384
  new_chat_title: {
365
- session: SessionLike & {
385
+ session: {
386
+ access?: AccessRecord;
387
+ } & LangSession & {
366
388
  $clear: () => Promise<void>;
367
389
  };
368
390
  };
369
391
  new_chat_photo: {
370
- session: SessionLike & {
392
+ session: {
393
+ access?: AccessRecord;
394
+ } & LangSession & {
371
395
  $clear: () => Promise<void>;
372
396
  };
373
397
  };
374
398
  delete_chat_photo: {
375
- session: SessionLike & {
399
+ session: {
400
+ access?: AccessRecord;
401
+ } & LangSession & {
376
402
  $clear: () => Promise<void>;
377
403
  };
378
404
  };
379
405
  group_chat_created: {
380
- session: SessionLike & {
406
+ session: {
407
+ access?: AccessRecord;
408
+ } & LangSession & {
381
409
  $clear: () => Promise<void>;
382
410
  };
383
411
  };
384
412
  message_auto_delete_timer_changed: {
385
- session: SessionLike & {
413
+ session: {
414
+ access?: AccessRecord;
415
+ } & LangSession & {
386
416
  $clear: () => Promise<void>;
387
417
  };
388
418
  };
389
419
  migrate_to_chat_id: {
390
- session: SessionLike & {
420
+ session: {
421
+ access?: AccessRecord;
422
+ } & LangSession & {
391
423
  $clear: () => Promise<void>;
392
424
  };
393
425
  };
394
426
  migrate_from_chat_id: {
395
- session: SessionLike & {
427
+ session: {
428
+ access?: AccessRecord;
429
+ } & LangSession & {
396
430
  $clear: () => Promise<void>;
397
431
  };
398
432
  };
399
433
  pinned_message: {
400
- session: SessionLike & {
434
+ session: {
435
+ access?: AccessRecord;
436
+ } & LangSession & {
401
437
  $clear: () => Promise<void>;
402
438
  };
403
439
  };
404
440
  invoice: {
405
- session: SessionLike & {
441
+ session: {
442
+ access?: AccessRecord;
443
+ } & LangSession & {
406
444
  $clear: () => Promise<void>;
407
445
  };
408
446
  };
409
447
  successful_payment: {
410
- session: SessionLike & {
448
+ session: {
449
+ access?: AccessRecord;
450
+ } & LangSession & {
411
451
  $clear: () => Promise<void>;
412
452
  };
413
453
  };
414
454
  chat_shared: {
415
- session: SessionLike & {
455
+ session: {
456
+ access?: AccessRecord;
457
+ } & LangSession & {
416
458
  $clear: () => Promise<void>;
417
459
  };
418
460
  };
419
461
  proximity_alert_triggered: {
420
- session: SessionLike & {
462
+ session: {
463
+ access?: AccessRecord;
464
+ } & LangSession & {
421
465
  $clear: () => Promise<void>;
422
466
  };
423
467
  };
424
468
  video_chat_scheduled: {
425
- session: SessionLike & {
469
+ session: {
470
+ access?: AccessRecord;
471
+ } & LangSession & {
426
472
  $clear: () => Promise<void>;
427
473
  };
428
474
  };
429
475
  video_chat_started: {
430
- session: SessionLike & {
476
+ session: {
477
+ access?: AccessRecord;
478
+ } & LangSession & {
431
479
  $clear: () => Promise<void>;
432
480
  };
433
481
  };
434
482
  video_chat_ended: {
435
- session: SessionLike & {
483
+ session: {
484
+ access?: AccessRecord;
485
+ } & LangSession & {
436
486
  $clear: () => Promise<void>;
437
487
  };
438
488
  };
439
489
  video_chat_participants_invited: {
440
- session: SessionLike & {
490
+ session: {
491
+ access?: AccessRecord;
492
+ } & LangSession & {
441
493
  $clear: () => Promise<void>;
442
494
  };
443
495
  };
444
496
  web_app_data: {
445
- session: SessionLike & {
497
+ session: {
498
+ access?: AccessRecord;
499
+ } & LangSession & {
446
500
  $clear: () => Promise<void>;
447
501
  };
448
502
  };
449
503
  location: {
450
- session: SessionLike & {
504
+ session: {
505
+ access?: AccessRecord;
506
+ } & LangSession & {
451
507
  $clear: () => Promise<void>;
452
508
  };
453
509
  };
454
510
  passport_data: {
455
- session: SessionLike & {
511
+ session: {
512
+ access?: AccessRecord;
513
+ } & LangSession & {
456
514
  $clear: () => Promise<void>;
457
515
  };
458
516
  };
@@ -1 +1 @@
1
- {"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/bot/access-control.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/bot/access-control.ts"],"names":[],"mappings":"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"}
@@ -137,10 +137,11 @@
137
137
  */
138
138
  import { CallbackData, InlineKeyboard, Plugin, } from "gramio";
139
139
  import { say } from "../say/index.js";
140
+ import { coarseSpan } from "../universal/time.js";
140
141
  import { botStorageKey, botSubKey } from "./keys.js";
142
+ import { ctxLang, langOfUser, loadFullRecord, } from "./lang.js";
141
143
  const FIRST_MSG_LIMIT = 200;
142
144
  const DEFAULT_THROTTLE_MS = 6 * 60 * 60 * 1000;
143
- const FALLBACK_LANG = "en";
144
145
  // ─── callback schemas ──────────────────────────────────────────────
145
146
  //
146
147
  // Short `nameId`s keep callback_data under Telegram's 64-byte cap.
@@ -172,18 +173,6 @@ const formatUser = (u, fallbackId) => {
172
173
  const handle = u.username ? `@${u.username}` : `id ${u.id}`;
173
174
  return `${name} (${handle})`;
174
175
  };
175
- const fmtAge = (ms) => {
176
- const s = Math.floor(ms / 1000);
177
- if (s < 60)
178
- return `${s}s`;
179
- const m = Math.floor(s / 60);
180
- if (m < 60)
181
- return `${m}min`;
182
- const h = Math.floor(m / 60);
183
- if (h < 24)
184
- return `${h}h`;
185
- return `${Math.floor(h / 24)}d`;
186
- };
187
176
  const requestNotificationText = (uid, r, repeat, lang) => {
188
177
  const parts = [
189
178
  say(repeat
@@ -192,7 +181,7 @@ const requestNotificationText = (uid, r, repeat, lang) => {
192
181
  "",
193
182
  `👤 ${formatUser(r.user, uid)}`,
194
183
  `🆔 ${uid}`,
195
- `⏰ ${say({ en: "ago", es: "hace" }, lang)} ${fmtAge(Date.now() - (r.requestedAt ?? Date.now()))}`,
184
+ `⏰ ${say({ en: "ago", es: "hace" }, lang)} ${coarseSpan(Date.now() - (r.requestedAt ?? Date.now()))}`,
196
185
  ];
197
186
  if (repeat) {
198
187
  parts.push(`🔁 ${say({ en: "attempts", es: "intentos" }, lang)}: ${(r.rejectedAttempts ?? 0) + 1}`);
@@ -253,29 +242,22 @@ const indexRemove = async (storage, key, uid) => {
253
242
  }
254
243
  await saveIndex(storage, key, idx);
255
244
  };
256
- const loadFullRecord = async (storage, ctx, userId) => (await storage.get(botStorageKey(ctx, userId))) ?? {};
245
+ const loadRecord = (storage, ctx, userId) => loadFullRecord(storage, ctx, userId);
257
246
  const saveAccess = async (storage, ctx, userId, rec) => {
258
- const full = await loadFullRecord(storage, ctx, userId);
247
+ const full = await loadRecord(storage, ctx, userId);
259
248
  full.access = rec;
260
249
  await storage.set(botStorageKey(ctx, userId), full);
261
250
  };
262
251
  const loadAccess = async (storage, ctx, userId) => {
263
- const full = await loadFullRecord(storage, ctx, userId);
252
+ const full = await loadRecord(storage, ctx, userId);
264
253
  return full.access;
265
254
  };
266
255
  /** Forget the access judgment on a user (other plugins' fields survive). */
267
256
  const dropAccess = async (storage, ctx, userId) => {
268
- const full = await loadFullRecord(storage, ctx, userId);
257
+ const full = await loadRecord(storage, ctx, userId);
269
258
  delete full.access;
270
259
  await storage.set(botStorageKey(ctx, userId), full);
271
260
  };
272
- /** Read recipient's stored language (set by bot/language); fallback to en. */
273
- const langOfUser = async (storage, ctx, userId) => {
274
- const full = await loadFullRecord(storage, ctx, userId);
275
- return full.language ?? FALLBACK_LANG;
276
- };
277
- /** Read current ctx's lang. */
278
- const ctxLang = (ctx) => ctx.session.language ?? FALLBACK_LANG;
279
261
  // Resolve the caller's lang for admins; for non-admins send the standard
280
262
  // rejection alert and return null so the caller bails. Shares one check
281
263
  // across the five admin callback handlers.
@@ -333,7 +315,7 @@ const groupRequestText = (rec, lang) => {
333
315
  ];
334
316
  if (rec.addedBy)
335
317
  parts.push(`👤 ${formatUser(rec.addedBy, rec.addedBy.id)}`);
336
- parts.push(`⏰ ${say({ en: "ago", es: "hace" }, lang)} ${fmtAge(Date.now() - (rec.requestedAt ?? Date.now()))}`);
318
+ parts.push(`⏰ ${say({ en: "ago", es: "hace" }, lang)} ${coarseSpan(Date.now() - (rec.requestedAt ?? Date.now()))}`);
337
319
  return parts.join("\n");
338
320
  };
339
321
  const groupRequestKeyboard = (chatId, lang) => new InlineKeyboard()
@@ -1142,7 +1124,7 @@ const listView = async (storage, ctx, idx, gIdx, filter, defaults, lang) => {
1142
1124
  continue;
1143
1125
  }
1144
1126
  const ageRef = rec.approvedAt ?? rec.deniedAt ?? rec.requestedAt ?? Date.now();
1145
- lines.push(`${n}. 👤 ${formatUser(rec.user, id)} · ${ago} ${fmtAge(Date.now() - ageRef)}` +
1127
+ lines.push(`${n}. 👤 ${formatUser(rec.user, id)} · ${ago} ${coarseSpan(Date.now() - ageRef)}` +
1146
1128
  (rec.messageCount ? ` · ${rec.messageCount} msgs` : ""));
1147
1129
  if (filter === "pending") {
1148
1130
  keyboard
@@ -1169,7 +1151,7 @@ const listView = async (storage, ctx, idx, gIdx, filter, defaults, lang) => {
1169
1151
  n += 1;
1170
1152
  const rec = await loadGroup(storage, ctx, id);
1171
1153
  const ageRef = rec?.approvedAt ?? rec?.deniedAt ?? rec?.requestedAt ?? Date.now();
1172
- lines.push(`${n}. 👥 ${formatGroup(rec, id)} · ${ago} ${fmtAge(Date.now() - ageRef)}`);
1154
+ lines.push(`${n}. 👥 ${formatGroup(rec, id)} · ${ago} ${coarseSpan(Date.now() - ageRef)}`);
1173
1155
  if (filter === "pending") {
1174
1156
  keyboard
1175
1157
  .text(`✅ ${n}`, acGroupApprove.pack({ gid: id, v: "pending" }), {