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,737 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { renderAdminScreen } from "../admin-actions.js";
3
+ import { isAdmin } from "../auth.js";
4
+ import { answerCallbackQuerySafely } from "../callbacks.js";
5
+ import { renderCustomerActionText } from "../customer-actions.js";
6
+ import { maskApiKey } from "../format.js";
7
+ import { getProxyErrorMessage, replyWithProxyError } from "../actions.js";
8
+ import { assertWorkspaceApiKeyCapacity } from "../grants.js";
9
+ import { sendCustomerCodexSetup } from "../codex-config-delivery.js";
10
+ export function registerApiKeyCommand(bot, deps, customerKeys, workspaces, billing, auditLog) {
11
+ bot.command("apikey", async (ctx) => {
12
+ const args = ctx.match?.toString().trim() || "";
13
+ if (args.startsWith("issue ")) {
14
+ await issueCustomerApiKey(ctx, deps, customerKeys, workspaces, billing, auditLog, args.slice("issue ".length).trim());
15
+ return;
16
+ }
17
+ if (args && isAdmin(ctx, deps.config)) {
18
+ await handleAdminApiKeyCommand(ctx, deps, customerKeys, workspaces, billing, auditLog, args);
19
+ return;
20
+ }
21
+ if (ctx.chat?.type !== "private") {
22
+ await ctx.reply("For safety, open a private chat with this bot and run /apikey there.");
23
+ return;
24
+ }
25
+ const userId = ctx.from?.id?.toString();
26
+ const record = userId ? customerKeys.getActiveKeyForUser(userId) : undefined;
27
+ if (!record) {
28
+ await renderCustomerActionText(ctx, "No Responses API key has been assigned to your Telegram user yet.", false);
29
+ return;
30
+ }
31
+ const apiKey = customerKeys.getApiKeySecret(record.id);
32
+ if (apiKey && userId) {
33
+ const delivered = await sendCustomerCodexSetup(ctx, {
34
+ telegramUserId: userId,
35
+ baseUrl: deps.config.publicResponsesBaseUrl,
36
+ apiKey,
37
+ model: deps.config.defaultModel,
38
+ title: "Your Responses API key",
39
+ details: [
40
+ `base_url: ${deps.config.publicResponsesBaseUrl}`,
41
+ `client_route: ${record.clientRoute}`,
42
+ `key_status: ${record.status}`,
43
+ `key_preview: ${record.apiKeyPreview}`,
44
+ "codex_config_files: attached_below",
45
+ ],
46
+ });
47
+ if (!delivered) {
48
+ await ctx.reply("Codex config files could not be delivered. Try /apikey again.");
49
+ return;
50
+ }
51
+ auditLog.record({
52
+ event: "api_key.revealed",
53
+ actor: { type: "customer", id: userId },
54
+ subjectType: "customer_api_key",
55
+ subjectId: record.id,
56
+ metadata: {
57
+ telegramUserId: userId,
58
+ workspaceId: record.workspaceId,
59
+ keyPreview: record.apiKeyPreview,
60
+ audience: "customer_private_chat",
61
+ apiKey,
62
+ },
63
+ });
64
+ return;
65
+ }
66
+ await renderCustomerActionText(ctx, [
67
+ "Your Responses API key",
68
+ `base_url: ${deps.config.publicResponsesBaseUrl}`,
69
+ `client_route: ${record.clientRoute}`,
70
+ `key_status: ${record.status}`,
71
+ `key_preview: ${record.apiKeyPreview}`,
72
+ "full_key: unavailable_for_legacy_key",
73
+ ]
74
+ .filter(Boolean)
75
+ .join("\n"), record.status === "active");
76
+ });
77
+ const adminKeyActions = {
78
+ show: async (ctx, record) => {
79
+ await answerCallbackQuerySafely(ctx, { text: "Key details loaded" });
80
+ await showAdminApiKey(ctx, customerKeys, auditLog, record);
81
+ },
82
+ suspend: async (ctx, record) => {
83
+ const ok = await changeCustomerApiKeyStatus(ctx, deps, customerKeys, auditLog, record, "suspend");
84
+ await answerCallbackQuerySafely(ctx, ok ? { text: "Key suspended" } : { text: "Key update failed. Check the bot message.", show_alert: true });
85
+ },
86
+ revoke: async (ctx, record) => {
87
+ const ok = await changeCustomerApiKeyStatus(ctx, deps, customerKeys, auditLog, record, "revoke");
88
+ await answerCallbackQuerySafely(ctx, ok ? { text: "Key revoked" } : { text: "Key update failed. Check the bot message.", show_alert: true });
89
+ },
90
+ activate: async (ctx, record) => {
91
+ const ok = await activateCustomerApiKey(ctx, deps, customerKeys, auditLog, record);
92
+ await answerCallbackQuerySafely(ctx, ok ? { text: "Key activated" } : { text: "Key activation failed. Check the bot message.", show_alert: true });
93
+ },
94
+ rotate: async (ctx, record) => {
95
+ if (!record.telegramUserId) {
96
+ await answerCallbackQuerySafely(ctx, { text: "This key is not linked to a Telegram user.", show_alert: true });
97
+ return;
98
+ }
99
+ const ok = await rotateCustomerApiKey(ctx, deps, customerKeys, workspaces, billing, auditLog, {
100
+ telegramUserId: record.telegramUserId,
101
+ clientRoute: record.clientRoute,
102
+ });
103
+ await answerCallbackQuerySafely(ctx, ok ? { text: "Key rotated" } : { text: "Key rotation failed. Check the bot message.", show_alert: true });
104
+ },
105
+ };
106
+ bot.callbackQuery(/^v1:apikey:(show|suspend|activate|revoke|rotate):([A-Za-z0-9_-]+)$/, async (ctx) => {
107
+ if (!isAdmin(ctx, deps.config)) {
108
+ await answerCallbackQuerySafely(ctx, { text: "Admin only.", show_alert: true });
109
+ return;
110
+ }
111
+ const action = ctx.match[1];
112
+ const keyId = ctx.match[2];
113
+ const record = customerKeys.getById(keyId);
114
+ if (!record) {
115
+ await answerCallbackQuerySafely(ctx, { text: "Key not found.", show_alert: true });
116
+ return;
117
+ }
118
+ await adminKeyActions[action](ctx, record);
119
+ });
120
+ bot.on("message:text", async (ctx, next) => {
121
+ if (!isAdmin(ctx, deps.config)) {
122
+ await next();
123
+ return;
124
+ }
125
+ if (ctx.message.text.trim().startsWith("/")) {
126
+ await next();
127
+ return;
128
+ }
129
+ const record = resolveAdminKeyPaste(customerKeys, ctx.message.text);
130
+ if (!record) {
131
+ await next();
132
+ return;
133
+ }
134
+ await showAdminApiKey(ctx, customerKeys, auditLog, record);
135
+ });
136
+ }
137
+ async function handleAdminApiKeyCommand(ctx, deps, customerKeys, workspaces, billing, auditLog, rawArgs) {
138
+ const args = rawArgs.split(/\s+/g).filter(Boolean);
139
+ const [subcommand, target, clientRouteRaw] = args;
140
+ if (!subcommand || subcommand === "help") {
141
+ await renderAdminApiKeyInfo(ctx, formatAdminApiKeyUsage());
142
+ return;
143
+ }
144
+ if (subcommand === "list") {
145
+ if (!/^\d+$/.test(target ?? "")) {
146
+ await renderAdminApiKeyInfo(ctx, "Usage: /apikey list <telegramUserId>");
147
+ return;
148
+ }
149
+ const keys = customerKeys.listKeysByUser(target);
150
+ await renderAdminKeyList(ctx, target, keys);
151
+ return;
152
+ }
153
+ if (subcommand === "show") {
154
+ const record = resolveAdminKeyTarget(customerKeys, target);
155
+ if (!record) {
156
+ await renderAdminApiKeyInfo(ctx, "Usage: /apikey show <keyId|telegramUserId>");
157
+ return;
158
+ }
159
+ await showAdminApiKey(ctx, customerKeys, auditLog, record);
160
+ return;
161
+ }
162
+ if (subcommand === "suspend" || subcommand === "revoke") {
163
+ const record = target ? customerKeys.getById(target) : undefined;
164
+ if (!record) {
165
+ await renderAdminApiKeyInfo(ctx, `Usage: /apikey ${subcommand} <keyId>`);
166
+ return;
167
+ }
168
+ await changeCustomerApiKeyStatus(ctx, deps, customerKeys, auditLog, record, subcommand);
169
+ return;
170
+ }
171
+ if (subcommand === "activate") {
172
+ const record = target ? customerKeys.getById(target) : undefined;
173
+ if (!record) {
174
+ await renderAdminApiKeyInfo(ctx, "Usage: /apikey activate <keyId>");
175
+ return;
176
+ }
177
+ await activateCustomerApiKey(ctx, deps, customerKeys, auditLog, record);
178
+ return;
179
+ }
180
+ if (subcommand === "rotate") {
181
+ if (!/^\d+$/.test(target ?? "")) {
182
+ await renderAdminApiKeyInfo(ctx, "Usage: /apikey rotate <telegramUserId> [clientRoute]");
183
+ return;
184
+ }
185
+ await rotateCustomerApiKey(ctx, deps, customerKeys, workspaces, billing, auditLog, {
186
+ telegramUserId: target,
187
+ clientRoute: clientRouteRaw,
188
+ });
189
+ return;
190
+ }
191
+ await renderAdminApiKeyInfo(ctx, formatAdminApiKeyUsage());
192
+ }
193
+ async function showAdminApiKey(ctx, customerKeys, auditLog, record) {
194
+ const apiKey = ctx.chat?.type === "private" ? customerKeys.getApiKeySecret(record.id) : undefined;
195
+ if (apiKey) {
196
+ auditLog.record({
197
+ event: "api_key.revealed",
198
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
199
+ subjectType: "customer_api_key",
200
+ subjectId: record.id,
201
+ metadata: {
202
+ telegramUserId: record.telegramUserId,
203
+ workspaceId: record.workspaceId,
204
+ keyPreview: record.apiKeyPreview,
205
+ audience: "admin_key_management",
206
+ apiKey,
207
+ },
208
+ });
209
+ }
210
+ await renderAdminScreen(ctx, {
211
+ text: [
212
+ "Customer API key",
213
+ ...formatCustomerKeyDetails(record),
214
+ apiKey ? `api_key: ${apiKey}` : undefined,
215
+ record.apiKeySecret && ctx.chat?.type !== "private"
216
+ ? "api_key_delivery: full key is only shown in a private admin chat."
217
+ : undefined,
218
+ !record.apiKeySecret ? "full_key: unavailable_for_legacy_key" : undefined,
219
+ ]
220
+ .filter(Boolean)
221
+ .join("\n"),
222
+ loop: "keys",
223
+ primaryKeyboard: buildAdminKeyActionsKeyboard(record),
224
+ });
225
+ }
226
+ async function changeCustomerApiKeyStatus(ctx, deps, customerKeys, auditLog, record, action) {
227
+ const status = action === "suspend" ? "suspended" : "revoked";
228
+ try {
229
+ const apiKey = customerKeys.getApiKeySecret(record.id);
230
+ if (apiKey) {
231
+ await syncRemoveRouteApiKey(deps, record.clientRoute, apiKey);
232
+ }
233
+ const updated = customerKeys.setStatus(record.id, status);
234
+ auditLog.record({
235
+ event: status === "suspended" ? "api_key.suspended" : "api_key.revoked",
236
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
237
+ subjectType: "customer_api_key",
238
+ subjectId: record.id,
239
+ metadata: {
240
+ telegramUserId: record.telegramUserId,
241
+ workspaceId: record.workspaceId,
242
+ keyPreview: record.apiKeyPreview,
243
+ reason: "admin_key_management",
244
+ proxySynced: !!apiKey,
245
+ },
246
+ });
247
+ await renderAdminScreen(ctx, {
248
+ text: [
249
+ `Customer API key ${status}.`,
250
+ `key_id: ${record.id}`,
251
+ `key_status: ${updated?.status ?? status}`,
252
+ apiKey ? "proxy_sync: removed_from_route" : "proxy_sync: skipped_legacy_secret_unavailable",
253
+ ].join("\n"),
254
+ loop: "keys",
255
+ primaryKeyboard: updated ? buildAdminKeyActionsKeyboard(updated) : undefined,
256
+ });
257
+ return true;
258
+ }
259
+ catch (error) {
260
+ await renderAdminScreen(ctx, {
261
+ text: getProxyErrorMessage(error),
262
+ loop: "keys",
263
+ primaryKeyboard: buildAdminKeyActionsKeyboard(record),
264
+ });
265
+ return false;
266
+ }
267
+ }
268
+ async function activateCustomerApiKey(ctx, deps, customerKeys, auditLog, record) {
269
+ const apiKey = customerKeys.getApiKeySecret(record.id);
270
+ if (!apiKey) {
271
+ await renderAdminScreen(ctx, {
272
+ text: "Cannot activate this legacy key because the full key is unavailable. Use /apikey rotate <telegramUserId>.",
273
+ loop: "keys",
274
+ primaryKeyboard: buildAdminKeyActionsKeyboard(record),
275
+ });
276
+ return false;
277
+ }
278
+ try {
279
+ await syncAddRouteApiKey(deps, record.clientRoute, apiKey);
280
+ const updated = customerKeys.setStatus(record.id, "active");
281
+ auditLog.record({
282
+ event: "api_key.activated",
283
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
284
+ subjectType: "customer_api_key",
285
+ subjectId: record.id,
286
+ metadata: {
287
+ telegramUserId: record.telegramUserId,
288
+ workspaceId: record.workspaceId,
289
+ keyPreview: record.apiKeyPreview,
290
+ reason: "admin_key_management",
291
+ },
292
+ });
293
+ await renderAdminScreen(ctx, {
294
+ text: [
295
+ "Customer API key activated.",
296
+ `key_id: ${record.id}`,
297
+ `key_status: ${updated?.status ?? "active"}`,
298
+ "proxy_sync: added_to_route",
299
+ ].join("\n"),
300
+ loop: "keys",
301
+ primaryKeyboard: updated ? buildAdminKeyActionsKeyboard(updated) : undefined,
302
+ });
303
+ return true;
304
+ }
305
+ catch (error) {
306
+ await renderAdminScreen(ctx, {
307
+ text: getProxyErrorMessage(error),
308
+ loop: "keys",
309
+ primaryKeyboard: buildAdminKeyActionsKeyboard(record),
310
+ });
311
+ return false;
312
+ }
313
+ }
314
+ async function issueCustomerApiKey(ctx, deps, customerKeys, workspaces, billing, auditLog, args) {
315
+ if (!isAdmin(ctx, deps.config)) {
316
+ await ctx.reply("Only admins can issue customer API keys.");
317
+ return;
318
+ }
319
+ const [userId, clientRouteRaw, apiKeyRaw] = args.split(/\s+/g);
320
+ if (!/^\d+$/.test(userId ?? "")) {
321
+ await renderAdminApiKeyInfo(ctx, "Usage: /apikey issue <telegramUserId> [clientRoute] [apiKey]");
322
+ return;
323
+ }
324
+ if (!apiKeyRaw && ctx.chat?.type !== "private") {
325
+ await renderAdminApiKeyInfo(ctx, "Run /apikey issue in a private admin chat when generating a new key, or provide an explicit apiKey.");
326
+ return;
327
+ }
328
+ const clientRoute = normalizeClientRoute(clientRouteRaw || deps.config.defaultCustomerRoute);
329
+ try {
330
+ const workspace = workspaces.ensureDefaultWorkspace({
331
+ ownerTelegramUserId: userId,
332
+ defaultClientRoute: clientRoute,
333
+ status: "active",
334
+ });
335
+ assertWorkspaceApiKeyCapacity({
336
+ workspaceId: workspace.id,
337
+ billing,
338
+ customerKeys,
339
+ });
340
+ const created = customerKeys.createKey({
341
+ workspaceId: workspace.id,
342
+ telegramUserId: userId,
343
+ clientRoute,
344
+ apiKey: apiKeyRaw,
345
+ status: "suspended",
346
+ });
347
+ auditLog.record({
348
+ event: "api_key.created",
349
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
350
+ subjectType: "customer_api_key",
351
+ subjectId: created.record.id,
352
+ metadata: {
353
+ telegramUserId: userId,
354
+ workspaceId: workspace.id,
355
+ clientRoute,
356
+ keyPreview: created.record.apiKeyPreview,
357
+ },
358
+ });
359
+ try {
360
+ const clientConfigs = await deps.proxyClient.getClientConfigs();
361
+ const currentKeys = readClientRouteKeys(clientConfigs, clientRoute);
362
+ const nextKeys = [...new Set([...currentKeys, created.apiKey])];
363
+ await deps.proxyClient.setClientRouteApiKeys({
364
+ client: clientRoute,
365
+ apiKeys: nextKeys,
366
+ });
367
+ customerKeys.setStatus(created.record.id, "active");
368
+ auditLog.record({
369
+ event: "api_key.activated",
370
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
371
+ subjectType: "customer_api_key",
372
+ subjectId: created.record.id,
373
+ metadata: {
374
+ telegramUserId: userId,
375
+ workspaceId: workspace.id,
376
+ keyPreview: created.record.apiKeyPreview,
377
+ reason: "proxy_sync_succeeded",
378
+ },
379
+ });
380
+ }
381
+ catch (error) {
382
+ customerKeys.setStatus(created.record.id, "revoked");
383
+ auditLog.record({
384
+ event: "api_key.revoked",
385
+ actor: { type: "system", id: "proxy-sync-rollback" },
386
+ subjectType: "customer_api_key",
387
+ subjectId: created.record.id,
388
+ metadata: {
389
+ telegramUserId: userId,
390
+ workspaceId: workspace.id,
391
+ keyPreview: created.record.apiKeyPreview,
392
+ reason: "proxy_sync_failed",
393
+ },
394
+ });
395
+ throw error;
396
+ }
397
+ const canShowApiKeyToAdmin = ctx.chat?.type === "private";
398
+ await renderAdminScreen(ctx, {
399
+ text: [
400
+ "Customer API key issued.",
401
+ `telegram_user_id: ${userId}`,
402
+ `workspace_id: ${workspace.id}`,
403
+ `client_route: ${created.record.clientRoute}`,
404
+ `key_preview: ${maskApiKey(created.apiKey)}`,
405
+ canShowApiKeyToAdmin ? `api_key: ${created.apiKey}` : undefined,
406
+ canShowApiKeyToAdmin
407
+ ? "This full key is shown once. Ask the customer to open this bot and run /apikey for status later."
408
+ : "api_key_delivery: full key is only shown in a private admin chat.",
409
+ ]
410
+ .filter(Boolean)
411
+ .join("\n"),
412
+ loop: "keys",
413
+ primaryKeyboard: buildAdminKeyActionsKeyboard(customerKeys.getById(created.record.id) ?? created.record),
414
+ });
415
+ if (canShowApiKeyToAdmin) {
416
+ auditLog.record({
417
+ event: "api_key.revealed",
418
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
419
+ subjectType: "customer_api_key",
420
+ subjectId: created.record.id,
421
+ metadata: {
422
+ telegramUserId: userId,
423
+ workspaceId: workspace.id,
424
+ keyPreview: created.record.apiKeyPreview,
425
+ audience: "admin_private_chat",
426
+ apiKey: created.apiKey,
427
+ },
428
+ });
429
+ }
430
+ }
431
+ catch (error) {
432
+ await replyWithProxyError(ctx, error);
433
+ }
434
+ }
435
+ async function rotateCustomerApiKey(ctx, deps, customerKeys, workspaces, billing, auditLog, input) {
436
+ const workspace = workspaces.getDefaultWorkspace(input.telegramUserId);
437
+ if (!workspace) {
438
+ await renderAdminScreen(ctx, {
439
+ text: "No customer workspace has been assigned to this Telegram user yet.",
440
+ loop: "keys",
441
+ });
442
+ return false;
443
+ }
444
+ const currentKey = customerKeys.getActiveKeyForUser(input.telegramUserId) ?? customerKeys.getLatestKeyForUser(input.telegramUserId);
445
+ const clientRoute = normalizeClientRoute(input.clientRoute || currentKey?.clientRoute || workspace.defaultClientRoute);
446
+ const ignoredKeyIds = currentKey && currentKey.status !== "revoked" ? [currentKey.id] : [];
447
+ assertWorkspaceApiKeyCapacity({
448
+ workspaceId: workspace.id,
449
+ billing,
450
+ customerKeys,
451
+ ignoredKeyIds,
452
+ });
453
+ const created = customerKeys.createKey({
454
+ workspaceId: workspace.id,
455
+ telegramUserId: input.telegramUserId,
456
+ clientRoute,
457
+ status: "suspended",
458
+ });
459
+ auditLog.record({
460
+ event: "api_key.created",
461
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
462
+ subjectType: "customer_api_key",
463
+ subjectId: created.record.id,
464
+ metadata: {
465
+ telegramUserId: input.telegramUserId,
466
+ workspaceId: workspace.id,
467
+ clientRoute,
468
+ keyPreview: created.record.apiKeyPreview,
469
+ reason: "admin_rotation",
470
+ },
471
+ });
472
+ if (currentKey) {
473
+ auditLog.record({
474
+ event: "api_key.rotated",
475
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
476
+ subjectType: "customer_api_key",
477
+ subjectId: created.record.id,
478
+ metadata: {
479
+ telegramUserId: input.telegramUserId,
480
+ workspaceId: workspace.id,
481
+ oldKeyId: currentKey.id,
482
+ newKeyId: created.record.id,
483
+ newKeyPreview: created.record.apiKeyPreview,
484
+ reason: "admin_rotation",
485
+ },
486
+ });
487
+ }
488
+ try {
489
+ const currentSecret = currentKey ? customerKeys.getApiKeySecret(currentKey.id) : undefined;
490
+ await syncRotateRouteApiKey(deps, clientRoute, created.apiKey, currentSecret);
491
+ customerKeys.setStatus(created.record.id, "active");
492
+ auditLog.record({
493
+ event: "api_key.activated",
494
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
495
+ subjectType: "customer_api_key",
496
+ subjectId: created.record.id,
497
+ metadata: {
498
+ telegramUserId: input.telegramUserId,
499
+ workspaceId: workspace.id,
500
+ keyPreview: created.record.apiKeyPreview,
501
+ reason: "proxy_sync_succeeded",
502
+ },
503
+ });
504
+ if (currentKey && currentKey.status !== "revoked") {
505
+ customerKeys.setStatus(currentKey.id, "revoked");
506
+ auditLog.record({
507
+ event: "api_key.revoked",
508
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
509
+ subjectType: "customer_api_key",
510
+ subjectId: currentKey.id,
511
+ metadata: {
512
+ telegramUserId: input.telegramUserId,
513
+ workspaceId: workspace.id,
514
+ keyPreview: currentKey.apiKeyPreview,
515
+ reason: "rotation",
516
+ proxySynced: !!currentSecret,
517
+ },
518
+ });
519
+ }
520
+ }
521
+ catch (error) {
522
+ customerKeys.setStatus(created.record.id, "revoked");
523
+ auditLog.record({
524
+ event: "api_key.revoked",
525
+ actor: { type: "system", id: "proxy-sync-rollback" },
526
+ subjectType: "customer_api_key",
527
+ subjectId: created.record.id,
528
+ metadata: {
529
+ telegramUserId: input.telegramUserId,
530
+ workspaceId: workspace.id,
531
+ keyPreview: created.record.apiKeyPreview,
532
+ reason: "proxy_sync_failed",
533
+ },
534
+ });
535
+ await renderAdminScreen(ctx, {
536
+ text: getProxyErrorMessage(error),
537
+ loop: "keys",
538
+ primaryKeyboard: currentKey ? buildAdminKeyActionsKeyboard(currentKey) : undefined,
539
+ });
540
+ return false;
541
+ }
542
+ const canShowApiKeyToAdmin = ctx.chat?.type === "private";
543
+ if (canShowApiKeyToAdmin) {
544
+ auditLog.record({
545
+ event: "api_key.revealed",
546
+ actor: { type: "admin", id: ctx.from?.id?.toString() },
547
+ subjectType: "customer_api_key",
548
+ subjectId: created.record.id,
549
+ metadata: {
550
+ telegramUserId: input.telegramUserId,
551
+ workspaceId: workspace.id,
552
+ keyPreview: created.record.apiKeyPreview,
553
+ audience: "admin_key_rotation",
554
+ apiKey: created.apiKey,
555
+ },
556
+ });
557
+ }
558
+ await renderAdminScreen(ctx, {
559
+ text: [
560
+ "Customer API key rotated.",
561
+ `telegram_user_id: ${input.telegramUserId}`,
562
+ `workspace_id: ${workspace.id}`,
563
+ currentKey ? `old_key_id: ${currentKey.id}` : undefined,
564
+ `new_key_id: ${created.record.id}`,
565
+ `client_route: ${created.record.clientRoute}`,
566
+ `key_preview: ${maskApiKey(created.apiKey)}`,
567
+ canShowApiKeyToAdmin ? `api_key: ${created.apiKey}` : "api_key_delivery: full key is only shown in a private admin chat.",
568
+ ]
569
+ .filter(Boolean)
570
+ .join("\n"),
571
+ loop: "keys",
572
+ primaryKeyboard: buildAdminKeyActionsKeyboard(customerKeys.getById(created.record.id) ?? created.record),
573
+ });
574
+ return true;
575
+ }
576
+ function readClientRouteKeys(payload, clientRoute) {
577
+ const routes = [
578
+ ...(Array.isArray(payload?.clientRoutes) ? payload.clientRoutes : []),
579
+ ...Object.values(payload?.clients ?? {})
580
+ .map((entry) => entry?.route)
581
+ .filter(Boolean),
582
+ ];
583
+ const route = routes.find((entry) => entry?.key === clientRoute);
584
+ return Array.isArray(route?.apiKeys)
585
+ ? route.apiKeys.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
586
+ : [];
587
+ }
588
+ async function syncAddRouteApiKey(deps, clientRoute, apiKey) {
589
+ const clientConfigs = await deps.proxyClient.getClientConfigs();
590
+ const currentKeys = readClientRouteKeys(clientConfigs, clientRoute);
591
+ const nextKeys = [...new Set([...currentKeys, apiKey])];
592
+ await deps.proxyClient.setClientRouteApiKeys({
593
+ client: clientRoute,
594
+ apiKeys: nextKeys,
595
+ });
596
+ }
597
+ async function syncRemoveRouteApiKey(deps, clientRoute, apiKey) {
598
+ const clientConfigs = await deps.proxyClient.getClientConfigs();
599
+ const currentKeys = readClientRouteKeys(clientConfigs, clientRoute);
600
+ const nextKeys = currentKeys.filter((key) => key !== apiKey);
601
+ await deps.proxyClient.setClientRouteApiKeys({
602
+ client: clientRoute,
603
+ apiKeys: nextKeys,
604
+ });
605
+ }
606
+ async function syncRotateRouteApiKey(deps, clientRoute, newApiKey, oldApiKey) {
607
+ const clientConfigs = await deps.proxyClient.getClientConfigs();
608
+ const currentKeys = readClientRouteKeys(clientConfigs, clientRoute);
609
+ const withoutOld = oldApiKey ? currentKeys.filter((key) => key !== oldApiKey) : currentKeys;
610
+ const nextKeys = [...new Set([...withoutOld, newApiKey])];
611
+ await deps.proxyClient.setClientRouteApiKeys({
612
+ client: clientRoute,
613
+ apiKeys: nextKeys,
614
+ });
615
+ }
616
+ function resolveAdminKeyTarget(customerKeys, target) {
617
+ if (!target) {
618
+ return undefined;
619
+ }
620
+ return /^\d+$/.test(target) ? customerKeys.getLatestKeyForUser(target) : customerKeys.getById(target);
621
+ }
622
+ function formatAdminApiKeyUsage() {
623
+ return [
624
+ "Usage:",
625
+ "/apikey list <telegramUserId>",
626
+ "/apikey show <keyId|telegramUserId> - opens action buttons",
627
+ "/apikey suspend <keyId>",
628
+ "/apikey activate <keyId>",
629
+ "/apikey revoke <keyId>",
630
+ "/apikey rotate <telegramUserId> [clientRoute]",
631
+ "/apikey issue <telegramUserId> [clientRoute] [apiKey]",
632
+ ].join("\n");
633
+ }
634
+ export function buildAdminKeyListKeyboard(keys) {
635
+ const keyboard = new InlineKeyboard();
636
+ keys.slice(0, 10).forEach((record) => {
637
+ keyboard.copyText(`📋 ${formatKeyButtonLabel(record)}`, record.id).row();
638
+ });
639
+ return keyboard;
640
+ }
641
+ function buildAdminKeyActionsKeyboard(record) {
642
+ const keyboard = new InlineKeyboard().text("🔐 Reveal", `v1:apikey:show:${record.id}`);
643
+ if (record.status === "active") {
644
+ keyboard.text("🟡 Suspend", `v1:apikey:suspend:${record.id}`);
645
+ }
646
+ else if (record.status === "suspended" || record.status === "expired") {
647
+ keyboard.text("🟢 Activate", `v1:apikey:activate:${record.id}`);
648
+ }
649
+ keyboard.row();
650
+ if (record.status !== "revoked") {
651
+ keyboard.text("🔴 Revoke", `v1:apikey:revoke:${record.id}`);
652
+ }
653
+ if (record.telegramUserId) {
654
+ keyboard.text("🔵 Rotate", `v1:apikey:rotate:${record.id}`);
655
+ }
656
+ return keyboard;
657
+ }
658
+ async function renderAdminKeyList(ctx, telegramUserId, keys) {
659
+ await renderAdminScreen(ctx, {
660
+ text: [
661
+ `Customer API keys for ${telegramUserId}`,
662
+ "Tap a key button to copy its id, paste it back into this chat, then send to manage.",
663
+ keys.length === 0 ? "none" : undefined,
664
+ ...keys.map(formatCustomerKeyLine),
665
+ ]
666
+ .filter(Boolean)
667
+ .join("\n"),
668
+ loop: "keys",
669
+ primaryKeyboard: keys.length > 0 ? buildAdminKeyListKeyboard(keys) : undefined,
670
+ });
671
+ }
672
+ async function renderAdminApiKeyInfo(ctx, text) {
673
+ await renderAdminScreen(ctx, {
674
+ text,
675
+ loop: "keys",
676
+ });
677
+ }
678
+ function formatKeyButtonLabel(record) {
679
+ const icon = record.status === "active"
680
+ ? "🟢"
681
+ : record.status === "suspended"
682
+ ? "🟡"
683
+ : record.status === "revoked"
684
+ ? "🔴"
685
+ : "⚪";
686
+ return `${icon} ${record.apiKeyPreview}`;
687
+ }
688
+ function resolveAdminKeyPaste(customerKeys, text) {
689
+ const value = text.trim();
690
+ if (!value || value.includes("\n") || value.length > 256) {
691
+ return undefined;
692
+ }
693
+ const byId = customerKeys.getById(value);
694
+ if (byId) {
695
+ return byId;
696
+ }
697
+ if (value.startsWith("sk-")) {
698
+ return customerKeys.getByApiKey(value);
699
+ }
700
+ return undefined;
701
+ }
702
+ function formatCustomerKeyLine(record) {
703
+ return [
704
+ `- ${record.id}`,
705
+ `status=${record.status}`,
706
+ `route=${record.clientRoute}`,
707
+ `preview=${record.apiKeyPreview}`,
708
+ `created=${record.createdAt}`,
709
+ record.lastUsedAt ? `last_used=${record.lastUsedAt}` : undefined,
710
+ ]
711
+ .filter(Boolean)
712
+ .join(" | ");
713
+ }
714
+ function formatCustomerKeyDetails(record) {
715
+ return [
716
+ `key_id: ${record.id}`,
717
+ `workspace_id: ${record.workspaceId}`,
718
+ record.telegramUserId ? `telegram_user_id: ${record.telegramUserId}` : undefined,
719
+ record.telegramChatId ? `telegram_chat_id: ${record.telegramChatId}` : undefined,
720
+ `client_route: ${record.clientRoute}`,
721
+ `key_status: ${record.status}`,
722
+ `key_preview: ${record.apiKeyPreview}`,
723
+ record.name ? `name: ${record.name}` : undefined,
724
+ record.expiresAt ? `expires_at: ${record.expiresAt}` : undefined,
725
+ record.lastUsedAt ? `last_used_at: ${record.lastUsedAt}` : undefined,
726
+ `created_at: ${record.createdAt}`,
727
+ `updated_at: ${record.updatedAt}`,
728
+ record.revokedAt ? `revoked_at: ${record.revokedAt}` : undefined,
729
+ ].filter((line) => !!line);
730
+ }
731
+ function normalizeClientRoute(value) {
732
+ return value
733
+ .trim()
734
+ .toLowerCase()
735
+ .replace(/[^a-z0-9_-]+/g, "-")
736
+ .replace(/^-+|-+$/g, "") || "customers";
737
+ }