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,178 @@
|
|
|
1
|
+
import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const LATEST_DIR_NAME = "latest";
|
|
4
|
+
const DATE_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
const CACHE_HIT_STREAK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
6
|
+
const MAX_CACHE_HIT_STREAK_SESSIONS = 2000;
|
|
7
|
+
let lastCleanupStartedAt = 0;
|
|
8
|
+
let cleanupPromise;
|
|
9
|
+
const cacheHitStreakBySession = new Map();
|
|
10
|
+
export function deriveSessionKey(body, traceContext) {
|
|
11
|
+
const promptCacheKey = readString(traceContext.promptCacheKey) ?? readString(body.prompt_cache_key);
|
|
12
|
+
if (promptCacheKey) {
|
|
13
|
+
return promptCacheKey;
|
|
14
|
+
}
|
|
15
|
+
const metadataUserId = readString(traceContext.metadataUserId);
|
|
16
|
+
if (metadataUserId) {
|
|
17
|
+
return metadataUserId;
|
|
18
|
+
}
|
|
19
|
+
const previousResponseId = readString(body.previous_response_id);
|
|
20
|
+
if (previousResponseId) {
|
|
21
|
+
return previousResponseId;
|
|
22
|
+
}
|
|
23
|
+
const user = readString(body.user);
|
|
24
|
+
if (user) {
|
|
25
|
+
return user;
|
|
26
|
+
}
|
|
27
|
+
return "unknown-session";
|
|
28
|
+
}
|
|
29
|
+
export function createSessionLogContext(logDir, sessionKey, retentionDays = 14, options) {
|
|
30
|
+
const fileName = `${sanitizeFileName(sessionKey)}.jsonl`;
|
|
31
|
+
const sessionFileFor = (date = new Date()) => path.join(logDir, formatLocalDate(date), fileName);
|
|
32
|
+
const latestSessionFile = () => path.join(logDir, LATEST_DIR_NAME, fileName);
|
|
33
|
+
return {
|
|
34
|
+
sessionKey,
|
|
35
|
+
sessionFileFor,
|
|
36
|
+
latestSessionFile,
|
|
37
|
+
async write(entry) {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const sessionFile = sessionFileFor(now);
|
|
40
|
+
const latestFile = latestSessionFile();
|
|
41
|
+
await Promise.all([
|
|
42
|
+
mkdir(path.dirname(sessionFile), { recursive: true }),
|
|
43
|
+
mkdir(path.dirname(latestFile), { recursive: true }),
|
|
44
|
+
]);
|
|
45
|
+
const enrichedEntry = enrichCacheMetrics(sessionKey, entry, options?.cacheMetricsStore);
|
|
46
|
+
const payload = JSON.stringify({
|
|
47
|
+
ts: now.toISOString(),
|
|
48
|
+
sessionKey,
|
|
49
|
+
...enrichedEntry,
|
|
50
|
+
});
|
|
51
|
+
await Promise.all([
|
|
52
|
+
appendFile(sessionFile, `${payload}\n`, "utf8"),
|
|
53
|
+
appendFile(latestFile, `${payload}\n`, "utf8"),
|
|
54
|
+
]);
|
|
55
|
+
scheduleRetentionCleanup(logDir, retentionDays);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function enrichCacheMetrics(sessionKey, entry, cacheMetricsStore) {
|
|
60
|
+
const cachedTokens = typeof entry.cachedTokens === "number" ? entry.cachedTokens : undefined;
|
|
61
|
+
if (cachedTokens === undefined) {
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
const persistedState = cacheMetricsStore?.recordCacheResult(sessionKey, cachedTokens);
|
|
65
|
+
if (persistedState) {
|
|
66
|
+
return {
|
|
67
|
+
...entry,
|
|
68
|
+
cacheHit: persistedState.cacheHit,
|
|
69
|
+
consecutiveCacheHits: persistedState.consecutiveCacheHits,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
pruneCacheHitStreaks();
|
|
73
|
+
const cacheHit = cachedTokens > 0;
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const consecutiveCacheHits = cacheHit
|
|
76
|
+
? (cacheHitStreakBySession.get(sessionKey)?.count ?? 0) + 1
|
|
77
|
+
: 0;
|
|
78
|
+
cacheHitStreakBySession.set(sessionKey, {
|
|
79
|
+
count: consecutiveCacheHits,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
...entry,
|
|
84
|
+
cacheHit,
|
|
85
|
+
consecutiveCacheHits,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function scheduleRetentionCleanup(logDir, retentionDays) {
|
|
89
|
+
if (retentionDays <= 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (cleanupPromise) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (now - lastCleanupStartedAt < 60_000) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
lastCleanupStartedAt = now;
|
|
100
|
+
cleanupPromise = cleanupOldLogDirs(logDir, retentionDays).finally(() => {
|
|
101
|
+
cleanupPromise = undefined;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function pruneCacheHitStreaks() {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
for (const [sessionKey, state] of cacheHitStreakBySession) {
|
|
107
|
+
if (now - state.updatedAt > CACHE_HIT_STREAK_TTL_MS) {
|
|
108
|
+
cacheHitStreakBySession.delete(sessionKey);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (cacheHitStreakBySession.size <= MAX_CACHE_HIT_STREAK_SESSIONS) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const overflow = cacheHitStreakBySession.size - MAX_CACHE_HIT_STREAK_SESSIONS;
|
|
115
|
+
let removed = 0;
|
|
116
|
+
for (const sessionKey of cacheHitStreakBySession.keys()) {
|
|
117
|
+
cacheHitStreakBySession.delete(sessionKey);
|
|
118
|
+
removed += 1;
|
|
119
|
+
if (removed >= overflow) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function cleanupOldLogDirs(logDir, retentionDays) {
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = await readdir(logDir);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (isMissingPathError(error)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
const cutoff = startOfLocalDay(new Date());
|
|
136
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
137
|
+
await Promise.all(entries
|
|
138
|
+
.filter((entry) => entry !== LATEST_DIR_NAME && DATE_DIR_PATTERN.test(entry))
|
|
139
|
+
.filter((entry) => {
|
|
140
|
+
const parsed = parseLocalDate(entry);
|
|
141
|
+
return parsed !== undefined && parsed.getTime() < cutoff.getTime();
|
|
142
|
+
})
|
|
143
|
+
.map((entry) => rm(path.join(logDir, entry), { recursive: true, force: true })));
|
|
144
|
+
}
|
|
145
|
+
function startOfLocalDay(value) {
|
|
146
|
+
return new Date(value.getFullYear(), value.getMonth(), value.getDate());
|
|
147
|
+
}
|
|
148
|
+
function parseLocalDate(value) {
|
|
149
|
+
const match = DATE_DIR_PATTERN.exec(value);
|
|
150
|
+
if (!match) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const [year, month, day] = value.split("-").map((part) => Number(part));
|
|
154
|
+
const parsed = new Date(year, month - 1, day);
|
|
155
|
+
if (parsed.getFullYear() !== year ||
|
|
156
|
+
parsed.getMonth() !== month - 1 ||
|
|
157
|
+
parsed.getDate() !== day) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
function isMissingPathError(error) {
|
|
163
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
164
|
+
}
|
|
165
|
+
function sanitizeFileName(value) {
|
|
166
|
+
const compact = value.trim().replace(/[^\w.-]+/g, "_");
|
|
167
|
+
const sanitized = compact.replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
168
|
+
return sanitized || "unknown-session";
|
|
169
|
+
}
|
|
170
|
+
function formatLocalDate(value) {
|
|
171
|
+
const year = value.getFullYear();
|
|
172
|
+
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
173
|
+
const day = String(value.getDate()).padStart(2, "0");
|
|
174
|
+
return `${year}-${month}-${day}`;
|
|
175
|
+
}
|
|
176
|
+
function readString(value) {
|
|
177
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
178
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
const scriptPath = path.resolve(process.cwd(), "scripts", "tailnet-funnel.sh");
|
|
6
|
+
const bashLookup = spawnSync("sh", ["-c", "command -v bash"], {
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
});
|
|
9
|
+
const bashPath = bashLookup.status === 0 ? bashLookup.stdout.trim() : "";
|
|
10
|
+
test("tailnet funnel script rejects invalid public https port", { skip: !bashPath }, () => {
|
|
11
|
+
const result = spawnSync(bashPath, [scriptPath], {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
env: {
|
|
14
|
+
...process.env,
|
|
15
|
+
TAILSCALE_LOCAL_TARGET: "http://127.0.0.1:8318",
|
|
16
|
+
TAILSCALE_FUNNEL_HTTPS_PORT: "8444",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
assert.notEqual(result.status, 0);
|
|
20
|
+
assert.match(`${result.stdout}${result.stderr}`, /TAILSCALE_FUNNEL_HTTPS_PORT/);
|
|
21
|
+
});
|
|
22
|
+
test("tailnet funnel script rejects non-localhost target", { skip: !bashPath }, () => {
|
|
23
|
+
const result = spawnSync(bashPath, [scriptPath], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
TAILSCALE_LOCAL_TARGET: "http://192.168.0.10:8318",
|
|
28
|
+
TAILSCALE_FUNNEL_HTTPS_PORT: "443",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
assert.notEqual(result.status, 0);
|
|
32
|
+
assert.match(`${result.stdout}${result.stderr}`, /TAILSCALE_LOCAL_TARGET must point at localhost/);
|
|
33
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { formatClientConfigs, formatHealthStatus, formatModels, formatOauthStatus, formatProviderDetails, formatProviders, formatProxyError, formatTestResult, } from "./format.js";
|
|
2
|
+
import { replyOrEditMessage } from "./callbacks.js";
|
|
3
|
+
import { ProxyClientError } from "./proxy-client.js";
|
|
4
|
+
export function getProxyErrorMessage(error) {
|
|
5
|
+
if (error instanceof ProxyClientError) {
|
|
6
|
+
return formatProxyError(error.body?.error ?? error);
|
|
7
|
+
}
|
|
8
|
+
return error instanceof Error ? error.message : "Unknown bot error";
|
|
9
|
+
}
|
|
10
|
+
export async function replyWithProxyError(ctx, error) {
|
|
11
|
+
await replyOrEditMessage(ctx, getProxyErrorMessage(error));
|
|
12
|
+
}
|
|
13
|
+
export async function loadStatusText(deps) {
|
|
14
|
+
const [health, providers, promptCache, usageStats] = await Promise.all([
|
|
15
|
+
deps.proxyClient.getHealth(),
|
|
16
|
+
deps.proxyClient.getProviders(),
|
|
17
|
+
deps.proxyClient.getLatestPromptCache(),
|
|
18
|
+
deps.proxyClient.getUsageStats(),
|
|
19
|
+
]);
|
|
20
|
+
const usageSummary = summarizeUsageStats(usageStats?.stats);
|
|
21
|
+
return formatHealthStatus({
|
|
22
|
+
...health,
|
|
23
|
+
activeProviderId: providers?.activeProviderId,
|
|
24
|
+
latestPromptCache: promptCache?.latest ?? null,
|
|
25
|
+
usageSummary,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function loadProvidersText(deps) {
|
|
29
|
+
return formatProviders(await deps.proxyClient.getProviders());
|
|
30
|
+
}
|
|
31
|
+
export async function loadClientsText(deps) {
|
|
32
|
+
return formatClientConfigs(await deps.proxyClient.getClientConfigs());
|
|
33
|
+
}
|
|
34
|
+
export async function loadModelsText(deps) {
|
|
35
|
+
return formatModels(await deps.proxyClient.getModels());
|
|
36
|
+
}
|
|
37
|
+
export async function loadOauthStatusText(deps) {
|
|
38
|
+
return formatOauthStatus(await deps.proxyClient.getOauthStatus());
|
|
39
|
+
}
|
|
40
|
+
export async function sendStatus(ctx, deps) {
|
|
41
|
+
try {
|
|
42
|
+
await ctx.reply(await loadStatusText(deps));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await replyWithProxyError(ctx, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function sendProviders(ctx, deps) {
|
|
49
|
+
try {
|
|
50
|
+
await ctx.reply(await loadProvidersText(deps));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await replyWithProxyError(ctx, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function sendClients(ctx, deps) {
|
|
57
|
+
try {
|
|
58
|
+
await ctx.reply(await loadClientsText(deps));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await replyWithProxyError(ctx, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function sendOauthStatus(ctx, deps) {
|
|
65
|
+
try {
|
|
66
|
+
await ctx.reply(await loadOauthStatusText(deps));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
await replyWithProxyError(ctx, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function sendTestResult(ctx, deps, input) {
|
|
73
|
+
try {
|
|
74
|
+
const result = await deps.proxyClient.sendTestPrompt({
|
|
75
|
+
prompt: input.prompt,
|
|
76
|
+
model: input.model?.trim() || deps.config.defaultModel,
|
|
77
|
+
providerId: input.providerId?.trim() || undefined,
|
|
78
|
+
});
|
|
79
|
+
await ctx.reply(formatTestResult(result));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await replyWithProxyError(ctx, error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function sendModels(ctx, deps) {
|
|
86
|
+
try {
|
|
87
|
+
await ctx.reply(formatModels(await deps.proxyClient.getModels()));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
await replyWithProxyError(ctx, error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function sendProviderDetails(ctx, deps, providerId) {
|
|
94
|
+
try {
|
|
95
|
+
const payload = await deps.proxyClient.getProviderDetails(providerId);
|
|
96
|
+
await ctx.reply(formatProviderDetails(payload.provider));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await replyWithProxyError(ctx, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function summarizeUsageStats(stats) {
|
|
103
|
+
if (!stats || typeof stats !== "object") {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const summary = stats;
|
|
107
|
+
const lines = [];
|
|
108
|
+
if (typeof summary.totalRequests === "number") {
|
|
109
|
+
lines.push(`total requests: ${summary.totalRequests}`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof summary.totalPromptTokens === "number") {
|
|
112
|
+
lines.push(`prompt tokens: ${summary.totalPromptTokens}`);
|
|
113
|
+
}
|
|
114
|
+
if (typeof summary.totalCompletionTokens === "number") {
|
|
115
|
+
lines.push(`completion tokens: ${summary.totalCompletionTokens}`);
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { replyOrEditMessage } from "./callbacks.js";
|
|
3
|
+
export const ADMIN_CALLBACK_ACTIONS = [
|
|
4
|
+
"status",
|
|
5
|
+
"clients",
|
|
6
|
+
"providers",
|
|
7
|
+
"models",
|
|
8
|
+
"oauth",
|
|
9
|
+
"plans",
|
|
10
|
+
"renewals",
|
|
11
|
+
"apikeys",
|
|
12
|
+
"apply",
|
|
13
|
+
"menu",
|
|
14
|
+
];
|
|
15
|
+
export const ADMIN_CALLBACK_PATTERN = new RegExp(`^v1:admin:(${ADMIN_CALLBACK_ACTIONS.join("|")})$`);
|
|
16
|
+
export const ADMIN_ACTION_BUTTONS = {
|
|
17
|
+
status: { label: "📈 Status", callbackData: "v1:admin:status" },
|
|
18
|
+
clients: { label: "👥 Clients", callbackData: "v1:admin:clients" },
|
|
19
|
+
providers: { label: "🧭 Providers", callbackData: "v1:admin:providers" },
|
|
20
|
+
models: { label: "🧠 Models", callbackData: "v1:admin:models" },
|
|
21
|
+
plans: { label: "💳 Plans", callbackData: "v1:admin:plans" },
|
|
22
|
+
renewals: { label: "🧾 Renewals", callbackData: "v1:admin:renewals" },
|
|
23
|
+
apikeys: { label: "🔑 API Keys", callbackData: "v1:admin:apikeys" },
|
|
24
|
+
apply: { label: "⚙️ Apply", callbackData: "v1:admin:apply" },
|
|
25
|
+
oauth: { label: "🔐 OAuth", callbackData: "v1:admin:oauth" },
|
|
26
|
+
accounts: { label: "👤 Accounts", callbackData: "v1:acct:list" },
|
|
27
|
+
menu: { label: "⬅️ Admin", callbackData: "v1:admin:menu" },
|
|
28
|
+
};
|
|
29
|
+
export const ADMIN_ACTION_LOOPS = {
|
|
30
|
+
main: {
|
|
31
|
+
title: "Admin actions",
|
|
32
|
+
actions: ["status", "clients", "providers", "models", "plans", "renewals", "apikeys", "apply", "oauth", "accounts"],
|
|
33
|
+
},
|
|
34
|
+
proxy: {
|
|
35
|
+
title: "Proxy actions",
|
|
36
|
+
actions: ["status", "providers", "models", "oauth", "accounts", "menu"],
|
|
37
|
+
},
|
|
38
|
+
config: {
|
|
39
|
+
title: "Config actions",
|
|
40
|
+
actions: ["clients", "apply", "providers", "models", "menu"],
|
|
41
|
+
},
|
|
42
|
+
billing: {
|
|
43
|
+
title: "Billing actions",
|
|
44
|
+
actions: ["plans", "renewals", "apikeys", "menu"],
|
|
45
|
+
},
|
|
46
|
+
keys: {
|
|
47
|
+
title: "API key actions",
|
|
48
|
+
actions: ["apikeys", "plans", "renewals", "clients", "menu"],
|
|
49
|
+
},
|
|
50
|
+
apply: {
|
|
51
|
+
title: "Apply actions",
|
|
52
|
+
actions: ["apply", "clients", "status", "menu"],
|
|
53
|
+
},
|
|
54
|
+
accounts: {
|
|
55
|
+
title: "Account actions",
|
|
56
|
+
actions: ["accounts", "oauth", "status", "menu"],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export function buildAdminActionKeyboard(actions) {
|
|
60
|
+
const keyboard = new InlineKeyboard();
|
|
61
|
+
actions.forEach((action, index) => {
|
|
62
|
+
const button = ADMIN_ACTION_BUTTONS[action];
|
|
63
|
+
keyboard.text(button.label, button.callbackData);
|
|
64
|
+
if (index % 2 === 1 && index < actions.length - 1) {
|
|
65
|
+
keyboard.row();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return keyboard;
|
|
69
|
+
}
|
|
70
|
+
export function buildAdminStartKeyboard() {
|
|
71
|
+
return buildAdminActionKeyboard(ADMIN_ACTION_LOOPS.main.actions);
|
|
72
|
+
}
|
|
73
|
+
export function mergeInlineKeyboards(...keyboards) {
|
|
74
|
+
const rows = keyboards.flatMap((keyboard) => {
|
|
75
|
+
if (!keyboard) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const raw = JSON.parse(JSON.stringify(keyboard));
|
|
79
|
+
return Array.isArray(raw.inline_keyboard) ? raw.inline_keyboard : [];
|
|
80
|
+
});
|
|
81
|
+
return rows.length > 0 ? { inline_keyboard: rows } : undefined;
|
|
82
|
+
}
|
|
83
|
+
export async function replyAdminActionLoop(ctx, loop = "main") {
|
|
84
|
+
const definition = ADMIN_ACTION_LOOPS[loop];
|
|
85
|
+
await replyOrEditMessage(ctx, definition.title, {
|
|
86
|
+
reply_markup: buildAdminActionKeyboard(definition.actions),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function renderAdminScreen(ctx, input) {
|
|
90
|
+
const loopKeyboard = buildAdminActionKeyboard(ADMIN_ACTION_LOOPS[input.loop].actions);
|
|
91
|
+
await replyOrEditMessage(ctx, input.text, {
|
|
92
|
+
reply_markup: mergeInlineKeyboards(input.primaryKeyboard, loopKeyboard),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function buildApplyClientKeyboard(includeMenu = true) {
|
|
96
|
+
const keyboard = new InlineKeyboard()
|
|
97
|
+
.text("Hermes", "v1:apply:client:hermes")
|
|
98
|
+
.text("Codex", "v1:apply:client:codex");
|
|
99
|
+
if (includeMenu) {
|
|
100
|
+
keyboard.row().text(ADMIN_ACTION_BUTTONS.menu.label, ADMIN_ACTION_BUTTONS.menu.callbackData);
|
|
101
|
+
}
|
|
102
|
+
return keyboard;
|
|
103
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function isAllowed(ctx, config) {
|
|
2
|
+
const fromId = ctx.from?.id?.toString();
|
|
3
|
+
const chatId = ctx.chat?.id?.toString();
|
|
4
|
+
const hasEmergencyUserLockdown = config.allowedUserIds.size > 0;
|
|
5
|
+
const isOwnerOrAdmin = !!fromId && (config.ownerUserIds.has(fromId) || config.adminUserIds.has(fromId));
|
|
6
|
+
const userAllowed = fromId
|
|
7
|
+
? isOwnerOrAdmin ||
|
|
8
|
+
config.allowedUserIds.has(fromId) ||
|
|
9
|
+
(!hasEmergencyUserLockdown && config.publicSignupEnabled)
|
|
10
|
+
: false;
|
|
11
|
+
const chatAllowed = config.allowedChatIds.size === 0 || (chatId ? config.allowedChatIds.has(chatId) : false);
|
|
12
|
+
return userAllowed && chatAllowed;
|
|
13
|
+
}
|
|
14
|
+
export function isAdmin(ctx, config) {
|
|
15
|
+
const fromId = ctx.from?.id?.toString();
|
|
16
|
+
return !!fromId && (config.ownerUserIds.has(fromId) || config.adminUserIds.has(fromId));
|
|
17
|
+
}
|
|
18
|
+
export function createAllowlistMiddleware(config) {
|
|
19
|
+
return async (ctx, next) => {
|
|
20
|
+
if (!isAllowed(ctx, config)) {
|
|
21
|
+
await ctx.reply("🔒 Access restricted\nThis bot is restricted. Your user or chat is not authorized.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await next();
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function createCustomerCommandMiddleware(config) {
|
|
28
|
+
const customerCommands = new Set(["start", "help", "me", "apikey", "usage", "quota", "renew", "tailscale"]);
|
|
29
|
+
return async (ctx, next) => {
|
|
30
|
+
if (isAdmin(ctx, config)) {
|
|
31
|
+
await next();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const text = ctx.message && "text" in ctx.message ? ctx.message.text : undefined;
|
|
35
|
+
const command = parseCommand(text);
|
|
36
|
+
if (!command || customerCommands.has(command)) {
|
|
37
|
+
await next();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await ctx.reply("🔒 Admin-only command\nThis command is admin-only. Use /apikey to view your Responses API key.");
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parseCommand(text) {
|
|
44
|
+
const match = text?.trim().match(/^\/([A-Za-z0-9_]+)(?:@[A-Za-z0-9_]+)?(?:\s|$)/);
|
|
45
|
+
return match?.[1]?.toLowerCase();
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
4
|
+
export class BotIdentityRepository {
|
|
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
|
+
ensureIdentitySchema(db);
|
|
13
|
+
return new BotIdentityRepository(db);
|
|
14
|
+
}
|
|
15
|
+
upsertUser(input) {
|
|
16
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
17
|
+
const existing = this.getUser(input.telegramUserId);
|
|
18
|
+
const role = existing?.role ?? input.defaultRole ?? "customer";
|
|
19
|
+
const status = existing?.status ?? input.defaultStatus ?? "active";
|
|
20
|
+
this.db
|
|
21
|
+
.prepare(`INSERT INTO telegram_users (
|
|
22
|
+
telegram_user_id,
|
|
23
|
+
username,
|
|
24
|
+
first_name,
|
|
25
|
+
last_name,
|
|
26
|
+
language_code,
|
|
27
|
+
role,
|
|
28
|
+
status,
|
|
29
|
+
created_at,
|
|
30
|
+
updated_at,
|
|
31
|
+
last_seen_at
|
|
32
|
+
)
|
|
33
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
34
|
+
ON CONFLICT(telegram_user_id) DO UPDATE SET
|
|
35
|
+
username = excluded.username,
|
|
36
|
+
first_name = excluded.first_name,
|
|
37
|
+
last_name = excluded.last_name,
|
|
38
|
+
language_code = excluded.language_code,
|
|
39
|
+
updated_at = excluded.updated_at,
|
|
40
|
+
last_seen_at = excluded.last_seen_at`)
|
|
41
|
+
.run(input.telegramUserId, input.username ?? null, input.firstName ?? null, input.lastName ?? null, input.languageCode ?? null, role, status, existing?.createdAt ?? now, now, now);
|
|
42
|
+
return this.getUser(input.telegramUserId);
|
|
43
|
+
}
|
|
44
|
+
getUser(telegramUserId) {
|
|
45
|
+
const row = this.db
|
|
46
|
+
.prepare(`SELECT
|
|
47
|
+
telegram_user_id,
|
|
48
|
+
username,
|
|
49
|
+
first_name,
|
|
50
|
+
last_name,
|
|
51
|
+
language_code,
|
|
52
|
+
role,
|
|
53
|
+
status,
|
|
54
|
+
created_at,
|
|
55
|
+
updated_at,
|
|
56
|
+
last_seen_at
|
|
57
|
+
FROM telegram_users
|
|
58
|
+
WHERE telegram_user_id = ?`)
|
|
59
|
+
.get(telegramUserId);
|
|
60
|
+
return row ? mapTelegramUserRow(row) : undefined;
|
|
61
|
+
}
|
|
62
|
+
setUserStatus(telegramUserId, status, now = new Date()) {
|
|
63
|
+
this.db
|
|
64
|
+
.prepare(`UPDATE telegram_users
|
|
65
|
+
SET status = ?, updated_at = ?
|
|
66
|
+
WHERE telegram_user_id = ?`)
|
|
67
|
+
.run(status, now.toISOString(), telegramUserId);
|
|
68
|
+
}
|
|
69
|
+
upsertChat(input) {
|
|
70
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
71
|
+
const existing = this.getChat(input.telegramChatId);
|
|
72
|
+
this.db
|
|
73
|
+
.prepare(`INSERT INTO telegram_chats (
|
|
74
|
+
telegram_chat_id,
|
|
75
|
+
chat_type,
|
|
76
|
+
title,
|
|
77
|
+
created_at,
|
|
78
|
+
updated_at,
|
|
79
|
+
last_seen_at
|
|
80
|
+
)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
82
|
+
ON CONFLICT(telegram_chat_id) DO UPDATE SET
|
|
83
|
+
chat_type = excluded.chat_type,
|
|
84
|
+
title = excluded.title,
|
|
85
|
+
updated_at = excluded.updated_at,
|
|
86
|
+
last_seen_at = excluded.last_seen_at`)
|
|
87
|
+
.run(input.telegramChatId, input.chatType, input.title ?? null, existing?.createdAt ?? now, now, now);
|
|
88
|
+
return this.getChat(input.telegramChatId);
|
|
89
|
+
}
|
|
90
|
+
getChat(telegramChatId) {
|
|
91
|
+
const row = this.db
|
|
92
|
+
.prepare(`SELECT
|
|
93
|
+
telegram_chat_id,
|
|
94
|
+
chat_type,
|
|
95
|
+
title,
|
|
96
|
+
created_at,
|
|
97
|
+
updated_at,
|
|
98
|
+
last_seen_at
|
|
99
|
+
FROM telegram_chats
|
|
100
|
+
WHERE telegram_chat_id = ?`)
|
|
101
|
+
.get(telegramChatId);
|
|
102
|
+
return row ? mapTelegramChatRow(row) : undefined;
|
|
103
|
+
}
|
|
104
|
+
upsertMembership(input) {
|
|
105
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
106
|
+
const role = input.role?.trim() || "member";
|
|
107
|
+
this.db
|
|
108
|
+
.prepare(`INSERT INTO telegram_chat_memberships (
|
|
109
|
+
telegram_user_id,
|
|
110
|
+
telegram_chat_id,
|
|
111
|
+
role,
|
|
112
|
+
created_at,
|
|
113
|
+
updated_at
|
|
114
|
+
)
|
|
115
|
+
VALUES (?, ?, ?, ?, ?)
|
|
116
|
+
ON CONFLICT(telegram_user_id, telegram_chat_id) DO UPDATE SET
|
|
117
|
+
role = excluded.role,
|
|
118
|
+
updated_at = excluded.updated_at`)
|
|
119
|
+
.run(input.telegramUserId, input.telegramChatId, role, now, now);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function ensureIdentitySchema(db) {
|
|
123
|
+
db.exec(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS telegram_users (
|
|
125
|
+
telegram_user_id TEXT PRIMARY KEY,
|
|
126
|
+
username TEXT,
|
|
127
|
+
first_name TEXT,
|
|
128
|
+
last_name TEXT,
|
|
129
|
+
language_code TEXT,
|
|
130
|
+
role TEXT NOT NULL DEFAULT 'customer',
|
|
131
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
132
|
+
created_at TEXT NOT NULL,
|
|
133
|
+
updated_at TEXT NOT NULL,
|
|
134
|
+
last_seen_at TEXT
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
CREATE TABLE IF NOT EXISTS telegram_chats (
|
|
138
|
+
telegram_chat_id TEXT PRIMARY KEY,
|
|
139
|
+
chat_type TEXT NOT NULL,
|
|
140
|
+
title TEXT,
|
|
141
|
+
created_at TEXT NOT NULL,
|
|
142
|
+
updated_at TEXT NOT NULL,
|
|
143
|
+
last_seen_at TEXT
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE TABLE IF NOT EXISTS telegram_chat_memberships (
|
|
147
|
+
telegram_user_id TEXT NOT NULL,
|
|
148
|
+
telegram_chat_id TEXT NOT NULL,
|
|
149
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
150
|
+
created_at TEXT NOT NULL,
|
|
151
|
+
updated_at TEXT NOT NULL,
|
|
152
|
+
PRIMARY KEY (telegram_user_id, telegram_chat_id)
|
|
153
|
+
);
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
function mapTelegramUserRow(row) {
|
|
157
|
+
return {
|
|
158
|
+
telegramUserId: row.telegram_user_id,
|
|
159
|
+
username: row.username ?? undefined,
|
|
160
|
+
firstName: row.first_name ?? undefined,
|
|
161
|
+
lastName: row.last_name ?? undefined,
|
|
162
|
+
languageCode: row.language_code ?? undefined,
|
|
163
|
+
role: normalizeRole(row.role),
|
|
164
|
+
status: normalizeStatus(row.status),
|
|
165
|
+
createdAt: row.created_at,
|
|
166
|
+
updatedAt: row.updated_at,
|
|
167
|
+
lastSeenAt: row.last_seen_at ?? undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function mapTelegramChatRow(row) {
|
|
171
|
+
return {
|
|
172
|
+
telegramChatId: row.telegram_chat_id,
|
|
173
|
+
chatType: row.chat_type,
|
|
174
|
+
title: row.title ?? undefined,
|
|
175
|
+
createdAt: row.created_at,
|
|
176
|
+
updatedAt: row.updated_at,
|
|
177
|
+
lastSeenAt: row.last_seen_at ?? undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function normalizeRole(value) {
|
|
181
|
+
return value === "owner" || value === "admin" || value === "support" || value === "customer"
|
|
182
|
+
? value
|
|
183
|
+
: "customer";
|
|
184
|
+
}
|
|
185
|
+
function normalizeStatus(value) {
|
|
186
|
+
return value === "active" || value === "pending_approval" || value === "blocked"
|
|
187
|
+
? value
|
|
188
|
+
: "active";
|
|
189
|
+
}
|