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,209 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { AnthropicSseEmitter, buildAnthropicMessage, buildAnthropicModelsList, buildCountTokensResponse, parseAnthropicRequest, } from "./anthropic-messages.js";
|
|
4
|
+
test("parseAnthropicRequest folds system into the first user turn", () => {
|
|
5
|
+
const parsed = parseAnthropicRequest({
|
|
6
|
+
model: "claude-sonnet-4",
|
|
7
|
+
system: "You are helpful.",
|
|
8
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
9
|
+
});
|
|
10
|
+
assert.equal(parsed.turns.length, 1);
|
|
11
|
+
assert.equal(parsed.turns[0].role, "user");
|
|
12
|
+
assert.equal(parsed.turns[0].content, "You are helpful.\n\nHi");
|
|
13
|
+
assert.equal(parsed.model, "claude-sonnet-4");
|
|
14
|
+
});
|
|
15
|
+
test("parseAnthropicRequest handles system as text-block array", () => {
|
|
16
|
+
const parsed = parseAnthropicRequest({
|
|
17
|
+
model: "claude-sonnet-4",
|
|
18
|
+
system: [
|
|
19
|
+
{ type: "text", text: "Part one." },
|
|
20
|
+
{ type: "text", text: "Part two." },
|
|
21
|
+
],
|
|
22
|
+
messages: [{ role: "user", content: "Q" }],
|
|
23
|
+
});
|
|
24
|
+
assert.equal(parsed.turns[0].content, "Part one.\n\nPart two.\n\nQ");
|
|
25
|
+
});
|
|
26
|
+
test("parseAnthropicRequest parses tools (input_schema → inputSchema)", () => {
|
|
27
|
+
const parsed = parseAnthropicRequest({
|
|
28
|
+
model: "claude-sonnet-4",
|
|
29
|
+
messages: [{ role: "user", content: "weather?" }],
|
|
30
|
+
tools: [
|
|
31
|
+
{
|
|
32
|
+
name: "get_weather",
|
|
33
|
+
description: "Get weather",
|
|
34
|
+
input_schema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
assert.equal(parsed.tools.length, 1);
|
|
39
|
+
assert.equal(parsed.tools[0].name, "get_weather");
|
|
40
|
+
assert.equal(parsed.tools[0].description, "Get weather");
|
|
41
|
+
assert.deepEqual(parsed.tools[0].inputSchema.required, ["city"]);
|
|
42
|
+
});
|
|
43
|
+
test("parseAnthropicRequest extracts tool_use from assistant and tool_result from user", () => {
|
|
44
|
+
const parsed = parseAnthropicRequest({
|
|
45
|
+
model: "claude-sonnet-4",
|
|
46
|
+
messages: [
|
|
47
|
+
{ role: "user", content: "weather in NYC?" },
|
|
48
|
+
{
|
|
49
|
+
role: "assistant",
|
|
50
|
+
content: [
|
|
51
|
+
{ type: "text", text: "Let me check." },
|
|
52
|
+
{ type: "tool_use", id: "tu_1", name: "get_weather", input: { city: "NYC" } },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
role: "user",
|
|
57
|
+
content: [{ type: "tool_result", tool_use_id: "tu_1", content: "Sunny, 25C" }],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
assert.equal(parsed.turns.length, 3);
|
|
62
|
+
const assistantTurn = parsed.turns[1];
|
|
63
|
+
assert.equal(assistantTurn.role, "assistant");
|
|
64
|
+
if (assistantTurn.role === "assistant") {
|
|
65
|
+
assert.equal(assistantTurn.toolUses?.length, 1);
|
|
66
|
+
assert.equal(assistantTurn.toolUses?.[0].toolUseId, "tu_1");
|
|
67
|
+
assert.deepEqual(assistantTurn.toolUses?.[0].input, { city: "NYC" });
|
|
68
|
+
}
|
|
69
|
+
const toolResultTurn = parsed.turns[2];
|
|
70
|
+
assert.equal(toolResultTurn.role, "user");
|
|
71
|
+
if (toolResultTurn.role === "user") {
|
|
72
|
+
assert.equal(toolResultTurn.toolResults?.length, 1);
|
|
73
|
+
assert.equal(toolResultTurn.toolResults?.[0].toolUseId, "tu_1");
|
|
74
|
+
assert.equal(toolResultTurn.toolResults?.[0].content, "Sunny, 25C");
|
|
75
|
+
assert.equal(toolResultTurn.toolResults?.[0].status, "success");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
test("parseAnthropicRequest marks is_error tool_result as error status", () => {
|
|
79
|
+
const parsed = parseAnthropicRequest({
|
|
80
|
+
model: "claude-sonnet-4",
|
|
81
|
+
messages: [
|
|
82
|
+
{
|
|
83
|
+
role: "user",
|
|
84
|
+
content: [{ type: "tool_result", tool_use_id: "tu_x", content: "boom", is_error: true }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const turn = parsed.turns[0];
|
|
89
|
+
if (turn.role === "user") {
|
|
90
|
+
assert.equal(turn.toolResults?.[0].status, "error");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
test("buildAnthropicMessage returns text + tool_use content with tool_use stop_reason", () => {
|
|
94
|
+
const message = buildAnthropicMessage({
|
|
95
|
+
text: "Calling tool",
|
|
96
|
+
toolUses: [{ toolUseId: "tu_1", name: "get_weather", input: { city: "NYC" } }],
|
|
97
|
+
model: "claude-sonnet-4",
|
|
98
|
+
inputTokens: 10,
|
|
99
|
+
outputTokens: 5,
|
|
100
|
+
});
|
|
101
|
+
assert.equal(message.type, "message");
|
|
102
|
+
assert.equal(message.role, "assistant");
|
|
103
|
+
assert.equal(message.stop_reason, "tool_use");
|
|
104
|
+
const content = message.content;
|
|
105
|
+
assert.equal(content[0].type, "text");
|
|
106
|
+
assert.equal(content[1].type, "tool_use");
|
|
107
|
+
assert.equal(content[1].id, "tu_1");
|
|
108
|
+
assert.deepEqual(content[1].input, { city: "NYC" });
|
|
109
|
+
});
|
|
110
|
+
test("buildAnthropicMessage uses end_turn and a non-empty content block for text-only", () => {
|
|
111
|
+
const message = buildAnthropicMessage({
|
|
112
|
+
text: "hello",
|
|
113
|
+
toolUses: [],
|
|
114
|
+
model: "claude-sonnet-4",
|
|
115
|
+
inputTokens: 1,
|
|
116
|
+
outputTokens: 1,
|
|
117
|
+
});
|
|
118
|
+
assert.equal(message.stop_reason, "end_turn");
|
|
119
|
+
const content = message.content;
|
|
120
|
+
assert.equal(content.length, 1);
|
|
121
|
+
assert.equal(content[0].text, "hello");
|
|
122
|
+
});
|
|
123
|
+
test("buildCountTokensResponse returns an estimated input_tokens", () => {
|
|
124
|
+
const result = buildCountTokensResponse("a".repeat(40));
|
|
125
|
+
assert.equal(typeof result.input_tokens, "number");
|
|
126
|
+
assert.equal(result.input_tokens, 10);
|
|
127
|
+
});
|
|
128
|
+
function parseEvents(frames) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const frame of frames) {
|
|
131
|
+
const eventMatch = /event: (.+)/.exec(frame);
|
|
132
|
+
const dataMatch = /data: (.+)/.exec(frame);
|
|
133
|
+
if (eventMatch && dataMatch) {
|
|
134
|
+
out.push({ event: eventMatch[1].trim(), data: JSON.parse(dataMatch[1]) });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
test("AnthropicSseEmitter emits a valid text-only sequence", () => {
|
|
140
|
+
const emitter = new AnthropicSseEmitter({ model: "claude-sonnet-4", inputTokens: 5 });
|
|
141
|
+
const frames = [
|
|
142
|
+
...emitter.start(),
|
|
143
|
+
...emitter.textDelta("Hello"),
|
|
144
|
+
...emitter.textDelta(" world"),
|
|
145
|
+
...emitter.finish(2),
|
|
146
|
+
];
|
|
147
|
+
const events = parseEvents(frames);
|
|
148
|
+
const types = events.map((e) => e.event);
|
|
149
|
+
assert.equal(types[0], "message_start");
|
|
150
|
+
assert.ok(types.includes("ping"));
|
|
151
|
+
assert.ok(types.includes("content_block_start"));
|
|
152
|
+
assert.equal(types.filter((t) => t === "content_block_delta").length, 2);
|
|
153
|
+
assert.ok(types.includes("content_block_stop"));
|
|
154
|
+
assert.ok(types.includes("message_delta"));
|
|
155
|
+
assert.equal(types[types.length - 1], "message_stop");
|
|
156
|
+
const messageDelta = events.find((e) => e.event === "message_delta");
|
|
157
|
+
assert.equal((messageDelta?.data.delta).stop_reason, "end_turn");
|
|
158
|
+
});
|
|
159
|
+
test("AnthropicSseEmitter opens a tool_use block and emits input_json_delta", () => {
|
|
160
|
+
const emitter = new AnthropicSseEmitter({ model: "claude-sonnet-4", inputTokens: 5 });
|
|
161
|
+
const toolStart = { toolUseId: "tu_1", name: "get_weather", inputDelta: '{"ci' };
|
|
162
|
+
const toolMore = { toolUseId: "tu_1", inputDelta: 'ty":"NYC"}' };
|
|
163
|
+
const frames = [
|
|
164
|
+
...emitter.start(),
|
|
165
|
+
...emitter.toolUseDelta(toolStart),
|
|
166
|
+
...emitter.toolUseDelta(toolMore),
|
|
167
|
+
...emitter.finish(3),
|
|
168
|
+
];
|
|
169
|
+
const events = parseEvents(frames);
|
|
170
|
+
const blockStart = events.find((e) => e.event === "content_block_start");
|
|
171
|
+
assert.equal((blockStart?.data.content_block).type, "tool_use");
|
|
172
|
+
assert.equal((blockStart?.data.content_block).id, "tu_1");
|
|
173
|
+
const deltas = events.filter((e) => e.event === "content_block_delta");
|
|
174
|
+
assert.equal(deltas.length, 2);
|
|
175
|
+
assert.equal(deltas[0].data.delta.type, "input_json_delta");
|
|
176
|
+
assert.equal(deltas[0].data.delta.partial_json, '{"ci');
|
|
177
|
+
const messageDelta = events.find((e) => e.event === "message_delta");
|
|
178
|
+
assert.equal((messageDelta?.data.delta).stop_reason, "tool_use");
|
|
179
|
+
});
|
|
180
|
+
test("AnthropicSseEmitter transitions from text block to tool block (closes first)", () => {
|
|
181
|
+
const emitter = new AnthropicSseEmitter({ model: "claude-sonnet-4", inputTokens: 1 });
|
|
182
|
+
const frames = [
|
|
183
|
+
...emitter.start(),
|
|
184
|
+
...emitter.textDelta("thinking"),
|
|
185
|
+
...emitter.toolUseDelta({ toolUseId: "tu_1", name: "f", inputDelta: "{}" }),
|
|
186
|
+
...emitter.finish(1),
|
|
187
|
+
];
|
|
188
|
+
const events = parseEvents(frames);
|
|
189
|
+
const types = events.map((e) => e.event);
|
|
190
|
+
// Two block_start (text, tool) and two block_stop (one before tool, one at finish).
|
|
191
|
+
assert.equal(types.filter((t) => t === "content_block_start").length, 2);
|
|
192
|
+
assert.equal(types.filter((t) => t === "content_block_stop").length, 2);
|
|
193
|
+
});
|
|
194
|
+
test("buildAnthropicModelsList returns a deduped Anthropic-format listing", () => {
|
|
195
|
+
const list = buildAnthropicModelsList([
|
|
196
|
+
"claude-sonnet-4",
|
|
197
|
+
"claude-sonnet-4",
|
|
198
|
+
" ",
|
|
199
|
+
"claude-haiku-4-5",
|
|
200
|
+
]);
|
|
201
|
+
const data = list.data;
|
|
202
|
+
assert.equal(data.length, 2);
|
|
203
|
+
assert.equal(data[0].type, "model");
|
|
204
|
+
assert.equal(data[0].id, "claude-sonnet-4");
|
|
205
|
+
assert.equal(typeof data[0].display_name, "string");
|
|
206
|
+
assert.equal(list.has_more, false);
|
|
207
|
+
assert.equal(list.first_id, "claude-sonnet-4");
|
|
208
|
+
assert.equal(list.last_id, "claude-haiku-4-5");
|
|
209
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
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 AuditLogRepository {
|
|
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
|
+
ensureAuditLogSchema(db);
|
|
14
|
+
return new AuditLogRepository(db);
|
|
15
|
+
}
|
|
16
|
+
record(input) {
|
|
17
|
+
const id = randomUUID();
|
|
18
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
19
|
+
this.db
|
|
20
|
+
.prepare(`INSERT INTO audit_log (
|
|
21
|
+
id,
|
|
22
|
+
event,
|
|
23
|
+
actor_type,
|
|
24
|
+
actor_id,
|
|
25
|
+
subject_type,
|
|
26
|
+
subject_id,
|
|
27
|
+
metadata_json,
|
|
28
|
+
created_at
|
|
29
|
+
)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
31
|
+
.run(id, input.event, input.actor?.type ?? "system", input.actor?.id ?? null, input.subjectType ?? null, input.subjectId ?? null, JSON.stringify(sanitizeMetadata(input.metadata ?? {})), now);
|
|
32
|
+
return this.listEvents({ limit: 1, subjectId: input.subjectId, event: input.event })[0];
|
|
33
|
+
}
|
|
34
|
+
listEvents(filter = {}) {
|
|
35
|
+
const conditions = [];
|
|
36
|
+
const values = [];
|
|
37
|
+
if (filter.event) {
|
|
38
|
+
conditions.push("event = ?");
|
|
39
|
+
values.push(filter.event);
|
|
40
|
+
}
|
|
41
|
+
if (filter.subjectId) {
|
|
42
|
+
conditions.push("subject_id = ?");
|
|
43
|
+
values.push(filter.subjectId);
|
|
44
|
+
}
|
|
45
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
46
|
+
const limit = filter.limit ? `LIMIT ${Math.max(1, Math.trunc(filter.limit))}` : "";
|
|
47
|
+
const rows = this.db
|
|
48
|
+
.prepare(`SELECT * FROM audit_log
|
|
49
|
+
${where}
|
|
50
|
+
ORDER BY created_at DESC
|
|
51
|
+
${limit}`)
|
|
52
|
+
.all(...values);
|
|
53
|
+
return rows.map(mapAuditLogRow);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function ensureAuditLogSchema(db) {
|
|
57
|
+
db.exec(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
event TEXT NOT NULL,
|
|
61
|
+
actor_type TEXT NOT NULL,
|
|
62
|
+
actor_id TEXT,
|
|
63
|
+
subject_type TEXT,
|
|
64
|
+
subject_id TEXT,
|
|
65
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
66
|
+
created_at TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_event_created_at
|
|
70
|
+
ON audit_log(event, created_at DESC);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_subject
|
|
73
|
+
ON audit_log(subject_type, subject_id, created_at DESC);
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
function mapAuditLogRow(row) {
|
|
77
|
+
return {
|
|
78
|
+
id: row.id,
|
|
79
|
+
event: normalizeEventName(row.event),
|
|
80
|
+
actorType: normalizeActorType(row.actor_type),
|
|
81
|
+
actorId: row.actor_id ?? undefined,
|
|
82
|
+
subjectType: row.subject_type ?? undefined,
|
|
83
|
+
subjectId: row.subject_id ?? undefined,
|
|
84
|
+
metadata: parseMetadata(row.metadata_json),
|
|
85
|
+
createdAt: row.created_at,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseMetadata(raw) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
return isRecord(parsed) ? parsed : {};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function sanitizeMetadata(value, keyName) {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
return value.map((entry) => sanitizeMetadata(entry));
|
|
100
|
+
}
|
|
101
|
+
if (isRecord(value)) {
|
|
102
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeMetadata(entry, key)]));
|
|
103
|
+
}
|
|
104
|
+
if (typeof value === "string" && keyName && /(api[_-]?key|token|authorization)/i.test(keyName)) {
|
|
105
|
+
return "[redacted]";
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function normalizeEventName(value) {
|
|
110
|
+
return [
|
|
111
|
+
"user.created",
|
|
112
|
+
"user.approved",
|
|
113
|
+
"workspace.created",
|
|
114
|
+
"workspace.approved",
|
|
115
|
+
"api_key.created",
|
|
116
|
+
"api_key.revealed",
|
|
117
|
+
"api_key.rotated",
|
|
118
|
+
"api_key.revoked",
|
|
119
|
+
"api_key.suspended",
|
|
120
|
+
"api_key.activated",
|
|
121
|
+
"subscription.granted",
|
|
122
|
+
"subscription.renewed",
|
|
123
|
+
"renewal.requested",
|
|
124
|
+
"payment.confirmed_manual",
|
|
125
|
+
"payment.confirmed_sepay",
|
|
126
|
+
"payment.amount_mismatch",
|
|
127
|
+
"renewal.approved",
|
|
128
|
+
"renewal.closed",
|
|
129
|
+
].includes(value)
|
|
130
|
+
? value
|
|
131
|
+
: "renewal.requested";
|
|
132
|
+
}
|
|
133
|
+
function normalizeActorType(value) {
|
|
134
|
+
return value === "admin" || value === "customer" || value === "bot" ? value : "system";
|
|
135
|
+
}
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
138
|
+
}
|