responses-proxy 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 (161) hide show
  1. package/README.md +56 -0
  2. package/cli.js +118 -0
  3. package/dist/anthropic-messages.js +383 -0
  4. package/dist/anthropic-messages.test.js +209 -0
  5. package/dist/audit-log.js +138 -0
  6. package/dist/audit-log.test.js +480 -0
  7. package/dist/billing-expiration.js +70 -0
  8. package/dist/billing-expiration.test.js +114 -0
  9. package/dist/billing.js +716 -0
  10. package/dist/billing.test.js +228 -0
  11. package/dist/chatgpt-oauth-store.js +240 -0
  12. package/dist/chatgpt-oauth-store.test.js +88 -0
  13. package/dist/chatgpt-oauth.js +118 -0
  14. package/dist/chatgpt-oauth.test.js +63 -0
  15. package/dist/chatgpt-provider-auth.js +60 -0
  16. package/dist/chatgpt-provider-auth.test.js +101 -0
  17. package/dist/client/app-icon.svg +17 -0
  18. package/dist/client/assets/index-C7Vvhst8.js +14 -0
  19. package/dist/client/assets/index-DpqgYK3L.css +1 -0
  20. package/dist/client/favicon.svg +17 -0
  21. package/dist/client/index.html +31 -0
  22. package/dist/client-config-apply.js +345 -0
  23. package/dist/client-config-apply.test.js +185 -0
  24. package/dist/client-token-limits.js +111 -0
  25. package/dist/client-token-limits.test.js +129 -0
  26. package/dist/codex-config.js +47 -0
  27. package/dist/codex-setup.js +87 -0
  28. package/dist/codex-setup.test.js +30 -0
  29. package/dist/config.js +314 -0
  30. package/dist/cost-analytics.js +31 -0
  31. package/dist/cost-analytics.test.js +38 -0
  32. package/dist/customer-key-access.js +126 -0
  33. package/dist/customer-key-access.test.js +178 -0
  34. package/dist/customer-keys.js +209 -0
  35. package/dist/customer-keys.test.js +68 -0
  36. package/dist/customer-usage.js +18 -0
  37. package/dist/customer-usage.test.js +55 -0
  38. package/dist/dashboard-auth.js +318 -0
  39. package/dist/dashboard-auth.test.js +133 -0
  40. package/dist/dashboard-serving.test.js +235 -0
  41. package/dist/error-response.js +174 -0
  42. package/dist/error-response.test.js +88 -0
  43. package/dist/forward.js +357 -0
  44. package/dist/health-websocket-manager.js +174 -0
  45. package/dist/http-rate-limit.js +36 -0
  46. package/dist/http-rate-limit.test.js +62 -0
  47. package/dist/kiro-auth.js +136 -0
  48. package/dist/kiro-auth.test.js +234 -0
  49. package/dist/kiro-codewhisperer.js +646 -0
  50. package/dist/kiro-codewhisperer.test.js +219 -0
  51. package/dist/kiro-device-login.js +338 -0
  52. package/dist/kiro-eventstream.js +219 -0
  53. package/dist/kiro-eventstream.test.js +79 -0
  54. package/dist/kiro-forward.js +401 -0
  55. package/dist/kiro-import-cli.js +69 -0
  56. package/dist/kiro-import.js +94 -0
  57. package/dist/kiro-import.test.js +125 -0
  58. package/dist/kiro-token-store.js +196 -0
  59. package/dist/kiro-token-store.test.js +207 -0
  60. package/dist/krouter-usage.js +243 -0
  61. package/dist/model-combo-repository.js +147 -0
  62. package/dist/model-routing.js +69 -0
  63. package/dist/model-routing.test.js +41 -0
  64. package/dist/normalize-request.js +531 -0
  65. package/dist/normalize-request.test.js +277 -0
  66. package/dist/omv-public-firewall.test.js +11 -0
  67. package/dist/package.json +17 -0
  68. package/dist/prompt-cache-state.js +146 -0
  69. package/dist/prompt-cache-state.test.js +71 -0
  70. package/dist/prompt-cache.js +229 -0
  71. package/dist/provider-health-service.js +404 -0
  72. package/dist/provider-request-parameters.js +107 -0
  73. package/dist/provider-request-parameters.test.js +26 -0
  74. package/dist/provider-routing.js +114 -0
  75. package/dist/provider-routing.test.js +64 -0
  76. package/dist/provider-usage.js +314 -0
  77. package/dist/request-timeout-policy.js +61 -0
  78. package/dist/request-timeout-policy.test.js +40 -0
  79. package/dist/response-cache.js +69 -0
  80. package/dist/response-cache.test.js +28 -0
  81. package/dist/routing-combo-repository.js +300 -0
  82. package/dist/routing-engine.js +377 -0
  83. package/dist/routing-integration.js +155 -0
  84. package/dist/routing-simulation-engine.js +326 -0
  85. package/dist/rtk-layer.js +483 -0
  86. package/dist/rtk-layer.test.js +198 -0
  87. package/dist/runtime-provider-repository.js +1742 -0
  88. package/dist/runtime-provider-repository.test.js +1177 -0
  89. package/dist/schema.js +118 -0
  90. package/dist/schema.test.js +16 -0
  91. package/dist/sepay-webhook.js +87 -0
  92. package/dist/sepay-webhook.test.js +142 -0
  93. package/dist/server-body-limit.test.js +35 -0
  94. package/dist/server-client-token-limits.test.js +161 -0
  95. package/dist/server-codex-config-setup.test.js +76 -0
  96. package/dist/server-http-rate-limit.test.js +80 -0
  97. package/dist/server-response-cache.test.js +105 -0
  98. package/dist/server-routes-alias.test.js +39 -0
  99. package/dist/server-sepay-webhook-security.test.js +59 -0
  100. package/dist/server.js +5906 -0
  101. package/dist/session-log.js +178 -0
  102. package/dist/tailnet-funnel-script.test.js +33 -0
  103. package/dist/telegram-bot/actions.js +118 -0
  104. package/dist/telegram-bot/admin-actions.js +103 -0
  105. package/dist/telegram-bot/auth.js +46 -0
  106. package/dist/telegram-bot/auth.test.js +1 -0
  107. package/dist/telegram-bot/bot-identity-repository.js +189 -0
  108. package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
  109. package/dist/telegram-bot/callbacks.js +30 -0
  110. package/dist/telegram-bot/codex-config-delivery.js +38 -0
  111. package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
  112. package/dist/telegram-bot/commands/accounts.js +140 -0
  113. package/dist/telegram-bot/commands/apikey.js +737 -0
  114. package/dist/telegram-bot/commands/apply.js +265 -0
  115. package/dist/telegram-bot/commands/clients.js +13 -0
  116. package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
  117. package/dist/telegram-bot/commands/grant.js +138 -0
  118. package/dist/telegram-bot/commands/grant.test.js +217 -0
  119. package/dist/telegram-bot/commands/help.js +52 -0
  120. package/dist/telegram-bot/commands/me.js +53 -0
  121. package/dist/telegram-bot/commands/models.js +6 -0
  122. package/dist/telegram-bot/commands/oauth.js +64 -0
  123. package/dist/telegram-bot/commands/plans.js +96 -0
  124. package/dist/telegram-bot/commands/providers.js +27 -0
  125. package/dist/telegram-bot/commands/quota.js +10 -0
  126. package/dist/telegram-bot/commands/renew-user.js +139 -0
  127. package/dist/telegram-bot/commands/renew-user.test.js +184 -0
  128. package/dist/telegram-bot/commands/renew.js +1369 -0
  129. package/dist/telegram-bot/commands/renew.test.js +1633 -0
  130. package/dist/telegram-bot/commands/start.js +212 -0
  131. package/dist/telegram-bot/commands/start.test.js +280 -0
  132. package/dist/telegram-bot/commands/status.js +6 -0
  133. package/dist/telegram-bot/commands/tailscale.js +15 -0
  134. package/dist/telegram-bot/commands/tailscale.test.js +76 -0
  135. package/dist/telegram-bot/commands/test.js +51 -0
  136. package/dist/telegram-bot/commands/test.test.js +14 -0
  137. package/dist/telegram-bot/commands/usage.js +10 -0
  138. package/dist/telegram-bot/config.js +98 -0
  139. package/dist/telegram-bot/config.test.js +42 -0
  140. package/dist/telegram-bot/customer-actions.js +160 -0
  141. package/dist/telegram-bot/customer-api-keys.js +68 -0
  142. package/dist/telegram-bot/customer-billing.js +72 -0
  143. package/dist/telegram-bot/customer-workspace-repository.js +134 -0
  144. package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
  145. package/dist/telegram-bot/dashboard-login.js +39 -0
  146. package/dist/telegram-bot/format.js +140 -0
  147. package/dist/telegram-bot/grants.js +370 -0
  148. package/dist/telegram-bot/grants.test.js +290 -0
  149. package/dist/telegram-bot/index.js +85 -0
  150. package/dist/telegram-bot/message-cleanup.js +55 -0
  151. package/dist/telegram-bot/message-cleanup.test.js +77 -0
  152. package/dist/telegram-bot/message-format.js +45 -0
  153. package/dist/telegram-bot/message-format.test.js +10 -0
  154. package/dist/telegram-bot/proxy-client.js +174 -0
  155. package/dist/telegram-bot/rate-limit.js +95 -0
  156. package/dist/telegram-bot/rate-limit.test.js +58 -0
  157. package/dist/telegram-bot/sessions.js +171 -0
  158. package/dist/telegram-bot/sessions.test.js +107 -0
  159. package/dist/telegram-bot/telegram-adapter.js +126 -0
  160. package/dist/telegram-bot/worker.js +63 -0
  161. package/package.json +39 -0
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ function parseIdList(value) {
3
+ return (value ?? "")
4
+ .split(/[,\r?\n]+/g)
5
+ .map((entry) => entry.trim())
6
+ .filter(Boolean);
7
+ }
8
+ function normalizeBaseUrl(value) {
9
+ const trimmed = value.trim().replace(/\/+$/, "");
10
+ if (trimmed.endsWith("/v1")) {
11
+ return trimmed.slice(0, -3);
12
+ }
13
+ return trimmed;
14
+ }
15
+ const envBoolean = (defaultValue) => z
16
+ .string()
17
+ .optional()
18
+ .transform((value) => {
19
+ const normalized = value?.trim().toLowerCase();
20
+ if (normalized === undefined || normalized === "") {
21
+ return defaultValue;
22
+ }
23
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
24
+ });
25
+ const envSchema = z.object({
26
+ TELEGRAM_BOT_TOKEN: z.string().min(1),
27
+ TELEGRAM_ALLOWED_USER_IDS: z.string().optional(),
28
+ TELEGRAM_ALLOWED_CHAT_IDS: z.string().optional(),
29
+ TELEGRAM_OWNER_USER_IDS: z.string().optional(),
30
+ TELEGRAM_ADMIN_USER_IDS: z.string().optional(),
31
+ // Deprecated. Customers should be managed in the bot database, not in env.
32
+ TELEGRAM_CUSTOMER_USER_IDS: z.string().optional(),
33
+ TELEGRAM_BOT_MODE: z.enum(["polling", "webhook"]).default("polling"),
34
+ TELEGRAM_WEBHOOK_URL: z.string().optional(),
35
+ TELEGRAM_WEBHOOK_SECRET: z.string().optional(),
36
+ RESPONSES_PROXY_ADMIN_BASE_URL: z
37
+ .string()
38
+ .default("http://127.0.0.1:8318")
39
+ .transform(normalizeBaseUrl),
40
+ RESPONSES_PROXY_CLIENT_API_KEY: z.string().optional(),
41
+ RESPONSES_PROXY_DEFAULT_MODEL: z.string().default("gpt-5.5"),
42
+ BOT_PUBLIC_SIGNUP_ENABLED: envBoolean(false),
43
+ BOT_REQUIRE_ADMIN_APPROVAL: envBoolean(true),
44
+ BOT_DEFAULT_CUSTOMER_ROUTE: z.string().default("customers"),
45
+ BOT_PUBLIC_RESPONSES_BASE_URL: z
46
+ .string()
47
+ .default("http://127.0.0.1:8318/v1")
48
+ .transform((value) => value.trim().replace(/\/+$/, "")),
49
+ BOT_PROXY_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
50
+ BOT_SESSION_DB_PATH: z.string().default("./logs/telegram-bot.sqlite"),
51
+ BOT_SESSION_TTL_MS: z.coerce.number().int().positive().default(15 * 60 * 1000),
52
+ BOT_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60 * 1000),
53
+ BOT_RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().positive().default(12),
54
+ BOT_LOG_LEVEL: z
55
+ .enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"])
56
+ .default("info"),
57
+ BOT_SEPAY_ACCOUNT_NUMBER: z.string().optional(),
58
+ BOT_SEPAY_BANK_CODE: z.string().default("MBBank"),
59
+ BOT_SEPAY_TEMPLATE: z.string().default("compact"),
60
+ BOT_SEPAY_DOWNLOAD: envBoolean(false),
61
+ });
62
+ export function readTelegramBotConfig(env) {
63
+ const parsed = envSchema.parse(env);
64
+ return {
65
+ telegramBotToken: parsed.TELEGRAM_BOT_TOKEN,
66
+ allowedUserIds: new Set(parseIdList(parsed.TELEGRAM_ALLOWED_USER_IDS)),
67
+ allowedChatIds: new Set(parseIdList(parsed.TELEGRAM_ALLOWED_CHAT_IDS)),
68
+ ownerUserIds: new Set(parseIdList(parsed.TELEGRAM_OWNER_USER_IDS)),
69
+ adminUserIds: new Set(parseIdList(parsed.TELEGRAM_ADMIN_USER_IDS)),
70
+ botMode: parsed.TELEGRAM_BOT_MODE,
71
+ webhookUrl: parsed.TELEGRAM_WEBHOOK_URL?.trim() || undefined,
72
+ webhookSecret: parsed.TELEGRAM_WEBHOOK_SECRET?.trim() || undefined,
73
+ proxyAdminBaseUrl: parsed.RESPONSES_PROXY_ADMIN_BASE_URL,
74
+ proxyClientApiKey: parsed.RESPONSES_PROXY_CLIENT_API_KEY?.trim() || undefined,
75
+ defaultModel: parsed.RESPONSES_PROXY_DEFAULT_MODEL.trim(),
76
+ publicSignupEnabled: parsed.BOT_PUBLIC_SIGNUP_ENABLED,
77
+ requireAdminApproval: parsed.BOT_REQUIRE_ADMIN_APPROVAL,
78
+ defaultCustomerRoute: normalizeRouteKey(parsed.BOT_DEFAULT_CUSTOMER_ROUTE),
79
+ publicResponsesBaseUrl: parsed.BOT_PUBLIC_RESPONSES_BASE_URL,
80
+ proxyRequestTimeoutMs: parsed.BOT_PROXY_REQUEST_TIMEOUT_MS,
81
+ sessionDbPath: parsed.BOT_SESSION_DB_PATH.trim(),
82
+ sessionTtlMs: parsed.BOT_SESSION_TTL_MS,
83
+ rateLimitWindowMs: parsed.BOT_RATE_LIMIT_WINDOW_MS,
84
+ rateLimitMaxRequests: parsed.BOT_RATE_LIMIT_MAX_REQUESTS,
85
+ logLevel: parsed.BOT_LOG_LEVEL,
86
+ sepayAccountNumber: parsed.BOT_SEPAY_ACCOUNT_NUMBER?.trim() || undefined,
87
+ sepayBankCode: parsed.BOT_SEPAY_BANK_CODE.trim() || "MBBank",
88
+ sepayTemplate: parsed.BOT_SEPAY_TEMPLATE.trim() || "compact",
89
+ sepayDownload: parsed.BOT_SEPAY_DOWNLOAD,
90
+ };
91
+ }
92
+ function normalizeRouteKey(value) {
93
+ return (value
94
+ .trim()
95
+ .toLowerCase()
96
+ .replace(/[^a-z0-9_-]+/g, "-")
97
+ .replace(/^-+|-+$/g, "") || "customers");
98
+ }
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { readTelegramBotConfig } from "./config.js";
4
+ test("readTelegramBotConfig supports legacy allowlist env", () => {
5
+ const config = readTelegramBotConfig({
6
+ TELEGRAM_BOT_TOKEN: "bot-token",
7
+ TELEGRAM_ALLOWED_USER_IDS: "123,456",
8
+ TELEGRAM_ADMIN_USER_IDS: "123",
9
+ });
10
+ assert.equal(config.telegramBotToken, "bot-token");
11
+ assert.deepEqual([...config.allowedUserIds], ["123", "456"]);
12
+ assert.deepEqual([...config.adminUserIds], ["123"]);
13
+ assert.equal(config.publicSignupEnabled, false);
14
+ assert.equal(config.requireAdminApproval, true);
15
+ });
16
+ test("readTelegramBotConfig supports public bot env without customer allowlist", () => {
17
+ const config = readTelegramBotConfig({
18
+ TELEGRAM_BOT_TOKEN: "bot-token",
19
+ TELEGRAM_OWNER_USER_IDS: "1283361952",
20
+ BOT_PUBLIC_SIGNUP_ENABLED: "true",
21
+ BOT_REQUIRE_ADMIN_APPROVAL: "false",
22
+ BOT_DEFAULT_CUSTOMER_ROUTE: "Paid Customers",
23
+ BOT_PUBLIC_RESPONSES_BASE_URL: "https://proxy.example.com/v1/",
24
+ });
25
+ assert.deepEqual([...config.allowedUserIds], []);
26
+ assert.deepEqual([...config.ownerUserIds], ["1283361952"]);
27
+ assert.equal(config.publicSignupEnabled, true);
28
+ assert.equal(config.requireAdminApproval, false);
29
+ assert.equal(config.defaultCustomerRoute, "paid-customers");
30
+ assert.equal(config.publicResponsesBaseUrl, "https://proxy.example.com/v1");
31
+ });
32
+ test("readTelegramBotConfig ignores deprecated customer env allowlist", () => {
33
+ const config = readTelegramBotConfig({
34
+ TELEGRAM_BOT_TOKEN: "bot-token",
35
+ TELEGRAM_OWNER_USER_IDS: "1283361952",
36
+ TELEGRAM_CUSTOMER_USER_IDS: "42,43",
37
+ BOT_PUBLIC_SIGNUP_ENABLED: "true",
38
+ });
39
+ assert.deepEqual([...config.ownerUserIds], ["1283361952"]);
40
+ assert.deepEqual([...config.allowedUserIds], []);
41
+ assert.equal(config.publicSignupEnabled, true);
42
+ });
@@ -0,0 +1,160 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { answerCallbackQuerySafely, replyOrEditMessage } from "./callbacks.js";
3
+ import { readCustomerBillingOverview } from "./customer-billing.js";
4
+ import { formatDateTime, formatField, formatMessage, formatSection } from "./message-format.js";
5
+ export function buildCustomerActionKeyboard(hasActiveKey) {
6
+ const keyboard = new InlineKeyboard()
7
+ .text("🔐 View key", "v1:customer:key")
8
+ .text("📊 Usage", "v1:customer:usage")
9
+ .row()
10
+ .text("🧾 Quota", "v1:customer:quota")
11
+ .text(hasActiveKey ? "⏱ Renew 24h" : "💳 Buy API key", "v1:renew:open");
12
+ if (hasActiveKey) {
13
+ keyboard.row().text("➕ Buy tokens", "v1:topup:open");
14
+ }
15
+ return keyboard.row().text("🔄 Refresh", "v1:customer:dashboard");
16
+ }
17
+ export function registerCustomerActionCallbacks(bot, workspaces, customerKeys, billing, auditLog) {
18
+ bot.callbackQuery(/^v1:customer:(dashboard|key|usage|quota)$/, async (ctx) => {
19
+ const view = ctx.match[1];
20
+ await answerCallbackQuerySafely(ctx, { text: view === "dashboard" ? "Refreshed" : "Loaded" });
21
+ await replyWithCustomerView(ctx, view, workspaces, customerKeys, billing, auditLog);
22
+ });
23
+ }
24
+ export async function renderCustomerActionText(ctx, text, hasActiveKey) {
25
+ await replyOrEditMessage(ctx, text, {
26
+ reply_markup: buildCustomerActionKeyboard(hasActiveKey),
27
+ });
28
+ }
29
+ export async function replyWithCustomerView(ctx, view, workspaces, customerKeys, billing, auditLog) {
30
+ if (ctx.chat?.type !== "private") {
31
+ await replyOrEditMessage(ctx, "For safety, open a private chat with this bot.");
32
+ return;
33
+ }
34
+ const userId = ctx.from?.id?.toString();
35
+ if (!userId) {
36
+ await replyOrEditMessage(ctx, "Could not determine your Telegram user.");
37
+ return;
38
+ }
39
+ const overview = readCustomerBillingOverview({
40
+ telegramUserId: userId,
41
+ workspaces,
42
+ customerKeys,
43
+ billing,
44
+ });
45
+ if (!overview.workspace) {
46
+ await renderCustomerActionText(ctx, formatMessage("⚠️ Workspace not ready", ["No customer workspace has been assigned to your Telegram user yet."]), false);
47
+ return;
48
+ }
49
+ await renderCustomerActionText(ctx, formatCustomerView(view, userId, overview, customerKeys, auditLog), overview.apiKey?.status === "active");
50
+ }
51
+ function formatCustomerView(view, userId, overview, customerKeys, auditLog) {
52
+ if (view === "key") {
53
+ const apiKey = overview.apiKey ? customerKeys.getApiKeySecret(overview.apiKey.id) : undefined;
54
+ if (apiKey && overview.apiKey) {
55
+ auditLog?.record({
56
+ event: "api_key.revealed",
57
+ actor: { type: "customer", id: userId },
58
+ subjectType: "customer_api_key",
59
+ subjectId: overview.apiKey.id,
60
+ metadata: {
61
+ telegramUserId: userId,
62
+ workspaceId: overview.apiKey.workspaceId,
63
+ keyPreview: overview.apiKey.apiKeyPreview,
64
+ audience: "customer_action_key",
65
+ apiKey,
66
+ },
67
+ });
68
+ }
69
+ return formatCustomerMessage("🔐 Your API key", [
70
+ formatWorkspaceSection(overview),
71
+ formatSection("API key", [
72
+ overview.apiKey ? formatField("Status", formatStatus(overview.apiKey.status)) : formatField("Status", "none"),
73
+ overview.apiKey ? formatField("Preview", overview.apiKey.apiKeyPreview) : undefined,
74
+ formatField("Client route", overview.workspace?.defaultClientRoute ?? overview.apiKey?.clientRoute ?? "none"),
75
+ ]),
76
+ formatSection("Copy value", [
77
+ apiKey ? `api_key: ${apiKey}` : undefined,
78
+ overview.apiKey && !apiKey ? "Full key: unavailable for legacy key" : undefined,
79
+ ]),
80
+ ]);
81
+ }
82
+ if (view === "usage") {
83
+ const lotLines = formatTokenLotLines(overview);
84
+ return formatCustomerMessage("📊 Usage", [
85
+ formatWorkspaceSection(overview),
86
+ formatSection("Entitlement", [
87
+ formatField("Status", formatStatus(overview.entitlementStatus)),
88
+ overview.entitlement ? formatField("Period start", formatDateTime(overview.entitlement.validFrom)) : undefined,
89
+ overview.entitlement ? formatField("Period end", formatDateTime(overview.entitlement.validUntil)) : undefined,
90
+ ]),
91
+ formatUsageSection(overview),
92
+ lotLines.length > 0 ? formatSection("Token lots", lotLines) : undefined,
93
+ formatApiKeySummarySection(overview),
94
+ ]);
95
+ }
96
+ if (view === "quota") {
97
+ const lotLines = formatTokenLotLines(overview);
98
+ return formatCustomerMessage("🧾 Quota", [
99
+ formatWorkspaceSection(overview),
100
+ formatSection("Entitlement", [
101
+ formatField("Status", formatStatus(overview.entitlementStatus)),
102
+ overview.entitlement ? formatField("Expires at", formatDateTime(overview.entitlement.validUntil)) : undefined,
103
+ ]),
104
+ formatUsageSection(overview),
105
+ lotLines.length > 0 ? formatSection("Token lots", lotLines) : undefined,
106
+ formatApiKeySummarySection(overview),
107
+ ]);
108
+ }
109
+ return formatCustomerMessage("🏠 Dashboard", [
110
+ formatWorkspaceSection(overview),
111
+ formatSection("Access", [
112
+ formatField("Entitlement", formatStatus(overview.entitlementStatus)),
113
+ overview.entitlement ? formatField("Expires at", formatDateTime(overview.entitlement.validUntil)) : undefined,
114
+ overview.apiKey ? formatField("API key", formatStatus(overview.apiKey.status)) : formatField("API key", "none"),
115
+ overview.apiKey ? formatField("Key preview", overview.apiKey.apiKeyPreview) : undefined,
116
+ ]),
117
+ formatUsageSection(overview),
118
+ overview.tokenLots.length > 0 ? formatSection("Token lots", formatTokenLotLines(overview)) : undefined,
119
+ ]);
120
+ }
121
+ function formatCustomerMessage(title, blocks) {
122
+ return [title, ...blocks.filter(Boolean)].join("\n\n");
123
+ }
124
+ function formatWorkspaceSection(overview) {
125
+ return formatSection("Workspace", [
126
+ formatField("ID", overview.workspace?.id ?? "none"),
127
+ formatField("Status", formatStatus(overview.workspace?.status ?? "none")),
128
+ formatField("Client route", overview.workspace?.defaultClientRoute ?? "none"),
129
+ ]);
130
+ }
131
+ function formatApiKeySummarySection(overview) {
132
+ return formatSection("API key", [
133
+ overview.apiKey ? formatField("Status", formatStatus(overview.apiKey.status)) : formatField("Status", "none"),
134
+ overview.apiKey ? formatField("Preview", overview.apiKey.apiKeyPreview) : undefined,
135
+ ]);
136
+ }
137
+ function formatUsageSection(overview) {
138
+ const limit = overview.tokenLots.length > 0
139
+ ? overview.tokenLots.reduce((sum, lot) => sum + lot.entitlement.monthlyTokenLimit, 0)
140
+ : overview.entitlement?.monthlyTokenLimit;
141
+ return formatSection("Tokens", [
142
+ formatField("Input", formatTokenCount(overview.usage.inputTokens)),
143
+ formatField("Output", formatTokenCount(overview.usage.outputTokens)),
144
+ formatField("Used", formatTokenCount(overview.usage.totalTokens)),
145
+ typeof limit === "number" ? formatField("Limit", formatTokenCount(limit)) : undefined,
146
+ overview.remainingTokens !== null ? formatField("Remaining", formatTokenCount(overview.remainingTokens)) : undefined,
147
+ ]);
148
+ }
149
+ function formatTokenLotLines(overview) {
150
+ if (overview.tokenLots.length === 0) {
151
+ return [];
152
+ }
153
+ return overview.tokenLots.map((lot, index) => `• Lot ${index + 1}: ${formatTokenCount(lot.remainingTokens)} remaining / ${formatTokenCount(lot.entitlement.monthlyTokenLimit)} limit, expires ${formatDateTime(lot.entitlement.validUntil)}`);
154
+ }
155
+ function formatTokenCount(value) {
156
+ return new Intl.NumberFormat("en-US").format(value);
157
+ }
158
+ function formatStatus(value) {
159
+ return value.replace(/_/g, " ");
160
+ }
@@ -0,0 +1,68 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import BetterSqlite3 from "better-sqlite3";
4
+ export class CustomerApiKeyStore {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ static create(dbFile) {
10
+ mkdirSync(path.dirname(dbFile), { recursive: true });
11
+ const db = new BetterSqlite3(dbFile);
12
+ ensureCustomerApiKeySchema(db);
13
+ return new CustomerApiKeyStore(db);
14
+ }
15
+ get(userId) {
16
+ const row = this.db
17
+ .prepare(`SELECT user_id, client_route, api_key, created_by_user_id, created_at
18
+ FROM telegram_customer_api_keys
19
+ WHERE user_id = ?`)
20
+ .get(userId);
21
+ return row ? mapCustomerApiKeyRow(row) : undefined;
22
+ }
23
+ upsert(input) {
24
+ const createdAt = (input.now ?? new Date()).toISOString();
25
+ this.db
26
+ .prepare(`INSERT INTO telegram_customer_api_keys (
27
+ user_id,
28
+ client_route,
29
+ api_key,
30
+ created_by_user_id,
31
+ created_at
32
+ )
33
+ VALUES (?, ?, ?, ?, ?)
34
+ ON CONFLICT(user_id) DO UPDATE SET
35
+ client_route = excluded.client_route,
36
+ api_key = excluded.api_key,
37
+ created_by_user_id = excluded.created_by_user_id,
38
+ created_at = excluded.created_at`)
39
+ .run(input.userId, input.clientRoute, input.apiKey, input.createdByUserId, createdAt);
40
+ return {
41
+ userId: input.userId,
42
+ clientRoute: input.clientRoute,
43
+ apiKey: input.apiKey,
44
+ createdByUserId: input.createdByUserId,
45
+ createdAt,
46
+ };
47
+ }
48
+ }
49
+ function ensureCustomerApiKeySchema(db) {
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS telegram_customer_api_keys (
52
+ user_id TEXT PRIMARY KEY,
53
+ client_route TEXT NOT NULL,
54
+ api_key TEXT NOT NULL,
55
+ created_by_user_id TEXT NOT NULL,
56
+ created_at TEXT NOT NULL
57
+ )
58
+ `);
59
+ }
60
+ function mapCustomerApiKeyRow(row) {
61
+ return {
62
+ userId: row.user_id,
63
+ clientRoute: row.client_route,
64
+ apiKey: row.api_key,
65
+ createdByUserId: row.created_by_user_id,
66
+ createdAt: row.created_at,
67
+ };
68
+ }
@@ -0,0 +1,72 @@
1
+ export function readCustomerBillingOverview(args) {
2
+ const now = args.now ?? new Date();
3
+ const workspace = args.workspaces.getDefaultWorkspace(args.telegramUserId);
4
+ const apiKey = args.customerKeys.getActiveKeyForUser(args.telegramUserId) ??
5
+ args.customerKeys.getLatestKeyForUser(args.telegramUserId);
6
+ const activeLots = workspace ? args.billing.getActiveEntitlementLotsForWorkspace(workspace.id, now) : [];
7
+ const entitlement = activeLots[0]?.entitlement ?? (workspace ? args.billing.getLatestEntitlementForWorkspace(workspace.id) : undefined);
8
+ const tokenLots = workspace
9
+ ? activeLots.length > 0
10
+ ? activeLots
11
+ : entitlement
12
+ ? [{
13
+ entitlement,
14
+ usage: args.billing.getEntitlementUsage(entitlement.id) ?? buildEmptyUsage(entitlement.id, workspace.id),
15
+ remainingTokens: entitlement.status === "active"
16
+ ? Math.max(0, entitlement.monthlyTokenLimit -
17
+ (args.billing.getEntitlementUsage(entitlement.id)?.totalTokens ?? 0))
18
+ : 0,
19
+ }]
20
+ : []
21
+ : [];
22
+ const usage = tokenLots.length > 0
23
+ ? tokenLots.reduce((acc, lot) => ({
24
+ entitlementId: acc.entitlementId,
25
+ workspaceId: acc.workspaceId,
26
+ inputTokens: acc.inputTokens + lot.usage.inputTokens,
27
+ outputTokens: acc.outputTokens + lot.usage.outputTokens,
28
+ totalTokens: acc.totalTokens + lot.usage.totalTokens,
29
+ createdAt: acc.createdAt,
30
+ updatedAt: acc.updatedAt,
31
+ }), buildEmptyUsage(tokenLots[0]?.entitlement.id, workspace?.id))
32
+ : buildEmptyUsage(undefined, workspace?.id);
33
+ const entitlementStatus = resolveEntitlementStatus(entitlement, now);
34
+ const remainingTokens = tokenLots.length > 0
35
+ ? tokenLots.reduce((sum, lot) => sum + lot.remainingTokens, 0)
36
+ : entitlement
37
+ ? 0
38
+ : null;
39
+ return {
40
+ workspace,
41
+ apiKey,
42
+ entitlement,
43
+ tokenLots,
44
+ usage,
45
+ entitlementStatus,
46
+ remainingTokens,
47
+ };
48
+ }
49
+ function resolveEntitlementStatus(entitlement, now) {
50
+ if (!entitlement) {
51
+ return "none";
52
+ }
53
+ if (entitlement.status === "suspended") {
54
+ return "suspended";
55
+ }
56
+ if (entitlement.status === "expired" || new Date(entitlement.validUntil).getTime() < now.getTime()) {
57
+ return "expired";
58
+ }
59
+ return "active";
60
+ }
61
+ function buildEmptyUsage(entitlementId, workspaceId) {
62
+ const timestamp = new Date(0).toISOString();
63
+ return {
64
+ entitlementId: entitlementId ?? "none",
65
+ workspaceId: workspaceId ?? "none",
66
+ inputTokens: 0,
67
+ outputTokens: 0,
68
+ totalTokens: 0,
69
+ createdAt: timestamp,
70
+ updatedAt: timestamp,
71
+ };
72
+ }
@@ -0,0 +1,134 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import BetterSqlite3 from "better-sqlite3";
5
+ export class CustomerWorkspaceRepository {
6
+ db;
7
+ constructor(db) {
8
+ this.db = db;
9
+ }
10
+ static create(dbFile) {
11
+ mkdirSync(path.dirname(dbFile), { recursive: true });
12
+ const db = new BetterSqlite3(dbFile);
13
+ ensureWorkspaceSchema(db);
14
+ return new CustomerWorkspaceRepository(db);
15
+ }
16
+ getDefaultWorkspace(ownerTelegramUserId) {
17
+ const row = this.db
18
+ .prepare(`SELECT
19
+ id,
20
+ owner_telegram_user_id,
21
+ telegram_chat_id,
22
+ name,
23
+ default_client_route,
24
+ status,
25
+ created_at,
26
+ updated_at
27
+ FROM customer_workspaces
28
+ WHERE owner_telegram_user_id = ?
29
+ ORDER BY created_at ASC
30
+ LIMIT 1`)
31
+ .get(ownerTelegramUserId);
32
+ return row ? mapWorkspaceRow(row) : undefined;
33
+ }
34
+ getById(id) {
35
+ const row = this.db
36
+ .prepare(`SELECT
37
+ id,
38
+ owner_telegram_user_id,
39
+ telegram_chat_id,
40
+ name,
41
+ default_client_route,
42
+ status,
43
+ created_at,
44
+ updated_at
45
+ FROM customer_workspaces
46
+ WHERE id = ?`)
47
+ .get(id);
48
+ return row ? mapWorkspaceRow(row) : undefined;
49
+ }
50
+ listWorkspaces() {
51
+ const rows = this.db
52
+ .prepare(`SELECT
53
+ id,
54
+ owner_telegram_user_id,
55
+ telegram_chat_id,
56
+ name,
57
+ default_client_route,
58
+ status,
59
+ created_at,
60
+ updated_at
61
+ FROM customer_workspaces
62
+ ORDER BY created_at ASC`)
63
+ .all();
64
+ return rows.map(mapWorkspaceRow);
65
+ }
66
+ ensureDefaultWorkspace(input) {
67
+ const existing = this.getDefaultWorkspace(input.ownerTelegramUserId);
68
+ if (existing) {
69
+ return existing;
70
+ }
71
+ const now = (input.now ?? new Date()).toISOString();
72
+ const id = randomUUID();
73
+ this.db
74
+ .prepare(`INSERT INTO customer_workspaces (
75
+ id,
76
+ owner_telegram_user_id,
77
+ telegram_chat_id,
78
+ name,
79
+ default_client_route,
80
+ status,
81
+ created_at,
82
+ updated_at
83
+ )
84
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
85
+ .run(id, input.ownerTelegramUserId, input.telegramChatId ?? null, input.name ?? null, input.defaultClientRoute, input.status, now, now);
86
+ return this.getDefaultWorkspace(input.ownerTelegramUserId);
87
+ }
88
+ setStatus(id, status, now = new Date()) {
89
+ this.db
90
+ .prepare(`UPDATE customer_workspaces
91
+ SET status = ?,
92
+ updated_at = ?
93
+ WHERE id = ?`)
94
+ .run(status, now.toISOString(), id);
95
+ return this.getById(id);
96
+ }
97
+ }
98
+ function ensureWorkspaceSchema(db) {
99
+ db.exec(`
100
+ CREATE TABLE IF NOT EXISTS customer_workspaces (
101
+ id TEXT PRIMARY KEY,
102
+ owner_telegram_user_id TEXT NOT NULL,
103
+ telegram_chat_id TEXT,
104
+ name TEXT,
105
+ default_client_route TEXT NOT NULL DEFAULT 'customers',
106
+ status TEXT NOT NULL DEFAULT 'active',
107
+ created_at TEXT NOT NULL,
108
+ updated_at TEXT NOT NULL
109
+ );
110
+
111
+ CREATE INDEX IF NOT EXISTS idx_customer_workspaces_owner
112
+ ON customer_workspaces(owner_telegram_user_id, created_at);
113
+ `);
114
+ }
115
+ function mapWorkspaceRow(row) {
116
+ return {
117
+ id: row.id,
118
+ ownerTelegramUserId: row.owner_telegram_user_id,
119
+ telegramChatId: row.telegram_chat_id ?? undefined,
120
+ name: row.name ?? undefined,
121
+ defaultClientRoute: row.default_client_route,
122
+ status: normalizeStatus(row.status),
123
+ createdAt: row.created_at,
124
+ updatedAt: row.updated_at,
125
+ };
126
+ }
127
+ function normalizeStatus(value) {
128
+ return value === "active" ||
129
+ value === "pending_approval" ||
130
+ value === "suspended" ||
131
+ value === "closed"
132
+ ? value
133
+ : "active";
134
+ }
@@ -0,0 +1,47 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import assert from "node:assert/strict";
5
+ import test from "node:test";
6
+ import { CustomerWorkspaceRepository } from "./customer-workspace-repository.js";
7
+ function withRepository(fn) {
8
+ const dir = mkdtempSync(path.join(os.tmpdir(), "customer-workspace-"));
9
+ try {
10
+ fn(CustomerWorkspaceRepository.create(path.join(dir, "bot.sqlite")));
11
+ }
12
+ finally {
13
+ rmSync(dir, { recursive: true, force: true });
14
+ }
15
+ }
16
+ test("CustomerWorkspaceRepository creates default workspace idempotently", () => {
17
+ withRepository((repo) => {
18
+ const first = repo.ensureDefaultWorkspace({
19
+ ownerTelegramUserId: "1283361952",
20
+ telegramChatId: "1283361952",
21
+ defaultClientRoute: "customers",
22
+ status: "active",
23
+ now: new Date("2026-04-27T00:00:00.000Z"),
24
+ });
25
+ const second = repo.ensureDefaultWorkspace({
26
+ ownerTelegramUserId: "1283361952",
27
+ defaultClientRoute: "other",
28
+ status: "pending_approval",
29
+ now: new Date("2026-04-28T00:00:00.000Z"),
30
+ });
31
+ assert.equal(second.id, first.id);
32
+ assert.equal(second.defaultClientRoute, "customers");
33
+ assert.equal(second.status, "active");
34
+ assert.equal(second.createdAt, "2026-04-27T00:00:00.000Z");
35
+ });
36
+ });
37
+ test("CustomerWorkspaceRepository can create pending approval workspace", () => {
38
+ withRepository((repo) => {
39
+ const workspace = repo.ensureDefaultWorkspace({
40
+ ownerTelegramUserId: "42",
41
+ defaultClientRoute: "customers",
42
+ status: "pending_approval",
43
+ });
44
+ assert.equal(workspace.ownerTelegramUserId, "42");
45
+ assert.equal(workspace.status, "pending_approval");
46
+ });
47
+ });
@@ -0,0 +1,39 @@
1
+ import { answerCallbackQuerySafely, replyOrEditMessage } from "./callbacks.js";
2
+ import { isAdmin } from "./auth.js";
3
+ export function registerDashboardLoginCallbacks(bot, config, dashboardAuth) {
4
+ bot.callbackQuery(/^v1:dashauth:([a-f0-9]+):(\d{2})$/, async (ctx) => {
5
+ if (!isAdmin(ctx, config)) {
6
+ await answerCallbackQuerySafely(ctx, { text: "Admin only.", show_alert: true });
7
+ return;
8
+ }
9
+ const challengeId = ctx.match[1];
10
+ const selectedCode = ctx.match[2];
11
+ const telegramUserId = ctx.from?.id?.toString();
12
+ if (!telegramUserId) {
13
+ await answerCallbackQuerySafely(ctx, { text: "Missing Telegram user.", show_alert: true });
14
+ return;
15
+ }
16
+ const result = dashboardAuth.resolveApprovalChoice({
17
+ challengeId,
18
+ telegramUserId,
19
+ selectedCode,
20
+ });
21
+ if (!result.ok) {
22
+ await answerCallbackQuerySafely(ctx, {
23
+ text: result.reason === "expired"
24
+ ? "Approval request expired."
25
+ : result.reason === "consumed"
26
+ ? "Approval request already handled."
27
+ : "Approval request not found.",
28
+ show_alert: true,
29
+ });
30
+ return;
31
+ }
32
+ const approved = result.status === "approved";
33
+ await answerCallbackQuerySafely(ctx, {
34
+ text: approved ? "Dashboard login approved." : "Wrong code.",
35
+ show_alert: !approved,
36
+ });
37
+ await replyOrEditMessage(ctx, approved ? "Responses Proxy dashboard login approved." : "Responses Proxy dashboard login rejected.");
38
+ });
39
+ }