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