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,290 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { BillingRepository } from "../billing.js";
7
+ import { CustomerKeyRepository } from "../customer-keys.js";
8
+ import { BotIdentityRepository } from "./bot-identity-repository.js";
9
+ import { CustomerWorkspaceRepository } from "./customer-workspace-repository.js";
10
+ import { grantCustomerAccess, renewCustomerAccess } from "./grants.js";
11
+ function createMockProxyClient() {
12
+ let routeKeys = [];
13
+ return {
14
+ client: {
15
+ async getClientConfigs() {
16
+ return {
17
+ clientRoutes: [{ key: "customers", apiKeys: [...routeKeys] }],
18
+ };
19
+ },
20
+ async setClientRouteApiKeys(input) {
21
+ if (input.client === "customers") {
22
+ routeKeys = [...input.apiKeys];
23
+ }
24
+ return { ok: true };
25
+ },
26
+ },
27
+ getRouteKeys() {
28
+ return [...routeKeys];
29
+ },
30
+ };
31
+ }
32
+ function createFailingProxyClient() {
33
+ let routeKeys = [];
34
+ return {
35
+ client: {
36
+ async getClientConfigs() {
37
+ return {
38
+ clientRoutes: [{ key: "customers", apiKeys: [...routeKeys] }],
39
+ };
40
+ },
41
+ async setClientRouteApiKeys(input) {
42
+ routeKeys = [...input.apiKeys];
43
+ throw new Error("proxy sync failed");
44
+ },
45
+ },
46
+ getRouteKeys() {
47
+ return [...routeKeys];
48
+ },
49
+ };
50
+ }
51
+ async function withRepos(fn) {
52
+ const dir = mkdtempSync(path.join(os.tmpdir(), "grant-customer-access-"));
53
+ try {
54
+ const dbFile = path.join(dir, "bot.sqlite");
55
+ await fn({
56
+ identities: BotIdentityRepository.create(dbFile),
57
+ workspaces: CustomerWorkspaceRepository.create(dbFile),
58
+ customerKeys: CustomerKeyRepository.create(dbFile),
59
+ billing: BillingRepository.create(dbFile),
60
+ });
61
+ }
62
+ finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ }
66
+ test("grantCustomerAccess creates workspace, entitlement, and a new key", async () => {
67
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
68
+ const proxy = createMockProxyClient();
69
+ const result = await grantCustomerAccess({
70
+ telegramUserId: "1283361952",
71
+ planId: "basic",
72
+ days: 30,
73
+ defaultClientRoute: "customers",
74
+ identities,
75
+ workspaces,
76
+ customerKeys,
77
+ billing,
78
+ proxyClient: proxy.client,
79
+ });
80
+ assert.equal(result.mode, "new_key_created");
81
+ assert.equal(typeof result.apiKey, "string");
82
+ assert.equal(proxy.getRouteKeys().length, 1);
83
+ assert.equal(customerKeys.getActiveKeyForUser("1283361952")?.id, result.keyId);
84
+ assert.equal(identities.getUser("1283361952")?.status, "active");
85
+ assert.equal(workspaces.getDefaultWorkspace("1283361952")?.status, "active");
86
+ assert.ok(billing.getActiveEntitlementForWorkspace(result.workspaceId));
87
+ });
88
+ });
89
+ test("grantCustomerAccess revokes a newly created key when proxy sync fails", async () => {
90
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
91
+ const proxy = createFailingProxyClient();
92
+ await assert.rejects(() => grantCustomerAccess({
93
+ telegramUserId: "1283361952",
94
+ planId: "basic",
95
+ days: 30,
96
+ defaultClientRoute: "customers",
97
+ identities,
98
+ workspaces,
99
+ customerKeys,
100
+ billing,
101
+ proxyClient: proxy.client,
102
+ }), /proxy sync failed/);
103
+ const workspace = workspaces.getDefaultWorkspace("1283361952");
104
+ assert.ok(workspace);
105
+ const keys = customerKeys.listKeysByWorkspace(workspace.id);
106
+ assert.equal(keys.length, 1);
107
+ assert.equal(keys[0]?.status, "revoked");
108
+ assert.equal(customerKeys.getActiveKeyForUser("1283361952"), undefined);
109
+ assert.equal(billing.getActiveEntitlementForWorkspace(workspace.id), undefined);
110
+ });
111
+ });
112
+ test("grantCustomerAccess reactivates an existing suspended key", async () => {
113
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
114
+ const proxy = createMockProxyClient();
115
+ const workspace = workspaces.ensureDefaultWorkspace({
116
+ ownerTelegramUserId: "42",
117
+ defaultClientRoute: "customers",
118
+ status: "suspended",
119
+ });
120
+ const created = customerKeys.createKey({
121
+ workspaceId: workspace.id,
122
+ telegramUserId: "42",
123
+ clientRoute: "customers",
124
+ });
125
+ customerKeys.setStatus(created.record.id, "suspended");
126
+ const result = await grantCustomerAccess({
127
+ telegramUserId: "42",
128
+ planId: "basic",
129
+ days: 7,
130
+ defaultClientRoute: "customers",
131
+ identities,
132
+ workspaces,
133
+ customerKeys,
134
+ billing,
135
+ proxyClient: proxy.client,
136
+ });
137
+ assert.equal(result.mode, "existing_key_reactivated");
138
+ assert.equal(result.apiKey, created.apiKey);
139
+ assert.equal(customerKeys.getActiveKeyForUser("42")?.id, created.record.id);
140
+ assert.equal(workspaces.getById(workspace.id)?.status, "active");
141
+ });
142
+ });
143
+ test("renewCustomerAccess extends an active subscription window", async () => {
144
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
145
+ const proxy = createMockProxyClient();
146
+ const first = await grantCustomerAccess({
147
+ telegramUserId: "77",
148
+ planId: "basic",
149
+ days: 30,
150
+ defaultClientRoute: "customers",
151
+ identities,
152
+ workspaces,
153
+ customerKeys,
154
+ billing,
155
+ proxyClient: proxy.client,
156
+ });
157
+ const before = billing.getLatestSubscriptionForWorkspace(first.workspaceId);
158
+ assert.ok(before);
159
+ const renewed = await renewCustomerAccess({
160
+ telegramUserId: "77",
161
+ planId: "basic",
162
+ days: 15,
163
+ defaultClientRoute: "customers",
164
+ identities,
165
+ workspaces,
166
+ customerKeys,
167
+ billing,
168
+ proxyClient: proxy.client,
169
+ });
170
+ const after = billing.getLatestSubscriptionForWorkspace(first.workspaceId);
171
+ assert.ok(after);
172
+ assert.equal(renewed.mode, "existing_key_already_active");
173
+ assert.equal(renewed.apiKey, first.apiKey);
174
+ assert.ok(new Date(after.currentPeriodEnd).getTime() > new Date(before.currentPeriodEnd).getTime());
175
+ });
176
+ });
177
+ test("renewCustomerAccess rotates legacy active keys that cannot reveal a full secret", async () => {
178
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
179
+ const proxy = createMockProxyClient();
180
+ const workspace = workspaces.ensureDefaultWorkspace({
181
+ ownerTelegramUserId: "99",
182
+ defaultClientRoute: "customers",
183
+ status: "active",
184
+ });
185
+ const legacy = customerKeys.createKey({
186
+ workspaceId: workspace.id,
187
+ telegramUserId: "99",
188
+ clientRoute: "customers",
189
+ });
190
+ customerKeys.db
191
+ .prepare("UPDATE customer_api_keys SET api_key_secret = NULL WHERE id = ?")
192
+ .run(legacy.record.id);
193
+ billing.grantSubscription({
194
+ workspaceId: workspace.id,
195
+ planId: "basic",
196
+ days: 30,
197
+ });
198
+ const renewed = await renewCustomerAccess({
199
+ telegramUserId: "99",
200
+ planId: "basic",
201
+ days: 15,
202
+ defaultClientRoute: "customers",
203
+ identities,
204
+ workspaces,
205
+ customerKeys,
206
+ billing,
207
+ proxyClient: proxy.client,
208
+ });
209
+ assert.equal(renewed.mode, "existing_key_replaced");
210
+ assert.equal(typeof renewed.apiKey, "string");
211
+ assert.equal(customerKeys.getById(legacy.record.id)?.status, "revoked");
212
+ assert.equal(customerKeys.getActiveKeyForUser("99")?.id, renewed.keyId);
213
+ assert.equal(proxy.getRouteKeys().includes(renewed.apiKey), true);
214
+ });
215
+ });
216
+ test("renewCustomerAccess can replace the latest key", async () => {
217
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
218
+ const proxy = createMockProxyClient();
219
+ const first = await grantCustomerAccess({
220
+ telegramUserId: "88",
221
+ planId: "basic",
222
+ days: 30,
223
+ defaultClientRoute: "customers",
224
+ identities,
225
+ workspaces,
226
+ customerKeys,
227
+ billing,
228
+ proxyClient: proxy.client,
229
+ });
230
+ const previousKey = customerKeys.getLatestKeyForUser("88");
231
+ assert.ok(previousKey);
232
+ const renewed = await renewCustomerAccess({
233
+ telegramUserId: "88",
234
+ planId: "basic",
235
+ days: 30,
236
+ replaceKey: true,
237
+ defaultClientRoute: "customers",
238
+ identities,
239
+ workspaces,
240
+ customerKeys,
241
+ billing,
242
+ proxyClient: proxy.client,
243
+ });
244
+ const latestKey = customerKeys.getLatestKeyForUser("88");
245
+ assert.equal(renewed.mode, "existing_key_replaced");
246
+ assert.equal(typeof renewed.apiKey, "string");
247
+ assert.ok(latestKey);
248
+ assert.notEqual(latestKey.id, previousKey.id);
249
+ assert.equal(customerKeys.getById(previousKey.id)?.status, "revoked");
250
+ assert.equal(customerKeys.getById(latestKey.id)?.status, "active");
251
+ assert.equal(proxy.getRouteKeys().includes(renewed.apiKey), true);
252
+ assert.equal(first.apiKey === renewed.apiKey, false);
253
+ });
254
+ });
255
+ test("renewCustomerAccess keeps the old active key when replacement sync fails", async () => {
256
+ await withRepos(async ({ identities, workspaces, customerKeys, billing }) => {
257
+ const firstProxy = createMockProxyClient();
258
+ const first = await grantCustomerAccess({
259
+ telegramUserId: "88",
260
+ planId: "basic",
261
+ days: 30,
262
+ defaultClientRoute: "customers",
263
+ identities,
264
+ workspaces,
265
+ customerKeys,
266
+ billing,
267
+ proxyClient: firstProxy.client,
268
+ });
269
+ const failingProxy = createFailingProxyClient();
270
+ const before = billing.getLatestSubscriptionForWorkspace(first.workspaceId);
271
+ assert.ok(before);
272
+ await assert.rejects(() => renewCustomerAccess({
273
+ telegramUserId: "88",
274
+ planId: "basic",
275
+ days: 30,
276
+ defaultClientRoute: "customers",
277
+ identities,
278
+ workspaces,
279
+ customerKeys,
280
+ billing,
281
+ proxyClient: failingProxy.client,
282
+ replaceKey: true,
283
+ }), /proxy sync failed/);
284
+ assert.equal(customerKeys.getActiveKeyForUser("88")?.id, first.keyId);
285
+ assert.equal(billing.getLatestSubscriptionForWorkspace(first.workspaceId)?.currentPeriodEnd, before.currentPeriodEnd);
286
+ const keys = customerKeys.listKeysByWorkspace(first.workspaceId);
287
+ assert.equal(keys.filter((key) => key.status === "active").length, 1);
288
+ assert.equal(keys.filter((key) => key.status === "revoked").length, 1);
289
+ });
290
+ });
@@ -0,0 +1,85 @@
1
+ import { createTelegramBot } from "./telegram-adapter.js";
2
+ import { readTelegramBotConfig } from "./config.js";
3
+ import { ResponsesProxyClient } from "./proxy-client.js";
4
+ const config = readTelegramBotConfig(process.env);
5
+ const proxyClient = new ResponsesProxyClient(config.proxyAdminBaseUrl, config.proxyClientApiKey, config.proxyRequestTimeoutMs);
6
+ if (config.botMode === "webhook") {
7
+ throw new Error("Webhook mode is not implemented yet. Use TELEGRAM_BOT_MODE=polling.");
8
+ }
9
+ let stopping = false;
10
+ let activeBot;
11
+ async function registerCommandMetadata(bot) {
12
+ await bot.api.setMyCommands([
13
+ { command: "plans", description: "List billing plan ids and limits" },
14
+ { command: "grant", description: "Activate paid access for a customer" },
15
+ { command: "renewuser", description: "Renew access or rotate a customer key" },
16
+ { command: "renew", description: "Choose plan and send a renewal request" },
17
+ { command: "apikey", description: "Show or issue customer API keys" },
18
+ { command: "usage", description: "Show customer usage for the current period" },
19
+ { command: "quota", description: "Show customer quota and expiration" },
20
+ { command: "tailscale", description: "Install Tailscale and ask admin for invite" },
21
+ { command: "help", description: "Show ops help, /help customer, /help proxy" },
22
+ { command: "status", description: "Check proxy health and active provider" },
23
+ { command: "providers", description: "Show provider and client route mapping" },
24
+ { command: "clients", description: "Show Hermes and Codex quick config status" },
25
+ { command: "models", description: "List routed models through the proxy" },
26
+ { command: "apply", description: "Apply proxy config to Hermes or Codex" },
27
+ { command: "oauth", description: "Inspect or start ChatGPT OAuth setup" },
28
+ { command: "accounts", description: "List and manage OAuth accounts" },
29
+ { command: "test", description: "Send a small proxy test request" },
30
+ ]);
31
+ }
32
+ const stopBot = (signal) => {
33
+ if (stopping) {
34
+ return;
35
+ }
36
+ stopping = true;
37
+ console.info(`telegram bot stopping on ${signal}`);
38
+ activeBot?.stop();
39
+ };
40
+ process.once("SIGINT", () => stopBot("SIGINT"));
41
+ process.once("SIGTERM", () => stopBot("SIGTERM"));
42
+ while (!stopping) {
43
+ const bot = createTelegramBot({ config, proxyClient });
44
+ activeBot = bot;
45
+ try {
46
+ await registerCommandMetadata(bot);
47
+ }
48
+ catch (error) {
49
+ console.warn("telegram bot could not register command metadata", {
50
+ error: error instanceof Error ? error.message : String(error),
51
+ });
52
+ }
53
+ try {
54
+ console.info("telegram bot starting in polling mode");
55
+ await bot.start();
56
+ break;
57
+ }
58
+ catch (error) {
59
+ if (stopping) {
60
+ break;
61
+ }
62
+ if (isTelegramPollingConflict(error)) {
63
+ console.warn("telegram polling conflict; retrying after 35 seconds", {
64
+ error: error instanceof Error ? error.message : String(error),
65
+ });
66
+ await sleep(35_000);
67
+ continue;
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ console.info("telegram bot stopped");
73
+ function sleep(ms) {
74
+ return new Promise((resolve) => setTimeout(resolve, ms));
75
+ }
76
+ function isTelegramPollingConflict(error) {
77
+ if (typeof error !== "object" || error === null) {
78
+ return false;
79
+ }
80
+ const candidate = error;
81
+ const description = typeof candidate.description === "string" ? candidate.description : "";
82
+ return (candidate.method === "getUpdates" &&
83
+ candidate.error_code === 409 &&
84
+ description.includes("Conflict"));
85
+ }
@@ -0,0 +1,55 @@
1
+ import { isAdmin } from "./auth.js";
2
+ const customerCleanupCommands = new Set([
3
+ "start",
4
+ "help",
5
+ "me",
6
+ "apikey",
7
+ "usage",
8
+ "quota",
9
+ "renew",
10
+ "tailscale",
11
+ ]);
12
+ export function createCustomerMessageCleanupMiddleware(config) {
13
+ return async (ctx, next) => {
14
+ const cleanupTarget = readCustomerCleanupTarget(ctx, config);
15
+ try {
16
+ await next();
17
+ }
18
+ finally {
19
+ if (cleanupTarget) {
20
+ await deleteMessageBestEffort(ctx, cleanupTarget);
21
+ }
22
+ }
23
+ };
24
+ }
25
+ export async function deleteMessageBestEffort(ctx, target) {
26
+ try {
27
+ await ctx.api.deleteMessage(target.chatId, target.messageId);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function readCustomerCleanupTarget(ctx, config) {
35
+ if (isAdmin(ctx, config)) {
36
+ return undefined;
37
+ }
38
+ const message = ctx.message;
39
+ if (!message || !("text" in message) || typeof message.text !== "string") {
40
+ return undefined;
41
+ }
42
+ const command = parseCommand(message.text);
43
+ if (!command || !customerCleanupCommands.has(command)) {
44
+ return undefined;
45
+ }
46
+ const chatId = ctx.chat?.id;
47
+ const messageId = message.message_id;
48
+ return typeof chatId === "number" && typeof messageId === "number"
49
+ ? { chatId, messageId }
50
+ : undefined;
51
+ }
52
+ function parseCommand(text) {
53
+ const match = text.trim().match(/^\/([A-Za-z0-9_]+)(?:@[A-Za-z0-9_]+)?(?:\s|$)/);
54
+ return match?.[1]?.toLowerCase();
55
+ }
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createCustomerMessageCleanupMiddleware } from "./message-cleanup.js";
4
+ function createConfig() {
5
+ return {
6
+ telegramBotToken: "token",
7
+ allowedUserIds: new Set(),
8
+ allowedChatIds: new Set(),
9
+ ownerUserIds: new Set(["1"]),
10
+ adminUserIds: new Set(),
11
+ botMode: "polling",
12
+ proxyAdminBaseUrl: "http://127.0.0.1:8318",
13
+ defaultModel: "gpt-5.5",
14
+ publicSignupEnabled: true,
15
+ requireAdminApproval: false,
16
+ defaultCustomerRoute: "customers",
17
+ publicResponsesBaseUrl: "http://127.0.0.1:8318/v1",
18
+ proxyRequestTimeoutMs: 30_000,
19
+ sessionDbPath: ":memory:",
20
+ sessionTtlMs: 900_000,
21
+ rateLimitWindowMs: 60_000,
22
+ rateLimitMaxRequests: 12,
23
+ logLevel: "info",
24
+ };
25
+ }
26
+ function createContext(input) {
27
+ const deleted = [];
28
+ return {
29
+ from: { id: input.userId, is_bot: false, first_name: "User" },
30
+ chat: { id: input.chatId, type: "private", first_name: "User" },
31
+ message: {
32
+ message_id: 99,
33
+ date: 0,
34
+ chat: { id: input.chatId, type: "private", first_name: "User" },
35
+ text: input.text,
36
+ },
37
+ api: {
38
+ async deleteMessage(chatId, messageId) {
39
+ if (input.deleteImpl) {
40
+ await input.deleteImpl(chatId, messageId);
41
+ }
42
+ deleted.push({ chatId, messageId });
43
+ return true;
44
+ },
45
+ },
46
+ deleted,
47
+ };
48
+ }
49
+ test("cleanup middleware deletes customer command messages after handling", async () => {
50
+ const ctx = createContext({ userId: 42, chatId: 42, text: "/renew" });
51
+ const middleware = createCustomerMessageCleanupMiddleware(createConfig());
52
+ let handled = false;
53
+ await middleware(ctx, async () => {
54
+ handled = true;
55
+ });
56
+ assert.equal(handled, true);
57
+ assert.deepEqual(ctx.deleted, [{ chatId: 42, messageId: 99 }]);
58
+ });
59
+ test("cleanup middleware does not delete admin command messages", async () => {
60
+ const ctx = createContext({ userId: 1, chatId: 1, text: "/renew" });
61
+ const middleware = createCustomerMessageCleanupMiddleware(createConfig());
62
+ await middleware(ctx, async () => { });
63
+ assert.deepEqual(ctx.deleted, []);
64
+ });
65
+ test("cleanup middleware ignores delete errors", async () => {
66
+ const ctx = createContext({
67
+ userId: 42,
68
+ chatId: 42,
69
+ text: "/start",
70
+ deleteImpl: async () => {
71
+ throw new Error("delete forbidden");
72
+ },
73
+ });
74
+ const middleware = createCustomerMessageCleanupMiddleware(createConfig());
75
+ await middleware(ctx, async () => { });
76
+ assert.deepEqual(ctx.deleted, []);
77
+ });
@@ -0,0 +1,45 @@
1
+ export function formatMessage(title, lines) {
2
+ return [title, ...lines.filter(Boolean)].join("\n");
3
+ }
4
+ export function formatSection(title, lines) {
5
+ const visibleLines = lines.filter(Boolean);
6
+ return visibleLines.length > 0 ? [title, ...visibleLines].join("\n") : title;
7
+ }
8
+ export function formatField(label, value) {
9
+ return `• ${label}: ${value ?? "none"}`;
10
+ }
11
+ const DISPLAY_DATE_TIME_FORMATTER = new Intl.DateTimeFormat("vi-VN", {
12
+ timeZone: "Asia/Ho_Chi_Minh",
13
+ day: "2-digit",
14
+ month: "2-digit",
15
+ year: "numeric",
16
+ hour: "2-digit",
17
+ minute: "2-digit",
18
+ hour12: false,
19
+ });
20
+ export function formatDateTime(value) {
21
+ if (value === null || value === undefined || value === "") {
22
+ return "none";
23
+ }
24
+ const date = value instanceof Date ? value : new Date(value);
25
+ if (Number.isNaN(date.getTime())) {
26
+ return String(value);
27
+ }
28
+ const parts = DISPLAY_DATE_TIME_FORMATTER.formatToParts(date);
29
+ const lookup = new Map(parts.filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
30
+ const day = lookup.get("day");
31
+ const month = lookup.get("month");
32
+ const year = lookup.get("year");
33
+ const hour = lookup.get("hour");
34
+ const minute = lookup.get("minute");
35
+ if (!day || !month || !year || !hour || !minute) {
36
+ return DISPLAY_DATE_TIME_FORMATTER.format(date);
37
+ }
38
+ return `${day}/${month}/${year} ${hour}:${minute}`;
39
+ }
40
+ export function formatRawField(label, value) {
41
+ return `${label}: ${value ?? "none"}`;
42
+ }
43
+ export function formatEmptyState(title, hint) {
44
+ return [title, hint].join("\n");
45
+ }
@@ -0,0 +1,10 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { formatDateTime } from "./message-format.js";
4
+ test("formatDateTime renders a compact Vietnam-local timestamp", () => {
5
+ assert.equal(formatDateTime("2026-05-11T07:30:00.000Z"), "11/05/2026 14:30");
6
+ });
7
+ test("formatDateTime keeps invalid values visible", () => {
8
+ assert.equal(formatDateTime("not-a-date"), "not-a-date");
9
+ assert.equal(formatDateTime(undefined), "none");
10
+ });