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.
- package/README.md +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- package/package.json +39 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { ADMIN_CALLBACK_PATTERN, buildAdminStartKeyboard, buildApplyClientKeyboard, renderAdminScreen, } from "../admin-actions.js";
|
|
2
|
+
import { loadClientsText, loadModelsText, loadOauthStatusText, loadProvidersText, loadStatusText, replyWithProxyError, } from "../actions.js";
|
|
3
|
+
import { isAdmin } from "../auth.js";
|
|
4
|
+
import { answerCallbackQuerySafely } from "../callbacks.js";
|
|
5
|
+
import { renderCustomerActionText } from "../customer-actions.js";
|
|
6
|
+
import { buildAdminKeyListKeyboard } from "./apikey.js";
|
|
7
|
+
import { formatDateTime, formatField, formatSection } from "../message-format.js";
|
|
8
|
+
export function registerStartCommand(bot, deps, identities, workspaces, customerKeys, billing) {
|
|
9
|
+
bot.command("start", async (ctx) => {
|
|
10
|
+
if (isAdmin(ctx, deps.config)) {
|
|
11
|
+
await ctx.reply(formatAdminStartPanel(), {
|
|
12
|
+
reply_markup: buildAdminStartKeyboard(),
|
|
13
|
+
});
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const userId = ctx.from?.id?.toString();
|
|
17
|
+
const chatId = ctx.chat?.id?.toString();
|
|
18
|
+
const user = userId ? identities.getUser(userId) : undefined;
|
|
19
|
+
const activeKey = userId ? customerKeys.getActiveKeyForUser(userId) : undefined;
|
|
20
|
+
const workspace = userId && deps.config.publicSignupEnabled
|
|
21
|
+
? workspaces.ensureDefaultWorkspace({
|
|
22
|
+
ownerTelegramUserId: userId,
|
|
23
|
+
telegramChatId: chatId,
|
|
24
|
+
defaultClientRoute: deps.config.defaultCustomerRoute,
|
|
25
|
+
status: deps.config.requireAdminApproval ? "pending_approval" : "active",
|
|
26
|
+
})
|
|
27
|
+
: undefined;
|
|
28
|
+
await renderCustomerActionText(ctx, [
|
|
29
|
+
"Responses Proxy bot is ready",
|
|
30
|
+
formatSection("Account", [
|
|
31
|
+
user ? formatField("Status", formatStatus(user.status)) : formatField("Status", "not registered"),
|
|
32
|
+
workspace ? formatField("Workspace", formatStatus(workspace.status)) : undefined,
|
|
33
|
+
workspace ? formatField("Client route", workspace.defaultClientRoute) : undefined,
|
|
34
|
+
]),
|
|
35
|
+
"Use /help to see available commands.",
|
|
36
|
+
]
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join("\n\n"), !!activeKey);
|
|
39
|
+
});
|
|
40
|
+
const adminActions = {
|
|
41
|
+
status: {
|
|
42
|
+
loop: "proxy",
|
|
43
|
+
run: async (ctx) => {
|
|
44
|
+
await renderAdminScreen(ctx, {
|
|
45
|
+
text: await loadStatusText(deps),
|
|
46
|
+
loop: "proxy",
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
clients: {
|
|
51
|
+
loop: "config",
|
|
52
|
+
run: async (ctx) => {
|
|
53
|
+
await renderAdminScreen(ctx, {
|
|
54
|
+
text: await loadClientsText(deps),
|
|
55
|
+
loop: "config",
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
providers: {
|
|
60
|
+
loop: "proxy",
|
|
61
|
+
run: async (ctx) => {
|
|
62
|
+
await renderAdminScreen(ctx, {
|
|
63
|
+
text: await loadProvidersText(deps),
|
|
64
|
+
loop: "proxy",
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
models: {
|
|
69
|
+
loop: "proxy",
|
|
70
|
+
run: async (ctx) => {
|
|
71
|
+
await renderAdminScreen(ctx, {
|
|
72
|
+
text: await loadModelsText(deps),
|
|
73
|
+
loop: "proxy",
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
oauth: {
|
|
78
|
+
loop: "proxy",
|
|
79
|
+
run: async (ctx) => {
|
|
80
|
+
await renderAdminScreen(ctx, {
|
|
81
|
+
text: await loadOauthStatusText(deps),
|
|
82
|
+
loop: "proxy",
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
plans: {
|
|
87
|
+
loop: "billing",
|
|
88
|
+
run: async (ctx) => {
|
|
89
|
+
await renderAdminScreen(ctx, {
|
|
90
|
+
text: formatBillingPlans(billing),
|
|
91
|
+
loop: "billing",
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
renewals: {
|
|
96
|
+
loop: "billing",
|
|
97
|
+
run: async (ctx) => {
|
|
98
|
+
await renderAdminScreen(ctx, {
|
|
99
|
+
text: formatOpenRenewalRequests(billing),
|
|
100
|
+
loop: "billing",
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
apikeys: {
|
|
105
|
+
loop: "keys",
|
|
106
|
+
run: async (ctx) => {
|
|
107
|
+
const keys = customerKeys.listRecentKeys(10);
|
|
108
|
+
await renderAdminScreen(ctx, {
|
|
109
|
+
text: formatRecentCustomerKeys(keys),
|
|
110
|
+
loop: "keys",
|
|
111
|
+
primaryKeyboard: keys.length > 0 ? buildAdminKeyListKeyboard(keys) : undefined,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
apply: {
|
|
116
|
+
loop: "apply",
|
|
117
|
+
run: async (ctx) => {
|
|
118
|
+
await renderAdminScreen(ctx, {
|
|
119
|
+
text: "Choose a client to configure.",
|
|
120
|
+
loop: "apply",
|
|
121
|
+
primaryKeyboard: buildApplyClientKeyboard(false),
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
menu: {
|
|
126
|
+
run: async (ctx) => {
|
|
127
|
+
await renderAdminScreen(ctx, {
|
|
128
|
+
text: formatAdminStartPanel(),
|
|
129
|
+
loop: "main",
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
bot.callbackQuery(ADMIN_CALLBACK_PATTERN, async (ctx) => {
|
|
135
|
+
if (!isAdmin(ctx, deps.config)) {
|
|
136
|
+
await answerCallbackQuerySafely(ctx, { text: "Admin only.", show_alert: true });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const action = ctx.match[1];
|
|
140
|
+
const definition = adminActions[action];
|
|
141
|
+
await answerCallbackQuerySafely(ctx, { text: "Loaded" });
|
|
142
|
+
try {
|
|
143
|
+
await definition.run(ctx);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
await replyWithProxyError(ctx, error);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function formatStatus(value) {
|
|
151
|
+
return value.replace(/_/g, " ");
|
|
152
|
+
}
|
|
153
|
+
function formatAdminStartPanel() {
|
|
154
|
+
return [
|
|
155
|
+
"🛠 Admin panel",
|
|
156
|
+
"role: admin",
|
|
157
|
+
"Choose an action below to manage proxy, billing, and customer access.",
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
function formatBillingPlans(billing) {
|
|
161
|
+
const plans = billing.listPlans();
|
|
162
|
+
if (plans.length === 0) {
|
|
163
|
+
return "💳 Billing plans:\nNo billing plans are configured yet.";
|
|
164
|
+
}
|
|
165
|
+
return [
|
|
166
|
+
"💳 Billing plans:",
|
|
167
|
+
...plans.map((plan) => `• ${plan.id}: ${plan.name} | status=${plan.status} | price_cents=${plan.priceCents} | currency=${plan.currency} | billing_interval=${plan.billingInterval} | monthly_token_limit=${plan.monthlyTokenLimit} | max_api_keys=${plan.maxApiKeys}`),
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
function formatOpenRenewalRequests(billing) {
|
|
171
|
+
const openRequests = billing.listRenewalRequests("open");
|
|
172
|
+
if (openRequests.length === 0) {
|
|
173
|
+
return "🧾 Open renewal requests:\nNo open renewal requests right now.";
|
|
174
|
+
}
|
|
175
|
+
return [
|
|
176
|
+
"🧾 Open renewal requests:",
|
|
177
|
+
...openRequests.slice(0, 10).map(formatRenewalRequestLine),
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
180
|
+
function formatRecentCustomerKeys(keys) {
|
|
181
|
+
if (keys.length === 0) {
|
|
182
|
+
return [
|
|
183
|
+
"🔑 Customer API keys",
|
|
184
|
+
"No customer API keys found.",
|
|
185
|
+
"Use /grant or /apikey issue to create one.",
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
188
|
+
return [
|
|
189
|
+
"🔑 Customer API keys",
|
|
190
|
+
"Recent keys. Tap a key button to copy its id, paste it back into this chat, then send to manage.",
|
|
191
|
+
...keys.map((key) => [
|
|
192
|
+
`- ${key.id}`,
|
|
193
|
+
key.telegramUserId ? `user=${key.telegramUserId}` : undefined,
|
|
194
|
+
`status=${key.status}`,
|
|
195
|
+
`route=${key.clientRoute}`,
|
|
196
|
+
`preview=${key.apiKeyPreview}`,
|
|
197
|
+
]
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join(" | ")),
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
function formatRenewalRequestLine(request) {
|
|
203
|
+
return [
|
|
204
|
+
`• ${request.id}`,
|
|
205
|
+
`telegram_user_id=${request.telegramUserId}`,
|
|
206
|
+
request.requestedPlanId ? `plan_id=${request.requestedPlanId}` : undefined,
|
|
207
|
+
request.requestedDays ? `days=${request.requestedDays}` : undefined,
|
|
208
|
+
`requested_at=${formatDateTime(request.requestedAt)}`,
|
|
209
|
+
]
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.join(" ");
|
|
212
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
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 { registerStartCommand } from "./start.js";
|
|
11
|
+
function createConfig(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
telegramBotToken: "token",
|
|
14
|
+
allowedUserIds: new Set(),
|
|
15
|
+
allowedChatIds: new Set(),
|
|
16
|
+
ownerUserIds: new Set(["1"]),
|
|
17
|
+
adminUserIds: new Set(),
|
|
18
|
+
botMode: "polling",
|
|
19
|
+
proxyAdminBaseUrl: "http://127.0.0.1:8318",
|
|
20
|
+
defaultModel: "gpt-5.5",
|
|
21
|
+
publicSignupEnabled: true,
|
|
22
|
+
requireAdminApproval: false,
|
|
23
|
+
defaultCustomerRoute: "customers",
|
|
24
|
+
publicResponsesBaseUrl: "http://127.0.0.1:8318/v1",
|
|
25
|
+
proxyRequestTimeoutMs: 30_000,
|
|
26
|
+
sessionDbPath: ":memory:",
|
|
27
|
+
sessionTtlMs: 900_000,
|
|
28
|
+
rateLimitWindowMs: 60_000,
|
|
29
|
+
rateLimitMaxRequests: 12,
|
|
30
|
+
logLevel: "info",
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createBotHarness() {
|
|
35
|
+
const commandHandlers = new Map();
|
|
36
|
+
const callbackHandlers = [];
|
|
37
|
+
return {
|
|
38
|
+
bot: {
|
|
39
|
+
command(name, handler) {
|
|
40
|
+
commandHandlers.set(name, handler);
|
|
41
|
+
},
|
|
42
|
+
callbackQuery(pattern, handler) {
|
|
43
|
+
callbackHandlers.push({ pattern, handler });
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
handler(name) {
|
|
47
|
+
const handler = commandHandlers.get(name);
|
|
48
|
+
assert.ok(handler);
|
|
49
|
+
return handler;
|
|
50
|
+
},
|
|
51
|
+
callbackHandler(data) {
|
|
52
|
+
for (const entry of callbackHandlers) {
|
|
53
|
+
if (typeof entry.pattern === "string") {
|
|
54
|
+
if (entry.pattern === data) {
|
|
55
|
+
return { handler: entry.handler, match: [data] };
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const match = data.match(entry.pattern);
|
|
60
|
+
if (match) {
|
|
61
|
+
return { handler: entry.handler, match };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
assert.fail(`No callback handler for ${data}`);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createContext(input) {
|
|
69
|
+
const replies = [];
|
|
70
|
+
const editedReplies = [];
|
|
71
|
+
const answeredCallbacks = [];
|
|
72
|
+
return {
|
|
73
|
+
from: { id: input.fromId, is_bot: false, first_name: "User" },
|
|
74
|
+
chat: { id: input.chatId, type: "private", first_name: "User" },
|
|
75
|
+
callbackQuery: {
|
|
76
|
+
id: "callback-1",
|
|
77
|
+
from: { id: input.fromId, is_bot: false, first_name: "User" },
|
|
78
|
+
data: "callback",
|
|
79
|
+
chat_instance: "chat",
|
|
80
|
+
message: {
|
|
81
|
+
message_id: 99,
|
|
82
|
+
date: 0,
|
|
83
|
+
chat: { id: input.chatId, type: "private", first_name: "User" },
|
|
84
|
+
text: "Admin panel",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
match: "",
|
|
88
|
+
replies,
|
|
89
|
+
editedReplies,
|
|
90
|
+
answeredCallbacks,
|
|
91
|
+
reply(text, options) {
|
|
92
|
+
replies.push({ text, options });
|
|
93
|
+
return Promise.resolve({});
|
|
94
|
+
},
|
|
95
|
+
editMessageText(text, options) {
|
|
96
|
+
editedReplies.push({ text, options });
|
|
97
|
+
return Promise.resolve({});
|
|
98
|
+
},
|
|
99
|
+
answerCallbackQuery(payload) {
|
|
100
|
+
answeredCallbacks.push(payload ?? {});
|
|
101
|
+
return Promise.resolve(true);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function withRepos(fn) {
|
|
106
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-start-test-"));
|
|
107
|
+
const dbFile = path.join(dir, "bot.sqlite");
|
|
108
|
+
try {
|
|
109
|
+
const identities = BotIdentityRepository.create(dbFile);
|
|
110
|
+
const workspaces = CustomerWorkspaceRepository.create(dbFile);
|
|
111
|
+
const customerKeys = CustomerKeyRepository.create(dbFile);
|
|
112
|
+
const billing = BillingRepository.create(dbFile);
|
|
113
|
+
const deps = {
|
|
114
|
+
config: createConfig({ sessionDbPath: dbFile }),
|
|
115
|
+
proxyClient: createMockProxyClient(),
|
|
116
|
+
};
|
|
117
|
+
await fn({ identities, workspaces, customerKeys, billing, deps });
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function createMockProxyClient() {
|
|
124
|
+
return {
|
|
125
|
+
async getHealth() {
|
|
126
|
+
return { ok: true };
|
|
127
|
+
},
|
|
128
|
+
async getProviders() {
|
|
129
|
+
return { activeProviderId: "p1", providers: [{ id: "p1", name: "Provider 1" }], clientRoutes: [] };
|
|
130
|
+
},
|
|
131
|
+
async getLatestPromptCache() {
|
|
132
|
+
return { latest: null };
|
|
133
|
+
},
|
|
134
|
+
async getUsageStats() {
|
|
135
|
+
return { stats: { totalRequests: 1 } };
|
|
136
|
+
},
|
|
137
|
+
async getClientConfigs() {
|
|
138
|
+
return { clients: {}, clientRoutes: [] };
|
|
139
|
+
},
|
|
140
|
+
async getModels() {
|
|
141
|
+
return { models: ["gpt-test"] };
|
|
142
|
+
},
|
|
143
|
+
async getOauthStatus() {
|
|
144
|
+
return { enabled: true, accounts: [] };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
test("/start shows Buy button when the user has no active token", async () => {
|
|
149
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, deps }) => {
|
|
150
|
+
const harness = createBotHarness();
|
|
151
|
+
registerStartCommand(harness.bot, deps, identities, workspaces, customerKeys, billing);
|
|
152
|
+
const ctx = createContext({ fromId: 42, chatId: 42 });
|
|
153
|
+
await harness.handler("start")(ctx);
|
|
154
|
+
const firstResponse = ctx.editedReplies[0] ?? ctx.replies[0];
|
|
155
|
+
const keyboard = JSON.parse(JSON.stringify(firstResponse?.options?.reply_markup));
|
|
156
|
+
assert.equal(keyboard.inline_keyboard?.[0]?.[0]?.text, "🔐 View key");
|
|
157
|
+
assert.equal(keyboard.inline_keyboard?.[0]?.[0]?.callback_data, "v1:customer:key");
|
|
158
|
+
assert.equal(keyboard.inline_keyboard?.[1]?.[1]?.text, "💳 Buy API key");
|
|
159
|
+
assert.equal(keyboard.inline_keyboard?.[1]?.[1]?.callback_data, "v1:renew:open");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
test("/start shows Renew button when the user already has an active token", async () => {
|
|
163
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, deps }) => {
|
|
164
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
165
|
+
ownerTelegramUserId: "42",
|
|
166
|
+
defaultClientRoute: "customers",
|
|
167
|
+
status: "active",
|
|
168
|
+
});
|
|
169
|
+
customerKeys.createKey({
|
|
170
|
+
workspaceId: workspace.id,
|
|
171
|
+
telegramUserId: "42",
|
|
172
|
+
clientRoute: "customers",
|
|
173
|
+
});
|
|
174
|
+
const harness = createBotHarness();
|
|
175
|
+
registerStartCommand(harness.bot, deps, identities, workspaces, customerKeys, billing);
|
|
176
|
+
const ctx = createContext({ fromId: 42, chatId: 42 });
|
|
177
|
+
await harness.handler("start")(ctx);
|
|
178
|
+
const firstResponse = ctx.editedReplies[0] ?? ctx.replies[0];
|
|
179
|
+
const keyboard = JSON.parse(JSON.stringify(firstResponse?.options?.reply_markup));
|
|
180
|
+
assert.equal(keyboard.inline_keyboard?.[0]?.[0]?.text, "🔐 View key");
|
|
181
|
+
assert.equal(keyboard.inline_keyboard?.[0]?.[0]?.callback_data, "v1:customer:key");
|
|
182
|
+
assert.equal(keyboard.inline_keyboard?.[1]?.[1]?.text, "⏱ Renew 24h");
|
|
183
|
+
assert.equal(keyboard.inline_keyboard?.[1]?.[1]?.callback_data, "v1:renew:open");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
test("/start shows admin panel for admins without creating a customer workspace", async () => {
|
|
187
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, deps }) => {
|
|
188
|
+
const harness = createBotHarness();
|
|
189
|
+
registerStartCommand(harness.bot, deps, identities, workspaces, customerKeys, billing);
|
|
190
|
+
const ctx = createContext({ fromId: 1, chatId: 1 });
|
|
191
|
+
await harness.handler("start")(ctx);
|
|
192
|
+
assert.equal(ctx.replies[0]?.text.includes("Admin panel"), true);
|
|
193
|
+
assert.equal(workspaces.getDefaultWorkspace("1"), undefined);
|
|
194
|
+
const keyboard = JSON.parse(JSON.stringify(ctx.replies[0]?.options?.reply_markup));
|
|
195
|
+
assert.equal(keyboard.inline_keyboard?.[0]?.[0]?.text, "📈 Status");
|
|
196
|
+
assert.equal(keyboard.inline_keyboard?.[0]?.[0]?.callback_data, "v1:admin:status");
|
|
197
|
+
assert.equal(keyboard.inline_keyboard?.[2]?.[1]?.text, "🧾 Renewals");
|
|
198
|
+
assert.equal(keyboard.inline_keyboard?.[2]?.[1]?.callback_data, "v1:admin:renewals");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
test("admin start panel buttons run real actions", async () => {
|
|
202
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, deps }) => {
|
|
203
|
+
const harness = createBotHarness();
|
|
204
|
+
registerStartCommand(harness.bot, deps, identities, workspaces, customerKeys, billing);
|
|
205
|
+
const found = harness.callbackHandler("v1:admin:status");
|
|
206
|
+
const ctx = createContext({ fromId: 1, chatId: 1 });
|
|
207
|
+
ctx.match = found.match;
|
|
208
|
+
await found.handler(ctx);
|
|
209
|
+
assert.equal(ctx.answeredCallbacks[0]?.text, "Loaded");
|
|
210
|
+
assert.equal(ctx.editedReplies[0]?.text.includes("Proxy status"), true);
|
|
211
|
+
const loopKeyboard = JSON.parse(JSON.stringify(ctx.editedReplies[0]?.options?.reply_markup));
|
|
212
|
+
assert.equal(loopKeyboard.inline_keyboard?.[0]?.[0]?.callback_data, "v1:admin:status");
|
|
213
|
+
assert.equal(loopKeyboard.inline_keyboard?.[0]?.[1]?.callback_data, "v1:admin:providers");
|
|
214
|
+
assert.equal(loopKeyboard.inline_keyboard?.[2]?.[1]?.callback_data, "v1:admin:menu");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
test("admin start panel can show billing plans and renewal requests", async () => {
|
|
218
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, deps }) => {
|
|
219
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
220
|
+
ownerTelegramUserId: "42",
|
|
221
|
+
defaultClientRoute: "customers",
|
|
222
|
+
status: "active",
|
|
223
|
+
});
|
|
224
|
+
billing.createRenewalRequest({
|
|
225
|
+
workspaceId: workspace.id,
|
|
226
|
+
telegramUserId: "42",
|
|
227
|
+
requestedPlanId: "basic",
|
|
228
|
+
requestedDays: 30,
|
|
229
|
+
});
|
|
230
|
+
const harness = createBotHarness();
|
|
231
|
+
registerStartCommand(harness.bot, deps, identities, workspaces, customerKeys, billing);
|
|
232
|
+
const plans = harness.callbackHandler("v1:admin:plans");
|
|
233
|
+
const plansCtx = createContext({ fromId: 1, chatId: 1 });
|
|
234
|
+
plansCtx.match = plans.match;
|
|
235
|
+
await plans.handler(plansCtx);
|
|
236
|
+
assert.equal(plansCtx.editedReplies[0]?.text.includes("Billing plans:"), true);
|
|
237
|
+
assert.equal(plansCtx.editedReplies[0]?.text.includes("price_cents=5000"), true);
|
|
238
|
+
assert.equal(plansCtx.editedReplies[0]?.text.includes("currency=VND"), true);
|
|
239
|
+
const plansKeyboard = JSON.parse(JSON.stringify(plansCtx.editedReplies[0]?.options?.reply_markup));
|
|
240
|
+
assert.equal(plansKeyboard.inline_keyboard?.[0]?.[0]?.callback_data, "v1:admin:plans");
|
|
241
|
+
assert.equal(plansKeyboard.inline_keyboard?.[0]?.[1]?.callback_data, "v1:admin:renewals");
|
|
242
|
+
const renewals = harness.callbackHandler("v1:admin:renewals");
|
|
243
|
+
const renewalsCtx = createContext({ fromId: 1, chatId: 1 });
|
|
244
|
+
renewalsCtx.match = renewals.match;
|
|
245
|
+
await renewals.handler(renewalsCtx);
|
|
246
|
+
assert.equal(renewalsCtx.editedReplies[0]?.text.includes("Open renewal requests:"), true);
|
|
247
|
+
assert.equal(renewalsCtx.editedReplies[0]?.text.includes("telegram_user_id=42"), true);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
test("admin start panel shows recent API keys with manage buttons", async () => {
|
|
251
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, deps }) => {
|
|
252
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
253
|
+
ownerTelegramUserId: "42",
|
|
254
|
+
defaultClientRoute: "customers",
|
|
255
|
+
status: "active",
|
|
256
|
+
});
|
|
257
|
+
const created = customerKeys.createKey({
|
|
258
|
+
workspaceId: workspace.id,
|
|
259
|
+
telegramUserId: "42",
|
|
260
|
+
clientRoute: "customers",
|
|
261
|
+
});
|
|
262
|
+
const harness = createBotHarness();
|
|
263
|
+
registerStartCommand(harness.bot, deps, identities, workspaces, customerKeys, billing);
|
|
264
|
+
const found = harness.callbackHandler("v1:admin:apikeys");
|
|
265
|
+
const ctx = createContext({ fromId: 1, chatId: 1 });
|
|
266
|
+
ctx.match = found.match;
|
|
267
|
+
await found.handler(ctx);
|
|
268
|
+
assert.equal(ctx.editedReplies[0]?.text.includes("Customer API keys"), true);
|
|
269
|
+
assert.equal(ctx.editedReplies[0]?.text.includes(`user=42`), true);
|
|
270
|
+
assert.equal(ctx.editedReplies[0]?.text.includes(created.record.apiKeyPreview), true);
|
|
271
|
+
const keyKeyboard = JSON.parse(JSON.stringify(ctx.editedReplies[0]?.options?.reply_markup));
|
|
272
|
+
assert.equal(keyKeyboard.inline_keyboard?.[0]?.[0]?.copy_text?.text, created.record.id);
|
|
273
|
+
const callbackButtons = keyKeyboard.inline_keyboard
|
|
274
|
+
?.flatMap((row) => row)
|
|
275
|
+
.filter((button) => typeof button?.callback_data === "string")
|
|
276
|
+
.map((button) => button.callback_data);
|
|
277
|
+
assert.equal(callbackButtons.includes("v1:admin:apikeys"), true);
|
|
278
|
+
assert.equal(callbackButtons.includes("v1:admin:plans"), true);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function registerTailscaleCommand(bot, deps) {
|
|
2
|
+
bot.command("tailscale", async (ctx) => {
|
|
3
|
+
const lines = [
|
|
4
|
+
"Join Tailscale",
|
|
5
|
+
"1. Install Tailscale on your device:",
|
|
6
|
+
"https://tailscale.com/download",
|
|
7
|
+
"2. Sign in to Tailscale on your device.",
|
|
8
|
+
"3. Inbox admin in Telegram to get a fresh invite link for the private network.",
|
|
9
|
+
"4. Open that invite link, join the tailnet, then connect Tailscale.",
|
|
10
|
+
`5. Use this Responses base URL after you are connected:\n${deps.config.publicResponsesBaseUrl}`,
|
|
11
|
+
"6. Run /apikey in this chat to check your key status.",
|
|
12
|
+
];
|
|
13
|
+
await ctx.reply(lines.join("\n"));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerTailscaleCommand } from "./tailscale.js";
|
|
4
|
+
function createConfig(overrides = {}) {
|
|
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
|
+
webhookUrl: undefined,
|
|
13
|
+
webhookSecret: undefined,
|
|
14
|
+
proxyAdminBaseUrl: "http://127.0.0.1:8318",
|
|
15
|
+
proxyClientApiKey: undefined,
|
|
16
|
+
defaultModel: "gpt-5.5",
|
|
17
|
+
publicSignupEnabled: true,
|
|
18
|
+
requireAdminApproval: false,
|
|
19
|
+
defaultCustomerRoute: "customers",
|
|
20
|
+
publicResponsesBaseUrl: "https://example.tailnet.ts.net/v1",
|
|
21
|
+
proxyRequestTimeoutMs: 30_000,
|
|
22
|
+
sessionDbPath: ":memory:",
|
|
23
|
+
sessionTtlMs: 900_000,
|
|
24
|
+
rateLimitWindowMs: 60_000,
|
|
25
|
+
rateLimitMaxRequests: 12,
|
|
26
|
+
logLevel: "info",
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function createBotHarness() {
|
|
31
|
+
const handlers = new Map();
|
|
32
|
+
return {
|
|
33
|
+
bot: {
|
|
34
|
+
command(name, handler) {
|
|
35
|
+
handlers.set(name, handler);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
handler(name) {
|
|
39
|
+
const handler = handlers.get(name);
|
|
40
|
+
assert.ok(handler);
|
|
41
|
+
return handler;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function createContext() {
|
|
46
|
+
const replies = [];
|
|
47
|
+
return {
|
|
48
|
+
from: { id: 42, is_bot: false, first_name: "User" },
|
|
49
|
+
chat: { id: 42, type: "private", first_name: "User" },
|
|
50
|
+
message: {
|
|
51
|
+
message_id: 1,
|
|
52
|
+
date: 0,
|
|
53
|
+
chat: { id: 42, type: "private", first_name: "User" },
|
|
54
|
+
text: "/tailscale",
|
|
55
|
+
},
|
|
56
|
+
replies,
|
|
57
|
+
reply(text) {
|
|
58
|
+
replies.push(text);
|
|
59
|
+
return Promise.resolve({});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
test("tailscale command tells customer to ask admin for a fresh invite link", async () => {
|
|
64
|
+
const deps = {
|
|
65
|
+
config: createConfig(),
|
|
66
|
+
proxyClient: {},
|
|
67
|
+
};
|
|
68
|
+
const harness = createBotHarness();
|
|
69
|
+
registerTailscaleCommand(harness.bot, deps);
|
|
70
|
+
const ctx = createContext();
|
|
71
|
+
await harness.handler("tailscale")(ctx);
|
|
72
|
+
assert.equal(ctx.replies.length, 1);
|
|
73
|
+
assert.equal(ctx.replies[0].includes("Inbox admin in Telegram to get a fresh invite link"), true);
|
|
74
|
+
assert.equal(ctx.replies[0].includes("https://example.tailnet.ts.net/v1"), true);
|
|
75
|
+
assert.equal(ctx.replies[0].includes("https://login.tailscale.com/"), false);
|
|
76
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { sendTestResult } from "../actions.js";
|
|
2
|
+
import { buildTelegramSessionScope } from "../sessions.js";
|
|
3
|
+
export function registerTestCommand(bot, deps, sessions) {
|
|
4
|
+
bot.command("test", async (ctx) => {
|
|
5
|
+
const args = ctx.match?.toString().trim() || "";
|
|
6
|
+
if (args) {
|
|
7
|
+
const parsed = parseTestArgs(args);
|
|
8
|
+
await sendTestResult(ctx, deps, parsed);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const chatId = ctx.chat?.id?.toString();
|
|
12
|
+
const userId = ctx.from?.id?.toString();
|
|
13
|
+
if (chatId && userId) {
|
|
14
|
+
sessions.set(buildTelegramSessionScope(chatId, userId), { kind: "awaiting_test_prompt" });
|
|
15
|
+
}
|
|
16
|
+
await ctx.reply("Send the test prompt text you want to run through /v1/responses.");
|
|
17
|
+
});
|
|
18
|
+
bot.on("message:text", async (ctx, next) => {
|
|
19
|
+
const chatId = ctx.chat?.id?.toString();
|
|
20
|
+
const userId = ctx.from?.id?.toString();
|
|
21
|
+
if (!chatId || !userId) {
|
|
22
|
+
await next();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const scope = buildTelegramSessionScope(chatId, userId);
|
|
26
|
+
const session = sessions.get(scope);
|
|
27
|
+
if (session?.kind !== "awaiting_test_prompt") {
|
|
28
|
+
await next();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (ctx.message.text.startsWith("/")) {
|
|
32
|
+
await next();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
sessions.clear(scope);
|
|
36
|
+
await sendTestResult(ctx, deps, {
|
|
37
|
+
prompt: ctx.message.text.trim(),
|
|
38
|
+
model: session.model,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function parseTestArgs(args) {
|
|
43
|
+
const match = args.match(/^--provider-id\s+(\S+)\s+([\s\S]+)$/);
|
|
44
|
+
if (!match) {
|
|
45
|
+
return { prompt: args };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
providerId: match[1],
|
|
49
|
+
prompt: match[2].trim(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseTestArgs } from "./test.js";
|
|
4
|
+
test("parseTestArgs returns raw prompt when no provider flag is present", () => {
|
|
5
|
+
assert.deepEqual(parseTestArgs("Say hello"), {
|
|
6
|
+
prompt: "Say hello",
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
test("parseTestArgs extracts provider id and prompt", () => {
|
|
10
|
+
assert.deepEqual(parseTestArgs("--provider-id account-openai-codex Say hello"), {
|
|
11
|
+
providerId: "account-openai-codex",
|
|
12
|
+
prompt: "Say hello",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { replyWithCustomerView } from "../customer-actions.js";
|
|
2
|
+
export function registerUsageCommand(bot, workspaces, customerKeys, billing) {
|
|
3
|
+
bot.command("usage", async (ctx) => {
|
|
4
|
+
if (ctx.chat?.type !== "private") {
|
|
5
|
+
await ctx.reply("For safety, open a private chat with this bot and run /usage there.");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
await replyWithCustomerView(ctx, "usage", workspaces, customerKeys, billing);
|
|
9
|
+
});
|
|
10
|
+
}
|