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,217 @@
|
|
|
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 { AuditLogRepository } from "../../audit-log.js";
|
|
9
|
+
import { BotIdentityRepository } from "../bot-identity-repository.js";
|
|
10
|
+
import { CustomerWorkspaceRepository } from "../customer-workspace-repository.js";
|
|
11
|
+
import { registerGrantCommand } from "./grant.js";
|
|
12
|
+
import { registerPlansCommand } from "./plans.js";
|
|
13
|
+
function createConfig(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
telegramBotToken: "token",
|
|
16
|
+
allowedUserIds: new Set(),
|
|
17
|
+
allowedChatIds: new Set(),
|
|
18
|
+
ownerUserIds: new Set(["1"]),
|
|
19
|
+
adminUserIds: new Set(),
|
|
20
|
+
botMode: "polling",
|
|
21
|
+
proxyAdminBaseUrl: "http://127.0.0.1:8318",
|
|
22
|
+
defaultModel: "gpt-5.5",
|
|
23
|
+
publicSignupEnabled: true,
|
|
24
|
+
requireAdminApproval: false,
|
|
25
|
+
defaultCustomerRoute: "customers",
|
|
26
|
+
publicResponsesBaseUrl: "http://127.0.0.1:8318/v1",
|
|
27
|
+
proxyRequestTimeoutMs: 30_000,
|
|
28
|
+
sessionDbPath: ":memory:",
|
|
29
|
+
sessionTtlMs: 900_000,
|
|
30
|
+
rateLimitWindowMs: 60_000,
|
|
31
|
+
rateLimitMaxRequests: 12,
|
|
32
|
+
logLevel: "info",
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createMockProxyClient() {
|
|
37
|
+
return {
|
|
38
|
+
async getClientConfigs() {
|
|
39
|
+
return {
|
|
40
|
+
clientRoutes: [{ key: "customers", apiKeys: [] }],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
async setClientRouteApiKeys() {
|
|
44
|
+
return { ok: true };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createBotHarness() {
|
|
49
|
+
const handlers = new Map();
|
|
50
|
+
return {
|
|
51
|
+
bot: {
|
|
52
|
+
command(name, handler) {
|
|
53
|
+
handlers.set(name, handler);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
handler(name) {
|
|
57
|
+
const handler = handlers.get(name);
|
|
58
|
+
assert.ok(handler);
|
|
59
|
+
return handler;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function createContext(input) {
|
|
64
|
+
const replies = [];
|
|
65
|
+
const sentMessages = [];
|
|
66
|
+
const sentDocuments = [];
|
|
67
|
+
return {
|
|
68
|
+
from: { id: input.fromId, is_bot: false, first_name: "Admin" },
|
|
69
|
+
chat: input.chatType === "private"
|
|
70
|
+
? { id: input.chatId, type: "private", first_name: "Admin" }
|
|
71
|
+
: { id: input.chatId, type: "group", title: "Ops" },
|
|
72
|
+
message: {
|
|
73
|
+
message_id: 1,
|
|
74
|
+
date: 0,
|
|
75
|
+
chat: input.chatType === "private"
|
|
76
|
+
? { id: input.chatId, type: "private", first_name: "Admin" }
|
|
77
|
+
: { id: input.chatId, type: "group", title: "Ops" },
|
|
78
|
+
text: `/${input.command} ${input.match}`.trim(),
|
|
79
|
+
},
|
|
80
|
+
match: input.match,
|
|
81
|
+
replies,
|
|
82
|
+
sentMessages,
|
|
83
|
+
sentDocuments,
|
|
84
|
+
reply(text) {
|
|
85
|
+
replies.push(text);
|
|
86
|
+
return Promise.resolve({});
|
|
87
|
+
},
|
|
88
|
+
api: {
|
|
89
|
+
async sendMessage(chatId, text) {
|
|
90
|
+
sentMessages.push({ chatId, text });
|
|
91
|
+
return {};
|
|
92
|
+
},
|
|
93
|
+
async sendDocument(chatId, document) {
|
|
94
|
+
sentDocuments.push({
|
|
95
|
+
chatId,
|
|
96
|
+
filename: document.filename,
|
|
97
|
+
content: document.fileData ? Buffer.from(document.fileData).toString("utf8") : "",
|
|
98
|
+
});
|
|
99
|
+
return {};
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function withRepos(fn) {
|
|
105
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "grant-command-"));
|
|
106
|
+
try {
|
|
107
|
+
const dbFile = path.join(dir, "bot.sqlite");
|
|
108
|
+
await fn({
|
|
109
|
+
identities: BotIdentityRepository.create(dbFile),
|
|
110
|
+
workspaces: CustomerWorkspaceRepository.create(dbFile),
|
|
111
|
+
customerKeys: CustomerKeyRepository.create(dbFile),
|
|
112
|
+
billing: BillingRepository.create(dbFile),
|
|
113
|
+
auditLog: AuditLogRepository.create(dbFile),
|
|
114
|
+
deps: {
|
|
115
|
+
config: createConfig({ sessionDbPath: dbFile }),
|
|
116
|
+
proxyClient: createMockProxyClient(),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
rmSync(dir, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
test("plans lists available seeded plan ids for admins", async () => {
|
|
125
|
+
await withRepos(async ({ billing, deps }) => {
|
|
126
|
+
const harness = createBotHarness();
|
|
127
|
+
registerPlansCommand(harness.bot, deps, billing);
|
|
128
|
+
const ctx = createContext({
|
|
129
|
+
command: "plans",
|
|
130
|
+
fromId: 1,
|
|
131
|
+
chatId: 1,
|
|
132
|
+
chatType: "private",
|
|
133
|
+
match: "",
|
|
134
|
+
});
|
|
135
|
+
await harness.handler("plans")(ctx);
|
|
136
|
+
assert.equal(ctx.replies.length, 1);
|
|
137
|
+
assert.equal(ctx.replies[0].includes("• basic:"), true);
|
|
138
|
+
assert.equal(ctx.replies[0].includes("price_cents="), true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
test("plans create adds a new admin-created plan", async () => {
|
|
142
|
+
await withRepos(async ({ billing, deps }) => {
|
|
143
|
+
const harness = createBotHarness();
|
|
144
|
+
registerPlansCommand(harness.bot, deps, billing);
|
|
145
|
+
const ctx = createContext({
|
|
146
|
+
command: "plans",
|
|
147
|
+
fromId: 1,
|
|
148
|
+
chatId: 1,
|
|
149
|
+
chatType: "private",
|
|
150
|
+
match: "create premium Premium 2500000 2 2900 USD month",
|
|
151
|
+
});
|
|
152
|
+
await harness.handler("plans")(ctx);
|
|
153
|
+
const plan = billing.getPlan("premium");
|
|
154
|
+
assert.ok(plan);
|
|
155
|
+
assert.equal(plan.name, "Premium");
|
|
156
|
+
assert.equal(plan.monthlyTokenLimit, 2_500_000);
|
|
157
|
+
assert.equal(plan.maxApiKeys, 2);
|
|
158
|
+
assert.equal(plan.priceCents, 2_900);
|
|
159
|
+
assert.equal(plan.currency, "USD");
|
|
160
|
+
assert.equal(plan.billingInterval, "month");
|
|
161
|
+
assert.equal(ctx.replies[0].includes("Created plan premium"), true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
test("plans create rejects duplicate plan ids", async () => {
|
|
165
|
+
await withRepos(async ({ deps, billing }) => {
|
|
166
|
+
const harness = createBotHarness();
|
|
167
|
+
registerPlansCommand(harness.bot, deps, billing);
|
|
168
|
+
const ctx = createContext({
|
|
169
|
+
command: "plans",
|
|
170
|
+
fromId: 1,
|
|
171
|
+
chatId: 1,
|
|
172
|
+
chatType: "private",
|
|
173
|
+
match: "create basic Basic 10000000 1 5000 VND month",
|
|
174
|
+
});
|
|
175
|
+
await harness.handler("plans")(ctx);
|
|
176
|
+
assert.equal(ctx.replies[0], "Plan already exists: basic");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
test("grant sends Codex config files to the customer", async () => {
|
|
180
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, auditLog, deps }) => {
|
|
181
|
+
const harness = createBotHarness();
|
|
182
|
+
registerGrantCommand(harness.bot, deps, identities, workspaces, customerKeys, billing, auditLog);
|
|
183
|
+
const ctx = createContext({
|
|
184
|
+
command: "grant",
|
|
185
|
+
fromId: 1,
|
|
186
|
+
chatId: 1,
|
|
187
|
+
chatType: "private",
|
|
188
|
+
match: "42 basic 30",
|
|
189
|
+
});
|
|
190
|
+
await harness.handler("grant")(ctx);
|
|
191
|
+
assert.equal(customerKeys.getActiveKeyForUser("42")?.status, "active");
|
|
192
|
+
assert.equal(ctx.sentMessages[0]?.chatId, 42);
|
|
193
|
+
assert.equal(ctx.sentMessages[0]?.text.includes("Your access is active"), true);
|
|
194
|
+
assert.deepEqual(ctx.sentDocuments.map((document) => document.filename), ["config.toml", "auth.json"]);
|
|
195
|
+
assert.match(ctx.sentDocuments[0]?.content ?? "", /base_url = "http:\/\/127\.0\.0\.1:8318\/v1"/);
|
|
196
|
+
assert.match(ctx.sentDocuments[0]?.content ?? "", /api_key = "sk-/);
|
|
197
|
+
assert.match(ctx.sentDocuments[1]?.content ?? "", /"OPENAI_API_KEY": "sk-/);
|
|
198
|
+
assert.equal(auditLog.listEvents({ event: "api_key.revealed", limit: 5 }).length, 2);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
test("grant suggests valid plan ids when planId is unknown", async () => {
|
|
202
|
+
await withRepos(async ({ identities, workspaces, customerKeys, billing, auditLog, deps }) => {
|
|
203
|
+
const harness = createBotHarness();
|
|
204
|
+
registerGrantCommand(harness.bot, deps, identities, workspaces, customerKeys, billing, auditLog);
|
|
205
|
+
const ctx = createContext({
|
|
206
|
+
command: "grant",
|
|
207
|
+
fromId: 1,
|
|
208
|
+
chatId: 1,
|
|
209
|
+
chatType: "private",
|
|
210
|
+
match: "42 unknown 30",
|
|
211
|
+
});
|
|
212
|
+
await harness.handler("grant")(ctx);
|
|
213
|
+
assert.equal(ctx.replies.length, 1);
|
|
214
|
+
assert.equal(ctx.replies[0].includes("Unknown planId: unknown"), true);
|
|
215
|
+
assert.equal(ctx.replies[0].includes("Available planIds: basic"), true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isAdmin } from "../auth.js";
|
|
2
|
+
export function registerHelpCommand(bot, deps) {
|
|
3
|
+
bot.command("help", async (ctx) => {
|
|
4
|
+
const topic = (ctx.match?.toString() ?? "").trim().toLowerCase();
|
|
5
|
+
const lines = isAdmin(ctx, deps.config)
|
|
6
|
+
? topic === "proxy"
|
|
7
|
+
? [
|
|
8
|
+
"Proxy Maintenance:",
|
|
9
|
+
"/status - health and active provider",
|
|
10
|
+
"/providers - provider list and client routes",
|
|
11
|
+
"/clients - Hermes and Codex config status",
|
|
12
|
+
"/models - list models through proxy routing",
|
|
13
|
+
"/apply <hermes|codex> <model> [routeApiKey] - quick apply config",
|
|
14
|
+
"/oauth - show OAuth status and start account connect flow",
|
|
15
|
+
"/accounts - list OAuth accounts",
|
|
16
|
+
"/test [prompt] - send a small Responses API request",
|
|
17
|
+
]
|
|
18
|
+
: topic === "customer"
|
|
19
|
+
? [
|
|
20
|
+
"Customer Commands:",
|
|
21
|
+
"/me - show your Telegram account and workspace status",
|
|
22
|
+
"/apikey - show your Responses API key",
|
|
23
|
+
"/usage - show token usage for your current period",
|
|
24
|
+
"/quota - show token limit, remaining balance, and expiration",
|
|
25
|
+
"/renew - request a 24h renewal from admin",
|
|
26
|
+
"/tailscale - install Tailscale and ask admin for a fresh invite",
|
|
27
|
+
]
|
|
28
|
+
: [
|
|
29
|
+
"Admin Ops:",
|
|
30
|
+
"/plans - list billing plan ids, limits, and pricing",
|
|
31
|
+
"/plans create <planId> <name> <monthlyTokenLimit> <maxApiKeys> [priceCents] [currency] [billingInterval] - create a billing plan",
|
|
32
|
+
"/grant <telegramUserId> <planId> <days> - activate customer access",
|
|
33
|
+
"/renewuser <telegramUserId> <planId> <days> [replace-key] - renew customer access",
|
|
34
|
+
"/renew list | approve | close - manage renewal requests",
|
|
35
|
+
"/apikey issue <telegramUserId> [clientRoute] [apiKey] - issue a customer key",
|
|
36
|
+
"",
|
|
37
|
+
"More:",
|
|
38
|
+
"/help customer - show customer-facing commands",
|
|
39
|
+
"/help proxy - show proxy maintenance commands",
|
|
40
|
+
]
|
|
41
|
+
: [
|
|
42
|
+
"Available commands:",
|
|
43
|
+
"/me - show your Telegram account and workspace status",
|
|
44
|
+
"/apikey - show your Responses API key",
|
|
45
|
+
"/usage - show token usage for your current period",
|
|
46
|
+
"/quota - show token limit, remaining balance, and expiration",
|
|
47
|
+
"/renew - request a 24h renewal from admin",
|
|
48
|
+
"/tailscale - install Tailscale and ask admin for a fresh invite",
|
|
49
|
+
];
|
|
50
|
+
await ctx.reply(lines.join("\n"));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { renderCustomerActionText } from "../customer-actions.js";
|
|
2
|
+
import { formatField, formatSection } from "../message-format.js";
|
|
3
|
+
export function registerMeCommand(bot, identities, workspaces, customerKeys, auditLog) {
|
|
4
|
+
bot.command("me", async (ctx) => {
|
|
5
|
+
const userId = ctx.from?.id?.toString();
|
|
6
|
+
const chatId = ctx.chat?.id?.toString();
|
|
7
|
+
const user = userId ? identities.getUser(userId) : undefined;
|
|
8
|
+
const workspace = userId ? workspaces.getDefaultWorkspace(userId) : undefined;
|
|
9
|
+
const keyRecord = userId
|
|
10
|
+
? customerKeys.getActiveKeyForUser(userId) ?? customerKeys.getLatestKeyForUser(userId)
|
|
11
|
+
: undefined;
|
|
12
|
+
const canShowApiKey = ctx.chat?.type === "private" && !!keyRecord;
|
|
13
|
+
const apiKey = keyRecord && canShowApiKey ? customerKeys.getApiKeySecret(keyRecord.id) : undefined;
|
|
14
|
+
if (apiKey && keyRecord) {
|
|
15
|
+
auditLog.record({
|
|
16
|
+
event: "api_key.revealed",
|
|
17
|
+
actor: { type: "customer", id: userId },
|
|
18
|
+
subjectType: "customer_api_key",
|
|
19
|
+
subjectId: keyRecord.id,
|
|
20
|
+
metadata: {
|
|
21
|
+
telegramUserId: userId,
|
|
22
|
+
workspaceId: keyRecord.workspaceId,
|
|
23
|
+
keyPreview: keyRecord.apiKeyPreview,
|
|
24
|
+
audience: "customer_account_page",
|
|
25
|
+
apiKey,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
await renderCustomerActionText(ctx, [
|
|
30
|
+
"👤 Your account",
|
|
31
|
+
formatSection("Telegram", [
|
|
32
|
+
formatField("User ID", userId ?? "unknown"),
|
|
33
|
+
formatField("Chat ID", chatId ?? "unknown"),
|
|
34
|
+
formatField("Role", user?.role ?? "unknown"),
|
|
35
|
+
formatField("Account status", formatStatus(user?.status ?? "unknown")),
|
|
36
|
+
]),
|
|
37
|
+
formatSection("Workspace", [
|
|
38
|
+
formatField("ID", workspace?.id ?? "none"),
|
|
39
|
+
workspace ? formatField("Status", formatStatus(workspace.status)) : undefined,
|
|
40
|
+
workspace ? formatField("Client route", workspace.defaultClientRoute) : undefined,
|
|
41
|
+
]),
|
|
42
|
+
formatSection("API key", [
|
|
43
|
+
keyRecord ? formatField("Status", formatStatus(keyRecord.status)) : formatField("Status", "none"),
|
|
44
|
+
keyRecord ? formatField("Preview", keyRecord.apiKeyPreview) : undefined,
|
|
45
|
+
apiKey ? `api_key: ${apiKey}` : undefined,
|
|
46
|
+
keyRecord && canShowApiKey && !apiKey ? "Full key: unavailable for legacy key" : undefined,
|
|
47
|
+
]),
|
|
48
|
+
].join("\n\n"), keyRecord?.status === "active");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function formatStatus(value) {
|
|
52
|
+
return value.replace(/_/g, " ");
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { replyWithProxyError, sendOauthStatus } from "../actions.js";
|
|
3
|
+
import { answerCallbackQuerySafely } from "../callbacks.js";
|
|
4
|
+
import { buildTelegramSessionScope } from "../sessions.js";
|
|
5
|
+
export function registerOauthCommand(bot, deps, sessions) {
|
|
6
|
+
bot.command("oauth", async (ctx) => {
|
|
7
|
+
await sendOauthStatus(ctx, deps);
|
|
8
|
+
await ctx.reply("Choose an OAuth action.", {
|
|
9
|
+
reply_markup: new InlineKeyboard().text("Add Account", "oauth:start"),
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
bot.callbackQuery("oauth:start", async (ctx) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await deps.proxyClient.startOauth();
|
|
15
|
+
const chatId = ctx.chat?.id?.toString();
|
|
16
|
+
const userId = ctx.from?.id?.toString();
|
|
17
|
+
if (chatId && userId) {
|
|
18
|
+
sessions.set(buildTelegramSessionScope(chatId, userId), { kind: "awaiting_oauth_callback" });
|
|
19
|
+
}
|
|
20
|
+
await answerCallbackQuerySafely(ctx);
|
|
21
|
+
await ctx.reply([
|
|
22
|
+
"Open this URL and complete sign-in:",
|
|
23
|
+
result?.authUrl ?? "Missing auth URL",
|
|
24
|
+
"",
|
|
25
|
+
"Warning: the callback URL may contain short-lived authorization material.",
|
|
26
|
+
"Only paste it into this authorized bot chat.",
|
|
27
|
+
"",
|
|
28
|
+
"Then paste the full callback URL into this chat.",
|
|
29
|
+
].join("\n"));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
await answerCallbackQuerySafely(ctx);
|
|
33
|
+
await replyWithProxyError(ctx, error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
bot.on("message:text", async (ctx, next) => {
|
|
37
|
+
const chatId = ctx.chat?.id?.toString();
|
|
38
|
+
const userId = ctx.from?.id?.toString();
|
|
39
|
+
if (!chatId || !userId) {
|
|
40
|
+
await next();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const session = sessions.get(buildTelegramSessionScope(chatId, userId));
|
|
44
|
+
if (session?.kind !== "awaiting_oauth_callback") {
|
|
45
|
+
await next();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (ctx.message.text.startsWith("/")) {
|
|
49
|
+
await next();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const result = await deps.proxyClient.completeOauth(ctx.message.text.trim());
|
|
54
|
+
sessions.clear(buildTelegramSessionScope(chatId, userId));
|
|
55
|
+
await ctx.reply("OAuth account connected.");
|
|
56
|
+
if (result?.accounts) {
|
|
57
|
+
await sendOauthStatus(ctx, deps);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await replyWithProxyError(ctx, error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isAdmin } from "../auth.js";
|
|
2
|
+
function formatPlans(plans) {
|
|
3
|
+
if (plans.length === 0) {
|
|
4
|
+
return "💳 Billing plans:\nNo billing plans are configured yet.";
|
|
5
|
+
}
|
|
6
|
+
return [
|
|
7
|
+
"💳 Billing plans:",
|
|
8
|
+
...plans.map((plan) => `• ${plan.id}: ${plan.name} | status=${plan.status} | monthly_token_limit=${plan.monthlyTokenLimit} | max_api_keys=${plan.maxApiKeys} | price_cents=${plan.priceCents} | billing_interval=${plan.billingInterval}`),
|
|
9
|
+
].join("\n");
|
|
10
|
+
}
|
|
11
|
+
function formatCreatePlanUsage() {
|
|
12
|
+
return [
|
|
13
|
+
"💳 Plan commands",
|
|
14
|
+
"Usage: /plans create <planId> <name> <monthlyTokenLimit> <maxApiKeys> [priceCents] [currency] [billingInterval]",
|
|
15
|
+
"Example: /plans create pro Pro 5000000 3 4900 USD month",
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
18
|
+
function parseCreatePlanArgs(rawArgs) {
|
|
19
|
+
const parts = rawArgs.trim().split(/\s+/g).filter(Boolean);
|
|
20
|
+
if (parts.length < 4) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const [id] = parts;
|
|
24
|
+
let end = parts.length;
|
|
25
|
+
const billingIntervalCandidate = parts[end - 1];
|
|
26
|
+
const hasBillingInterval = billingIntervalCandidate === "month" || billingIntervalCandidate === "year" || billingIntervalCandidate === "one_time";
|
|
27
|
+
const billingInterval = hasBillingInterval ? billingIntervalCandidate : "month";
|
|
28
|
+
if (hasBillingInterval) {
|
|
29
|
+
end -= 1;
|
|
30
|
+
}
|
|
31
|
+
const currencyCandidate = parts[end - 1];
|
|
32
|
+
const hasCurrency = /^[A-Za-z]{3}$/.test(currencyCandidate);
|
|
33
|
+
const currency = hasCurrency ? currencyCandidate.toUpperCase() : "USD";
|
|
34
|
+
if (hasCurrency) {
|
|
35
|
+
end -= 1;
|
|
36
|
+
}
|
|
37
|
+
let numericStart = end;
|
|
38
|
+
while (numericStart > 1 && /^\d+$/.test(parts[numericStart - 1])) {
|
|
39
|
+
numericStart -= 1;
|
|
40
|
+
}
|
|
41
|
+
const numericParts = parts.slice(numericStart, end);
|
|
42
|
+
if (numericParts.length !== 2 && numericParts.length !== 3) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const [monthlyTokenLimitRaw, maxApiKeysRaw, priceCentsRaw] = numericParts;
|
|
46
|
+
const name = parts.slice(1, numericStart).join(" ");
|
|
47
|
+
if (!name) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const monthlyTokenLimit = Number(monthlyTokenLimitRaw);
|
|
51
|
+
const maxApiKeys = Number(maxApiKeysRaw);
|
|
52
|
+
const priceCents = priceCentsRaw ? Number(priceCentsRaw) : 0;
|
|
53
|
+
if (!Number.isInteger(monthlyTokenLimit) ||
|
|
54
|
+
monthlyTokenLimit <= 0 ||
|
|
55
|
+
!Number.isInteger(maxApiKeys) ||
|
|
56
|
+
maxApiKeys <= 0 ||
|
|
57
|
+
!Number.isInteger(priceCents) ||
|
|
58
|
+
priceCents < 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { id, name, monthlyTokenLimit, maxApiKeys, priceCents, currency, billingInterval };
|
|
62
|
+
}
|
|
63
|
+
export function registerPlansCommand(bot, deps, billing) {
|
|
64
|
+
bot.command("plans", async (ctx) => {
|
|
65
|
+
if (!isAdmin(ctx, deps.config)) {
|
|
66
|
+
await ctx.reply("Only admins can view billing plans.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const rawArgs = (ctx.match?.toString() || "").trim();
|
|
70
|
+
if (rawArgs.startsWith("create")) {
|
|
71
|
+
const createArgs = rawArgs.slice("create".length).trim();
|
|
72
|
+
const parsed = parseCreatePlanArgs(createArgs);
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
await ctx.reply(formatCreatePlanUsage());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (billing.getPlan(parsed.id)) {
|
|
78
|
+
await ctx.reply(`Plan already exists: ${parsed.id}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const created = billing.createPlan(parsed);
|
|
82
|
+
await ctx.reply([
|
|
83
|
+
`Created plan ${created.id}`,
|
|
84
|
+
`name=${created.name}`,
|
|
85
|
+
`status=${created.status}`,
|
|
86
|
+
`monthly_token_limit=${created.monthlyTokenLimit}`,
|
|
87
|
+
`max_api_keys=${created.maxApiKeys}`,
|
|
88
|
+
`price_cents=${created.priceCents}`,
|
|
89
|
+
`currency=${created.currency}`,
|
|
90
|
+
`billing_interval=${created.billingInterval}`,
|
|
91
|
+
].join("\n"));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await ctx.reply(formatPlans(billing.listPlans()));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { sendProviderDetails, sendProviders } from "../actions.js";
|
|
3
|
+
import { answerCallbackQuerySafely } from "../callbacks.js";
|
|
4
|
+
export function registerProvidersCommand(bot, deps) {
|
|
5
|
+
bot.command("providers", async (ctx) => {
|
|
6
|
+
await sendProviders(ctx, deps);
|
|
7
|
+
try {
|
|
8
|
+
const payload = await deps.proxyClient.getProviders();
|
|
9
|
+
for (const provider of payload?.providers ?? []) {
|
|
10
|
+
await ctx.reply(`${provider.name} (${provider.id})`, {
|
|
11
|
+
reply_markup: new InlineKeyboard()
|
|
12
|
+
.text("Details", `v1:provider:details:${provider.id}`)
|
|
13
|
+
.row()
|
|
14
|
+
.text("Apply Hermes", `v1:apply:start:hermes:${provider.id}`)
|
|
15
|
+
.text("Apply Codex", `v1:apply:start:codex:${provider.id}`),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
bot.callbackQuery(/^v1:provider:details:(.+)$/, async (ctx) => {
|
|
24
|
+
await answerCallbackQuerySafely(ctx);
|
|
25
|
+
await sendProviderDetails(ctx, deps, ctx.match[1]);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { replyWithCustomerView } from "../customer-actions.js";
|
|
2
|
+
export function registerQuotaCommand(bot, workspaces, customerKeys, billing) {
|
|
3
|
+
bot.command("quota", async (ctx) => {
|
|
4
|
+
if (ctx.chat?.type !== "private") {
|
|
5
|
+
await ctx.reply("For safety, open a private chat with this bot and run /quota there.");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
await replyWithCustomerView(ctx, "quota", workspaces, customerKeys, billing);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { isAdmin } from "../auth.js";
|
|
2
|
+
import { maskApiKey } from "../format.js";
|
|
3
|
+
import { renewCustomerAccess } from "../grants.js";
|
|
4
|
+
import { replyWithProxyError } from "../actions.js";
|
|
5
|
+
import { formatField, formatSection } from "../message-format.js";
|
|
6
|
+
import { sendCustomerCodexSetup } from "../codex-config-delivery.js";
|
|
7
|
+
export function registerRenewUserCommand(bot, deps, identities, workspaces, customerKeys, billing, auditLog) {
|
|
8
|
+
bot.command("renewuser", async (ctx) => {
|
|
9
|
+
if (!isAdmin(ctx, deps.config)) {
|
|
10
|
+
await ctx.reply("Only admins can renew customer access.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const parsed = parseRenewUserArgs(ctx.match?.toString() || "");
|
|
14
|
+
if (!parsed) {
|
|
15
|
+
await ctx.reply("Usage: /renewuser <telegramUserId> <planId> <days> [replace-key]");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const result = await renewCustomerAccess({
|
|
20
|
+
telegramUserId: parsed.telegramUserId,
|
|
21
|
+
planId: parsed.planId,
|
|
22
|
+
days: parsed.days,
|
|
23
|
+
replaceKey: parsed.replaceKey,
|
|
24
|
+
defaultClientRoute: deps.config.defaultCustomerRoute,
|
|
25
|
+
identities,
|
|
26
|
+
workspaces,
|
|
27
|
+
customerKeys,
|
|
28
|
+
billing,
|
|
29
|
+
proxyClient: deps.proxyClient,
|
|
30
|
+
auditLog,
|
|
31
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
32
|
+
});
|
|
33
|
+
const canShowApiKeyToAdmin = !!result.apiKey && ctx.chat?.type === "private";
|
|
34
|
+
if (result.apiKey && canShowApiKeyToAdmin) {
|
|
35
|
+
auditLog.record({
|
|
36
|
+
event: "api_key.revealed",
|
|
37
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
38
|
+
subjectType: "customer_api_key",
|
|
39
|
+
subjectId: result.keyId,
|
|
40
|
+
metadata: {
|
|
41
|
+
telegramUserId: parsed.telegramUserId,
|
|
42
|
+
workspaceId: result.workspaceId,
|
|
43
|
+
keyPreview: result.keyPreview,
|
|
44
|
+
audience: "admin_private_chat",
|
|
45
|
+
apiKey: result.apiKey,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
await ctx.reply([
|
|
50
|
+
"Customer access renewed",
|
|
51
|
+
formatSection("Customer", [
|
|
52
|
+
formatField("Telegram user ID", parsed.telegramUserId),
|
|
53
|
+
formatField("Plan", parsed.planId),
|
|
54
|
+
formatField("Client route", result.clientRoute),
|
|
55
|
+
formatField("Mode", result.mode),
|
|
56
|
+
]),
|
|
57
|
+
formatSection("Workspace", [
|
|
58
|
+
formatField("Workspace ID", result.workspaceId),
|
|
59
|
+
formatField("Subscription ends at", result.subscriptionEndsAt),
|
|
60
|
+
]),
|
|
61
|
+
formatSection("Key", [
|
|
62
|
+
formatField("Preview", result.apiKey ? maskApiKey(result.apiKey) : result.keyPreview),
|
|
63
|
+
canShowApiKeyToAdmin ? `api_key: ${result.apiKey}` : undefined,
|
|
64
|
+
result.apiKey && !canShowApiKeyToAdmin
|
|
65
|
+
? "Full key: replacement key is shown only in private chat"
|
|
66
|
+
: undefined,
|
|
67
|
+
]),
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n\n"));
|
|
71
|
+
const customerNotified = await notifyCustomer(ctx, parsed.telegramUserId, parsed.planId, deps.config.publicResponsesBaseUrl, deps.config.defaultModel, result);
|
|
72
|
+
if (result.apiKey && customerNotified) {
|
|
73
|
+
auditLog.record({
|
|
74
|
+
event: "api_key.revealed",
|
|
75
|
+
actor: { type: "bot", id: "renewuser" },
|
|
76
|
+
subjectType: "customer_api_key",
|
|
77
|
+
subjectId: result.keyId,
|
|
78
|
+
metadata: {
|
|
79
|
+
telegramUserId: parsed.telegramUserId,
|
|
80
|
+
workspaceId: result.workspaceId,
|
|
81
|
+
keyPreview: result.keyPreview,
|
|
82
|
+
audience: "customer_private_chat",
|
|
83
|
+
apiKey: result.apiKey,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
await replyWithProxyError(ctx, error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function parseRenewUserArgs(raw) {
|
|
94
|
+
const args = raw.trim().split(/\s+/g).filter(Boolean);
|
|
95
|
+
const [telegramUserId, planId, daysRaw, replaceKeyRaw] = args;
|
|
96
|
+
const days = Number(daysRaw);
|
|
97
|
+
if (!/^\d+$/.test(telegramUserId ?? "") || !planId || !Number.isInteger(days) || days <= 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (replaceKeyRaw && replaceKeyRaw !== "replace-key") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
telegramUserId,
|
|
105
|
+
planId,
|
|
106
|
+
days,
|
|
107
|
+
replaceKey: replaceKeyRaw === "replace-key",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function notifyCustomer(ctx, telegramUserId, planId, baseUrl, model, result) {
|
|
111
|
+
if (result.apiKey) {
|
|
112
|
+
const sent = await sendCustomerCodexSetup(ctx, {
|
|
113
|
+
telegramUserId,
|
|
114
|
+
baseUrl,
|
|
115
|
+
apiKey: result.apiKey,
|
|
116
|
+
model,
|
|
117
|
+
title: "Your access has been renewed",
|
|
118
|
+
details: [
|
|
119
|
+
formatField("Plan ID", planId),
|
|
120
|
+
formatField("Client route", result.clientRoute),
|
|
121
|
+
formatField("Subscription ends at", result.subscriptionEndsAt),
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
if (sent) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await ctx.api.sendMessage(Number(telegramUserId), [
|
|
130
|
+
"Your access has been renewed",
|
|
131
|
+
"Run /apikey in this private chat to receive your Codex config files.",
|
|
132
|
+
].join("\n\n"));
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
await ctx.reply("Customer notification could not be delivered yet. They may need to /start the bot first.");
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|