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
package/dist/schema.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const passthroughRecordSchema = z.record(z.string(), z.unknown());
|
|
3
|
+
const inputTextPartSchema = z.object({
|
|
4
|
+
type: z.literal("input_text"),
|
|
5
|
+
text: z.string(),
|
|
6
|
+
});
|
|
7
|
+
const inputImagePartSchema = z.object({
|
|
8
|
+
type: z.literal("input_image"),
|
|
9
|
+
image_url: z.string().min(1),
|
|
10
|
+
});
|
|
11
|
+
const outputTextPartSchema = z.object({
|
|
12
|
+
type: z.literal("output_text"),
|
|
13
|
+
text: z.string(),
|
|
14
|
+
});
|
|
15
|
+
const contentPartSchema = z.union([
|
|
16
|
+
inputTextPartSchema,
|
|
17
|
+
inputImagePartSchema,
|
|
18
|
+
outputTextPartSchema,
|
|
19
|
+
]);
|
|
20
|
+
const imageUrlValueSchema = z.union([
|
|
21
|
+
z.string().min(1),
|
|
22
|
+
z.object({
|
|
23
|
+
url: z.string().min(1),
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
const chatTextPartSchema = z.object({
|
|
27
|
+
type: z.literal("text"),
|
|
28
|
+
text: z.string(),
|
|
29
|
+
});
|
|
30
|
+
const chatImagePartSchema = z.object({
|
|
31
|
+
type: z.literal("image_url"),
|
|
32
|
+
image_url: imageUrlValueSchema,
|
|
33
|
+
});
|
|
34
|
+
const chatContentPartSchema = z.union([chatTextPartSchema, chatImagePartSchema]);
|
|
35
|
+
const assistantToolCallSchema = z.object({
|
|
36
|
+
id: z.string().optional(),
|
|
37
|
+
type: z.literal("function").optional(),
|
|
38
|
+
function: z.object({
|
|
39
|
+
name: z.string(),
|
|
40
|
+
arguments: z.string().optional(),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
const messageSchema = z.object({
|
|
44
|
+
role: z.enum(["user", "assistant", "system", "developer", "tool"]),
|
|
45
|
+
content: z.union([z.string(), z.array(chatContentPartSchema)]).optional(),
|
|
46
|
+
tool_calls: z.array(assistantToolCallSchema).optional(),
|
|
47
|
+
tool_call_id: z.string().optional(),
|
|
48
|
+
name: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
const inputItemSchema = z.object({
|
|
51
|
+
role: z.enum(["user", "assistant", "system", "tool"]),
|
|
52
|
+
content: z.union([z.string(), z.array(contentPartSchema)]),
|
|
53
|
+
});
|
|
54
|
+
const functionToolSchema = z.object({
|
|
55
|
+
type: z.literal("function"),
|
|
56
|
+
name: z.string().optional(),
|
|
57
|
+
description: z.string().optional(),
|
|
58
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
59
|
+
strict: z.boolean().optional(),
|
|
60
|
+
function: z
|
|
61
|
+
.object({
|
|
62
|
+
name: z.string(),
|
|
63
|
+
description: z.string().optional(),
|
|
64
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
65
|
+
strict: z.boolean().optional(),
|
|
66
|
+
})
|
|
67
|
+
.optional(),
|
|
68
|
+
});
|
|
69
|
+
const passthroughToolSchema = passthroughRecordSchema;
|
|
70
|
+
const toolChoiceSchema = z.union([
|
|
71
|
+
z.enum(["none", "auto", "required"]),
|
|
72
|
+
passthroughRecordSchema,
|
|
73
|
+
]);
|
|
74
|
+
const reasoningSchema = z.object({
|
|
75
|
+
effort: z.enum(["minimal", "low", "medium", "high"]).optional(),
|
|
76
|
+
summary: z.enum(["auto", "none", "concise", "detailed"]).optional(),
|
|
77
|
+
});
|
|
78
|
+
const textSchema = z.object({
|
|
79
|
+
verbosity: z.enum(["low", "medium", "high"]).optional(),
|
|
80
|
+
format: passthroughRecordSchema.optional(),
|
|
81
|
+
});
|
|
82
|
+
export const proxyResponsesRequestSchema = z
|
|
83
|
+
.object({
|
|
84
|
+
model: z.string().min(1),
|
|
85
|
+
input: z
|
|
86
|
+
.union([z.string(), z.array(z.union([inputItemSchema, passthroughRecordSchema]))])
|
|
87
|
+
.optional(),
|
|
88
|
+
messages: z.array(messageSchema).optional(),
|
|
89
|
+
instructions: z.string().optional(),
|
|
90
|
+
store: z.boolean().optional(),
|
|
91
|
+
stream: z.boolean().optional(),
|
|
92
|
+
tools: z.array(z.union([functionToolSchema, passthroughToolSchema])).optional(),
|
|
93
|
+
tool_choice: toolChoiceSchema.optional(),
|
|
94
|
+
parallel_tool_calls: z.boolean().optional(),
|
|
95
|
+
reasoning: reasoningSchema.nullable().optional(),
|
|
96
|
+
text: textSchema.nullable().optional(),
|
|
97
|
+
max_output_tokens: z.number().int().positive().optional(),
|
|
98
|
+
max_tool_calls: z.number().int().positive().optional(),
|
|
99
|
+
temperature: z.number().optional(),
|
|
100
|
+
top_p: z.number().optional(),
|
|
101
|
+
metadata: z.record(z.string(), z.string()).optional(),
|
|
102
|
+
user: z.string().optional(),
|
|
103
|
+
previous_response_id: z.string().optional(),
|
|
104
|
+
truncation: z.enum(["auto", "disabled"]).optional(),
|
|
105
|
+
include: z.array(z.string()).optional(),
|
|
106
|
+
prompt_cache_key: z.string().optional(),
|
|
107
|
+
prompt_cache_retention: z.string().optional(),
|
|
108
|
+
})
|
|
109
|
+
.passthrough()
|
|
110
|
+
.superRefine((value, ctx) => {
|
|
111
|
+
if (value.input === undefined && value.messages === undefined) {
|
|
112
|
+
ctx.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
path: ["input"],
|
|
115
|
+
message: "Either input or messages must be provided",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { proxyResponsesRequestSchema } from "./schema.js";
|
|
4
|
+
test("accepts nullable reasoning and text fields from responses clients", () => {
|
|
5
|
+
const parsed = proxyResponsesRequestSchema.safeParse({
|
|
6
|
+
model: "gpt-5.4",
|
|
7
|
+
input: "ping",
|
|
8
|
+
reasoning: null,
|
|
9
|
+
text: null,
|
|
10
|
+
});
|
|
11
|
+
assert.equal(parsed.success, true);
|
|
12
|
+
if (parsed.success) {
|
|
13
|
+
assert.equal(parsed.data.reasoning, null);
|
|
14
|
+
assert.equal(parsed.data.text, null);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const REQUEST_ID_PATTERN = /Chuc\s+ngon\s+mieng\s+ma\s+([A-Fa-f0-9-]{8,})/i;
|
|
2
|
+
const DEFAULT_FALLBACK_AMOUNT_VND = 5_000;
|
|
3
|
+
export function resolveExpectedAmountVnd(request, billing) {
|
|
4
|
+
if (typeof request.priceVnd === "number" && request.priceVnd > 0) {
|
|
5
|
+
return request.priceVnd;
|
|
6
|
+
}
|
|
7
|
+
if (request.kind === "renewal") {
|
|
8
|
+
const planId = request.requestedPlanId ?? "basic";
|
|
9
|
+
const plan = billing.getPlan(planId);
|
|
10
|
+
if (plan && plan.currency === "VND" && plan.priceCents > 0) {
|
|
11
|
+
return plan.priceCents;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return DEFAULT_FALLBACK_AMOUNT_VND;
|
|
15
|
+
}
|
|
16
|
+
export function extractRequestIdFromContent(content) {
|
|
17
|
+
if (!content) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const match = content.match(REQUEST_ID_PATTERN);
|
|
21
|
+
return match ? match[1] : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function processSepayWebhook(args) {
|
|
24
|
+
const payload = args.payload;
|
|
25
|
+
const transferType = typeof payload.transferType === "string" ? payload.transferType.toLowerCase() : undefined;
|
|
26
|
+
if (transferType && transferType !== "in") {
|
|
27
|
+
return { status: "ignored", reason: `transfer_type_${transferType}` };
|
|
28
|
+
}
|
|
29
|
+
const requestId = extractRequestIdFromContent(typeof payload.content === "string" ? payload.content : undefined);
|
|
30
|
+
if (!requestId) {
|
|
31
|
+
return { status: "ignored", reason: "no_request_id_in_content" };
|
|
32
|
+
}
|
|
33
|
+
const request = args.billing.getRenewalRequest(requestId);
|
|
34
|
+
if (!request) {
|
|
35
|
+
return { status: "ignored", reason: "request_not_found" };
|
|
36
|
+
}
|
|
37
|
+
if (request.status !== "open") {
|
|
38
|
+
return { status: "already_processed", request };
|
|
39
|
+
}
|
|
40
|
+
const receivedAmount = Number(payload.transferAmount);
|
|
41
|
+
if (!Number.isFinite(receivedAmount) || receivedAmount <= 0) {
|
|
42
|
+
return { status: "ignored", reason: "invalid_amount" };
|
|
43
|
+
}
|
|
44
|
+
const expectedAmount = resolveExpectedAmountVnd(request, args.billing);
|
|
45
|
+
if (receivedAmount !== expectedAmount) {
|
|
46
|
+
args.auditLog.record({
|
|
47
|
+
event: "payment.amount_mismatch",
|
|
48
|
+
actor: { type: "system", id: "sepay-webhook" },
|
|
49
|
+
subjectType: "renewal_request",
|
|
50
|
+
subjectId: request.id,
|
|
51
|
+
metadata: {
|
|
52
|
+
telegramUserId: request.telegramUserId,
|
|
53
|
+
workspaceId: request.workspaceId,
|
|
54
|
+
receivedAmountVnd: receivedAmount,
|
|
55
|
+
expectedAmountVnd: expectedAmount,
|
|
56
|
+
referenceCode: typeof payload.referenceCode === "string" ? payload.referenceCode : undefined,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return { status: "amount_mismatch", request, receivedAmount, expectedAmount };
|
|
60
|
+
}
|
|
61
|
+
const confirmed = args.billing.confirmRenewalPayment({
|
|
62
|
+
id: request.id,
|
|
63
|
+
resolution: "payment_confirmed_sepay_webhook",
|
|
64
|
+
expectedStatus: "open",
|
|
65
|
+
});
|
|
66
|
+
if (!confirmed) {
|
|
67
|
+
const latest = args.billing.getRenewalRequest(request.id);
|
|
68
|
+
return latest
|
|
69
|
+
? { status: "already_processed", request: latest }
|
|
70
|
+
: { status: "ignored", reason: "request_gone" };
|
|
71
|
+
}
|
|
72
|
+
args.auditLog.record({
|
|
73
|
+
event: "payment.confirmed_sepay",
|
|
74
|
+
actor: { type: "system", id: "sepay-webhook" },
|
|
75
|
+
subjectType: "renewal_request",
|
|
76
|
+
subjectId: request.id,
|
|
77
|
+
metadata: {
|
|
78
|
+
telegramUserId: request.telegramUserId,
|
|
79
|
+
workspaceId: request.workspaceId,
|
|
80
|
+
amountVnd: receivedAmount,
|
|
81
|
+
transferDescription: typeof payload.content === "string" ? payload.content : undefined,
|
|
82
|
+
referenceCode: typeof payload.referenceCode === "string" ? payload.referenceCode : undefined,
|
|
83
|
+
gateway: typeof payload.gateway === "string" ? payload.gateway : undefined,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return { status: "confirmed", request: confirmed, amountVnd: receivedAmount };
|
|
87
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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 { AuditLogRepository } from "./audit-log.js";
|
|
7
|
+
import { BillingRepository } from "./billing.js";
|
|
8
|
+
import { CustomerWorkspaceRepository } from "./telegram-bot/customer-workspace-repository.js";
|
|
9
|
+
import { processSepayWebhook } from "./sepay-webhook.js";
|
|
10
|
+
function setup() {
|
|
11
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "sepay-webhook-"));
|
|
12
|
+
const dbFile = path.join(dir, "bot.sqlite");
|
|
13
|
+
const billing = BillingRepository.create(dbFile);
|
|
14
|
+
const audit = AuditLogRepository.create(dbFile);
|
|
15
|
+
const workspaces = CustomerWorkspaceRepository.create(dbFile);
|
|
16
|
+
return {
|
|
17
|
+
billing,
|
|
18
|
+
audit,
|
|
19
|
+
workspaces,
|
|
20
|
+
cleanup() {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
test("sepay webhook confirms a matching renewal request", () => {
|
|
26
|
+
const { billing, audit, workspaces, cleanup } = setup();
|
|
27
|
+
try {
|
|
28
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
29
|
+
ownerTelegramUserId: "42",
|
|
30
|
+
defaultClientRoute: "customers",
|
|
31
|
+
status: "active",
|
|
32
|
+
});
|
|
33
|
+
const created = billing.createRenewalRequest({
|
|
34
|
+
workspaceId: workspace.id,
|
|
35
|
+
telegramUserId: "42",
|
|
36
|
+
requestedPlanId: "basic",
|
|
37
|
+
requestedDays: 1,
|
|
38
|
+
priceVnd: 5_000,
|
|
39
|
+
});
|
|
40
|
+
const outcome = processSepayWebhook({
|
|
41
|
+
payload: {
|
|
42
|
+
transferAmount: 5_000,
|
|
43
|
+
transferType: "in",
|
|
44
|
+
content: `Chuc ngon mieng ma ${created.request.id}`,
|
|
45
|
+
referenceCode: "FT123",
|
|
46
|
+
},
|
|
47
|
+
billing,
|
|
48
|
+
auditLog: audit,
|
|
49
|
+
});
|
|
50
|
+
assert.equal(outcome.status, "confirmed");
|
|
51
|
+
assert.equal(billing.getRenewalRequest(created.request.id)?.status, "payment_confirmed");
|
|
52
|
+
const events = audit.listEvents({ event: "payment.confirmed_sepay", limit: 5 });
|
|
53
|
+
assert.equal(events.length, 1);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
cleanup();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
test("sepay webhook records mismatch without confirming on wrong amount", () => {
|
|
60
|
+
const { billing, audit, workspaces, cleanup } = setup();
|
|
61
|
+
try {
|
|
62
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
63
|
+
ownerTelegramUserId: "42",
|
|
64
|
+
defaultClientRoute: "customers",
|
|
65
|
+
status: "active",
|
|
66
|
+
});
|
|
67
|
+
const created = billing.createRenewalRequest({
|
|
68
|
+
workspaceId: workspace.id,
|
|
69
|
+
telegramUserId: "42",
|
|
70
|
+
requestedPlanId: "basic",
|
|
71
|
+
requestedDays: 1,
|
|
72
|
+
priceVnd: 5_000,
|
|
73
|
+
});
|
|
74
|
+
const outcome = processSepayWebhook({
|
|
75
|
+
payload: {
|
|
76
|
+
transferAmount: 1_000,
|
|
77
|
+
transferType: "in",
|
|
78
|
+
content: `Chuc ngon mieng ma ${created.request.id}`,
|
|
79
|
+
},
|
|
80
|
+
billing,
|
|
81
|
+
auditLog: audit,
|
|
82
|
+
});
|
|
83
|
+
assert.equal(outcome.status, "amount_mismatch");
|
|
84
|
+
assert.equal(billing.getRenewalRequest(created.request.id)?.status, "open");
|
|
85
|
+
assert.equal(audit.listEvents({ event: "payment.amount_mismatch", limit: 5 }).length, 1);
|
|
86
|
+
assert.equal(audit.listEvents({ event: "payment.confirmed_sepay", limit: 5 }).length, 0);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
cleanup();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
test("sepay webhook ignores payloads without a recognizable request id", () => {
|
|
93
|
+
const { billing, audit, cleanup } = setup();
|
|
94
|
+
try {
|
|
95
|
+
const outcome = processSepayWebhook({
|
|
96
|
+
payload: {
|
|
97
|
+
transferAmount: 5_000,
|
|
98
|
+
transferType: "in",
|
|
99
|
+
content: "unrelated transfer",
|
|
100
|
+
},
|
|
101
|
+
billing,
|
|
102
|
+
auditLog: audit,
|
|
103
|
+
});
|
|
104
|
+
assert.equal(outcome.status, "ignored");
|
|
105
|
+
assert.equal(audit.listEvents({ limit: 5 }).length, 0);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
cleanup();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
test("sepay webhook reports already_processed for non-open requests", () => {
|
|
112
|
+
const { billing, audit, workspaces, cleanup } = setup();
|
|
113
|
+
try {
|
|
114
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
115
|
+
ownerTelegramUserId: "42",
|
|
116
|
+
defaultClientRoute: "customers",
|
|
117
|
+
status: "active",
|
|
118
|
+
});
|
|
119
|
+
const created = billing.createRenewalRequest({
|
|
120
|
+
workspaceId: workspace.id,
|
|
121
|
+
telegramUserId: "42",
|
|
122
|
+
requestedPlanId: "basic",
|
|
123
|
+
requestedDays: 1,
|
|
124
|
+
priceVnd: 5_000,
|
|
125
|
+
});
|
|
126
|
+
billing.confirmRenewalPayment({ id: created.request.id, expectedStatus: "open" });
|
|
127
|
+
const outcome = processSepayWebhook({
|
|
128
|
+
payload: {
|
|
129
|
+
transferAmount: 5_000,
|
|
130
|
+
transferType: "in",
|
|
131
|
+
content: `Chuc ngon mieng ma ${created.request.id}`,
|
|
132
|
+
},
|
|
133
|
+
billing,
|
|
134
|
+
auditLog: audit,
|
|
135
|
+
});
|
|
136
|
+
assert.equal(outcome.status, "already_processed");
|
|
137
|
+
assert.equal(audit.listEvents({ event: "payment.confirmed_sepay", limit: 5 }).length, 0);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
cleanup();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-server-body-limit-"));
|
|
7
|
+
const dbFile = path.join(tempDir, "app.sqlite");
|
|
8
|
+
process.env.RESPONSES_PROXY_DISABLE_LISTEN = "true";
|
|
9
|
+
process.env.APP_DB_PATH = dbFile;
|
|
10
|
+
process.env.CUSTOMER_KEY_DB_PATH = dbFile;
|
|
11
|
+
process.env.UPSTREAM_BASE_URL = "https://upstream.example/v1";
|
|
12
|
+
process.env.UPSTREAM_API_KEY = "provider-key";
|
|
13
|
+
process.env.PROVIDER_USAGE_CHECK_ENABLED = "false";
|
|
14
|
+
process.env.CHATGPT_OAUTH_ENABLED = "false";
|
|
15
|
+
process.env.LOG_LEVEL = "silent";
|
|
16
|
+
process.env.REQUEST_BODY_LIMIT_BYTES = "1024";
|
|
17
|
+
const { app } = await import("./server.js");
|
|
18
|
+
test.after(async () => {
|
|
19
|
+
await app.close();
|
|
20
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
test("server rejects payloads above configured body limit", async () => {
|
|
23
|
+
const response = await app.inject({
|
|
24
|
+
method: "POST",
|
|
25
|
+
url: "/v1/responses",
|
|
26
|
+
headers: {
|
|
27
|
+
authorization: "Bearer provider-key",
|
|
28
|
+
},
|
|
29
|
+
payload: {
|
|
30
|
+
model: "gpt-4.1",
|
|
31
|
+
input: "x".repeat(2048),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
assert.equal(response.statusCode, 413);
|
|
35
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
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 BetterSqlite3 from "better-sqlite3";
|
|
7
|
+
import { DashboardAuthRepository } from "./dashboard-auth.js";
|
|
8
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-server-token-limits-"));
|
|
9
|
+
const dbFile = path.join(tempDir, "app.sqlite");
|
|
10
|
+
process.env.RESPONSES_PROXY_DISABLE_LISTEN = "true";
|
|
11
|
+
process.env.APP_DB_PATH = dbFile;
|
|
12
|
+
process.env.CUSTOMER_KEY_DB_PATH = dbFile;
|
|
13
|
+
process.env.UPSTREAM_BASE_URL = "https://upstream.example/v1";
|
|
14
|
+
process.env.UPSTREAM_API_KEY = "provider-key";
|
|
15
|
+
process.env.PROVIDER_USAGE_CHECK_ENABLED = "false";
|
|
16
|
+
process.env.CHATGPT_OAUTH_ENABLED = "false";
|
|
17
|
+
process.env.LOG_LEVEL = "silent";
|
|
18
|
+
process.env.RESPONSES_PROXY_CLIENT_API_KEY = "test-bot-api-key";
|
|
19
|
+
process.env.TELEGRAM_BOT_TOKEN = "test-dashboard-bot-token";
|
|
20
|
+
process.env.TELEGRAM_OWNER_USER_IDS = "1";
|
|
21
|
+
process.env.TELEGRAM_ADMIN_USER_IDS = "1";
|
|
22
|
+
const { app } = await import("./server.js");
|
|
23
|
+
test.after(async () => {
|
|
24
|
+
await app.close();
|
|
25
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
async function loginDashboard() {
|
|
28
|
+
const approvalResponse = await app.inject({
|
|
29
|
+
method: "POST",
|
|
30
|
+
url: "/api/dashboard-auth/request-approval",
|
|
31
|
+
});
|
|
32
|
+
assert.equal(approvalResponse.statusCode, 200);
|
|
33
|
+
const approval = approvalResponse.json();
|
|
34
|
+
assert.equal(typeof approval.debugApprovalCode, "string");
|
|
35
|
+
const dashboardAuth = DashboardAuthRepository.create(dbFile);
|
|
36
|
+
const resolved = dashboardAuth.resolveApprovalChoice({
|
|
37
|
+
challengeId: approval.challengeId,
|
|
38
|
+
telegramUserId: "1",
|
|
39
|
+
selectedCode: approval.debugApprovalCode,
|
|
40
|
+
});
|
|
41
|
+
assert.equal(resolved.ok, true);
|
|
42
|
+
const statusResponse = await app.inject({
|
|
43
|
+
method: "GET",
|
|
44
|
+
url: `/api/dashboard-auth/approval-status?challengeId=${encodeURIComponent(approval.challengeId)}&pollToken=${encodeURIComponent(approval.pollToken)}`,
|
|
45
|
+
});
|
|
46
|
+
assert.equal(statusResponse.statusCode, 200);
|
|
47
|
+
assert.equal(statusResponse.json().status, "approved");
|
|
48
|
+
return statusResponse.headers["set-cookie"];
|
|
49
|
+
}
|
|
50
|
+
test("client token limit admin API creates reads and resets limits", async () => {
|
|
51
|
+
const cookie = await loginDashboard();
|
|
52
|
+
const initial = await app.inject({
|
|
53
|
+
method: "GET",
|
|
54
|
+
url: "/api/client-token-limits/default",
|
|
55
|
+
headers: { cookie },
|
|
56
|
+
});
|
|
57
|
+
assert.equal(initial.statusCode, 200);
|
|
58
|
+
assert.equal(initial.json().client.config, null);
|
|
59
|
+
const update = await app.inject({
|
|
60
|
+
method: "PUT",
|
|
61
|
+
url: "/api/client-token-limits/default",
|
|
62
|
+
headers: { cookie },
|
|
63
|
+
payload: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
tokenLimit: 12,
|
|
66
|
+
windowType: "daily",
|
|
67
|
+
hardBlock: true,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
assert.equal(update.statusCode, 200);
|
|
71
|
+
assert.equal(update.json().client.config.tokenLimit, 12);
|
|
72
|
+
assert.equal(update.json().client.status.remaining, 12);
|
|
73
|
+
const reset = await app.inject({
|
|
74
|
+
method: "POST",
|
|
75
|
+
url: "/api/client-token-limits/default/reset",
|
|
76
|
+
headers: { cookie },
|
|
77
|
+
});
|
|
78
|
+
assert.equal(reset.statusCode, 200);
|
|
79
|
+
assert.equal(reset.json().client.usage.totalTokens, 0);
|
|
80
|
+
});
|
|
81
|
+
test("client token limit admin API accepts disabled config without usable limit values", async () => {
|
|
82
|
+
const cookie = await loginDashboard();
|
|
83
|
+
const update = await app.inject({
|
|
84
|
+
method: "PUT",
|
|
85
|
+
url: "/api/client-token-limits/default",
|
|
86
|
+
headers: { cookie },
|
|
87
|
+
payload: {
|
|
88
|
+
enabled: false,
|
|
89
|
+
tokenLimit: 0,
|
|
90
|
+
windowType: "fixed",
|
|
91
|
+
windowSizeSeconds: 0,
|
|
92
|
+
hardBlock: true,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
assert.equal(update.statusCode, 200);
|
|
96
|
+
assert.equal(update.json().client.config.enabled, false);
|
|
97
|
+
assert.equal(update.json().client.status.blocked, false);
|
|
98
|
+
});
|
|
99
|
+
test("client token limit enforcement rejects over-limit requests before upstream", async () => {
|
|
100
|
+
const cookie = await loginDashboard();
|
|
101
|
+
const now = new Date();
|
|
102
|
+
const windowStart = new Date(now);
|
|
103
|
+
windowStart.setUTCHours(0, 0, 0, 0);
|
|
104
|
+
await app.inject({
|
|
105
|
+
method: "PUT",
|
|
106
|
+
url: "/api/client-token-limits/default",
|
|
107
|
+
headers: { cookie },
|
|
108
|
+
payload: {
|
|
109
|
+
enabled: true,
|
|
110
|
+
tokenLimit: 1,
|
|
111
|
+
windowType: "daily",
|
|
112
|
+
hardBlock: true,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const db = new BetterSqlite3(dbFile);
|
|
116
|
+
try {
|
|
117
|
+
db.prepare(`INSERT INTO client_token_usage (
|
|
118
|
+
client_route,
|
|
119
|
+
window_start,
|
|
120
|
+
input_tokens,
|
|
121
|
+
output_tokens,
|
|
122
|
+
total_tokens,
|
|
123
|
+
updated_at
|
|
124
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
125
|
+
ON CONFLICT(client_route, window_start) DO UPDATE SET
|
|
126
|
+
input_tokens = excluded.input_tokens,
|
|
127
|
+
output_tokens = excluded.output_tokens,
|
|
128
|
+
total_tokens = excluded.total_tokens,
|
|
129
|
+
updated_at = excluded.updated_at`).run("default", windowStart.toISOString(), 1, 0, 1, now.toISOString());
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
const response = await app.inject({
|
|
135
|
+
method: "POST",
|
|
136
|
+
url: "/v1/responses",
|
|
137
|
+
headers: {
|
|
138
|
+
authorization: "Bearer provider-key",
|
|
139
|
+
},
|
|
140
|
+
payload: {
|
|
141
|
+
model: "test-model",
|
|
142
|
+
input: "hello",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
assert.equal(response.statusCode, 429);
|
|
146
|
+
assert.equal(response.headers["x-proxy-error-code"], "CLIENT_TOKEN_LIMIT_EXCEEDED");
|
|
147
|
+
assert.equal(response.json().error.code, "CLIENT_TOKEN_LIMIT_EXCEEDED");
|
|
148
|
+
assert.equal(response.json().error.client, "default");
|
|
149
|
+
assert.equal(response.json().error.usage.blocked, true);
|
|
150
|
+
});
|
|
151
|
+
test("bot api key can reach protected admin routes without dashboard session", async () => {
|
|
152
|
+
const response = await app.inject({
|
|
153
|
+
method: "GET",
|
|
154
|
+
url: "/api/client-configs/status",
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: "Bearer test-bot-api-key",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
assert.equal(response.statusCode, 200);
|
|
160
|
+
assert.equal(response.json().ok, true);
|
|
161
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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 { CustomerWorkspaceRepository } from "./telegram-bot/customer-workspace-repository.js";
|
|
9
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-codex-setup-"));
|
|
10
|
+
const dbFile = path.join(tempDir, "app.sqlite");
|
|
11
|
+
process.env.RESPONSES_PROXY_DISABLE_LISTEN = "true";
|
|
12
|
+
process.env.APP_DB_PATH = dbFile;
|
|
13
|
+
process.env.CUSTOMER_KEY_DB_PATH = dbFile;
|
|
14
|
+
process.env.UPSTREAM_BASE_URL = "https://upstream.example/v1";
|
|
15
|
+
process.env.UPSTREAM_API_KEY = "provider-key";
|
|
16
|
+
process.env.PROVIDER_USAGE_CHECK_ENABLED = "false";
|
|
17
|
+
process.env.CHATGPT_OAUTH_ENABLED = "false";
|
|
18
|
+
process.env.LOG_LEVEL = "silent";
|
|
19
|
+
process.env.RESPONSES_PROXY_DEFAULT_MODEL = "gpt-5.4";
|
|
20
|
+
process.env.BOT_PUBLIC_RESPONSES_BASE_URL = "https://proxy.example.com/v1";
|
|
21
|
+
const { app } = await import("./server.js");
|
|
22
|
+
const billing = BillingRepository.create(dbFile);
|
|
23
|
+
const customerKeys = CustomerKeyRepository.create(dbFile);
|
|
24
|
+
const workspaces = CustomerWorkspaceRepository.create(dbFile);
|
|
25
|
+
const workspace = workspaces.ensureDefaultWorkspace({
|
|
26
|
+
ownerTelegramUserId: "42",
|
|
27
|
+
defaultClientRoute: "default",
|
|
28
|
+
status: "active",
|
|
29
|
+
});
|
|
30
|
+
if (!billing.getPlan("curl-setup")) {
|
|
31
|
+
billing.createPlan({
|
|
32
|
+
id: "curl-setup",
|
|
33
|
+
name: "Curl Setup",
|
|
34
|
+
monthlyTokenLimit: 1_000_000,
|
|
35
|
+
maxApiKeys: 1,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
billing.grantSubscription({
|
|
39
|
+
workspaceId: workspace.id,
|
|
40
|
+
planId: "curl-setup",
|
|
41
|
+
days: 30,
|
|
42
|
+
});
|
|
43
|
+
const { apiKey } = customerKeys.createKey({
|
|
44
|
+
workspaceId: workspace.id,
|
|
45
|
+
telegramUserId: "42",
|
|
46
|
+
clientRoute: "default",
|
|
47
|
+
apiKey: "sk-customer-test-secret",
|
|
48
|
+
});
|
|
49
|
+
test.after(async () => {
|
|
50
|
+
await app.close();
|
|
51
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
test("customer Codex setup script requires customer API key auth", async () => {
|
|
54
|
+
const response = await app.inject({
|
|
55
|
+
method: "GET",
|
|
56
|
+
url: "/api/customer/codex/setup.sh",
|
|
57
|
+
});
|
|
58
|
+
assert.equal(response.statusCode, 401);
|
|
59
|
+
assert.equal(response.json().error?.code, "CUSTOMER_API_KEY_REQUIRED");
|
|
60
|
+
});
|
|
61
|
+
test("customer Codex setup script returns patch script with customer key and public base URL", async () => {
|
|
62
|
+
const response = await app.inject({
|
|
63
|
+
method: "GET",
|
|
64
|
+
url: "/api/customer/codex/setup.sh",
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${apiKey}`,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
assert.equal(response.statusCode, 200);
|
|
70
|
+
assert.match(response.headers["content-type"] ?? "", /^text\/x-shellscript/);
|
|
71
|
+
assert.equal(response.headers["cache-control"], "no-store");
|
|
72
|
+
assert.match(response.body, /model = "gpt-5\.4"/);
|
|
73
|
+
assert.match(response.body, /base_url = "https:\/\/proxy\.example\.com\/v1"/);
|
|
74
|
+
assert.match(response.body, /OPENAI_API_KEY": "sk-customer-test-secret"/);
|
|
75
|
+
assert.match(response.body, /install_file "\$tmpdir\/config\.toml" "\$HOME\/\.codex\/config\.toml"/);
|
|
76
|
+
});
|