fest-gateway 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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/cli/claude.js +326 -0
- package/dist/cli/claude.js.map +1 -0
- package/dist/cli/config.js +59 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/login.js +130 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/logout.js +20 -0
- package/dist/cli/logout.js.map +1 -0
- package/dist/cli/whoami.js +34 -0
- package/dist/cli/whoami.js.map +1 -0
- package/dist/server/adapters/anthropic.js +74 -0
- package/dist/server/adapters/anthropic.js.map +1 -0
- package/dist/server/adapters/fireworks.js +54 -0
- package/dist/server/adapters/fireworks.js.map +1 -0
- package/dist/server/adapters/index.js +16 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/registry.js +38 -0
- package/dist/server/adapters/registry.js.map +1 -0
- package/dist/server/adapters/rewrite.js +152 -0
- package/dist/server/adapters/rewrite.js.map +1 -0
- package/dist/server/adapters/url.js +24 -0
- package/dist/server/adapters/url.js.map +1 -0
- package/dist/server/api/auth.js +213 -0
- package/dist/server/api/auth.js.map +1 -0
- package/dist/server/api/live.js +108 -0
- package/dist/server/api/live.js.map +1 -0
- package/dist/server/api/models.js +129 -0
- package/dist/server/api/models.js.map +1 -0
- package/dist/server/api/oauth.js +300 -0
- package/dist/server/api/oauth.js.map +1 -0
- package/dist/server/api/routes.js +164 -0
- package/dist/server/api/routes.js.map +1 -0
- package/dist/server/api/routing-view.js +47 -0
- package/dist/server/api/routing-view.js.map +1 -0
- package/dist/server/auth/accounts.js +111 -0
- package/dist/server/auth/accounts.js.map +1 -0
- package/dist/server/auth/gateway-401.js +85 -0
- package/dist/server/auth/gateway-401.js.map +1 -0
- package/dist/server/auth/guard.js +74 -0
- package/dist/server/auth/guard.js.map +1 -0
- package/dist/server/auth/oauth.js +43 -0
- package/dist/server/auth/oauth.js.map +1 -0
- package/dist/server/auth/password.js +104 -0
- package/dist/server/auth/password.js.map +1 -0
- package/dist/server/auth/posture.js +145 -0
- package/dist/server/auth/posture.js.map +1 -0
- package/dist/server/auth/session.js +161 -0
- package/dist/server/auth/session.js.map +1 -0
- package/dist/server/bin/fest.js +589 -0
- package/dist/server/bin/fest.js.map +1 -0
- package/dist/server/config.js +109 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/credentials/provider.js +70 -0
- package/dist/server/credentials/provider.js.map +1 -0
- package/dist/server/credentials/resolve.js +89 -0
- package/dist/server/credentials/resolve.js.map +1 -0
- package/dist/server/http/body.js +66 -0
- package/dist/server/http/body.js.map +1 -0
- package/dist/server/http/errors.js +144 -0
- package/dist/server/http/errors.js.map +1 -0
- package/dist/server/http/headers.js +189 -0
- package/dist/server/http/headers.js.map +1 -0
- package/dist/server/http/pipe.js +97 -0
- package/dist/server/http/pipe.js.map +1 -0
- package/dist/server/http/server.js +226 -0
- package/dist/server/http/server.js.map +1 -0
- package/dist/server/http/sse.js +167 -0
- package/dist/server/http/sse.js.map +1 -0
- package/dist/server/http/static.js +138 -0
- package/dist/server/http/static.js.map +1 -0
- package/dist/server/ingest/live-bus.js +67 -0
- package/dist/server/ingest/live-bus.js.map +1 -0
- package/dist/server/ingest/sink.js +141 -0
- package/dist/server/ingest/sink.js.map +1 -0
- package/dist/server/log.js +31 -0
- package/dist/server/log.js.map +1 -0
- package/dist/server/pipeline/count-tokens.js +48 -0
- package/dist/server/pipeline/count-tokens.js.map +1 -0
- package/dist/server/pipeline/dispatch.js +138 -0
- package/dist/server/pipeline/dispatch.js.map +1 -0
- package/dist/server/pipeline/passthrough.js +392 -0
- package/dist/server/pipeline/passthrough.js.map +1 -0
- package/dist/server/pipeline/preflight.js +66 -0
- package/dist/server/pipeline/preflight.js.map +1 -0
- package/dist/server/pipeline/record.js +49 -0
- package/dist/server/pipeline/record.js.map +1 -0
- package/dist/server/pipeline/substitute.js +287 -0
- package/dist/server/pipeline/substitute.js.map +1 -0
- package/dist/server/routes/resolve.js +176 -0
- package/dist/server/routes/resolve.js.map +1 -0
- package/dist/server/routes/table.js +247 -0
- package/dist/server/routes/table.js.map +1 -0
- package/dist/server/routes/watch.js +101 -0
- package/dist/server/routes/watch.js.map +1 -0
- package/dist/server/secret/fingerprint.js +107 -0
- package/dist/server/secret/fingerprint.js.map +1 -0
- package/dist/server/secret/non-persistable.js +69 -0
- package/dist/server/secret/non-persistable.js.map +1 -0
- package/dist/server/store/audit.js +53 -0
- package/dist/server/store/audit.js.map +1 -0
- package/dist/server/store/bootstrap.js +53 -0
- package/dist/server/store/bootstrap.js.map +1 -0
- package/dist/server/store/db.js +157 -0
- package/dist/server/store/db.js.map +1 -0
- package/dist/server/store/ids.js +14 -0
- package/dist/server/store/ids.js.map +1 -0
- package/dist/server/store/migrations/001-init.sql +213 -0
- package/dist/server/store/migrations/002-feed-and-error-indexes.sql +26 -0
- package/dist/server/store/migrations/003-routing.sql +42 -0
- package/dist/server/store/migrations/004-admin-auth.sql +72 -0
- package/dist/server/store/migrations/005-notional-cost.sql +28 -0
- package/dist/server/store/queries.js +904 -0
- package/dist/server/store/queries.js.map +1 -0
- package/dist/server/store/retention.js +79 -0
- package/dist/server/store/retention.js.map +1 -0
- package/dist/server/store/seed.js +203 -0
- package/dist/server/store/seed.js.map +1 -0
- package/dist/server/store/tokens.js +133 -0
- package/dist/server/store/tokens.js.map +1 -0
- package/dist/server/store/write.js +330 -0
- package/dist/server/store/write.js.map +1 -0
- package/dist/server/usage/accumulator.js +209 -0
- package/dist/server/usage/accumulator.js.map +1 -0
- package/dist/server/usage/cost.js +57 -0
- package/dist/server/usage/cost.js.map +1 -0
- package/dist/server/usage/prices/catalog.js +136 -0
- package/dist/server/usage/prices/catalog.js.map +1 -0
- package/dist/server/usage/prices/refresh.js +125 -0
- package/dist/server/usage/prices/refresh.js.map +1 -0
- package/dist/server/usage/prices/snapshot.json.gz +0 -0
- package/dist/server/usage/prices/table.js +102 -0
- package/dist/server/usage/prices/table.js.map +1 -0
- package/dist/server/usage/pricing.js +103 -0
- package/dist/server/usage/pricing.js.map +1 -0
- package/dist/shared/api.js +19 -0
- package/dist/shared/api.js.map +1 -0
- package/dist/shared/base-models.js +28 -0
- package/dist/shared/base-models.js.map +1 -0
- package/dist/shared/demotion-vars.js +17 -0
- package/dist/shared/demotion-vars.js.map +1 -0
- package/dist/shared/series.js +50 -0
- package/dist/shared/series.js.map +1 -0
- package/dist/shared/types.js +40 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/web/dist/assets/index-8HlKsJqt.css +2 -0
- package/dist/web/dist/assets/index-DQ7fyPVm.js +56 -0
- package/dist/web/dist/assets/index-DQ7fyPVm.js.map +1 -0
- package/dist/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist/web/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist/web/dist/index.html +44 -0
- package/dist/web/dist/nearform-icon.svg +4 -0
- package/package.json +82 -0
- package/routes.example.json +22 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routing table: which model goes where, and on whose credential.
|
|
3
|
+
*
|
|
4
|
+
* Loaded from a JSON file (`FEST_ROUTES`), validated in full at boot, and
|
|
5
|
+
* immutable thereafter. Validation is deliberately strict and noisy — a
|
|
6
|
+
* mistyped upstream name that silently disables a route would send traffic to
|
|
7
|
+
* Anthropic on a developer's subscription when the operator believed it was
|
|
8
|
+
* going to Fireworks, and nothing would say so. Every rejection names the
|
|
9
|
+
* offending entry.
|
|
10
|
+
*
|
|
11
|
+
* The plan's eventual design is a versioned, published, ETagged table in
|
|
12
|
+
* SQLite so an admin UI can edit it. That is deferred: it only earns its
|
|
13
|
+
* complexity once something can edit it, and nothing can until the admin work
|
|
14
|
+
* lands. A file is diffable, reviewable and atomic to deploy, which covers the
|
|
15
|
+
* spike. The parsed shape is what the store would hold, so that migration is
|
|
16
|
+
* additive.
|
|
17
|
+
*
|
|
18
|
+
* Example:
|
|
19
|
+
*
|
|
20
|
+
* {
|
|
21
|
+
* "upstreams": {
|
|
22
|
+
* "fireworks": {
|
|
23
|
+
* "adapter": "fireworks",
|
|
24
|
+
* "baseUrl": "https://api.fireworks.ai/inference",
|
|
25
|
+
* "credential": "{env:FIREWORKS_API_KEY}"
|
|
26
|
+
* }
|
|
27
|
+
* },
|
|
28
|
+
* "routes": [
|
|
29
|
+
* { "id": "oss-120b",
|
|
30
|
+
* "match": "gpt-oss-120b",
|
|
31
|
+
* "upstream": "fireworks",
|
|
32
|
+
* "model": "accounts/fireworks/models/gpt-oss-120b" }
|
|
33
|
+
* ]
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
import { explainSecretRef, parseSecretRef } from "../credentials/provider.js";
|
|
37
|
+
/** Adapters that exist. Anything else is a config error, not a silent no-op. */
|
|
38
|
+
export const ADAPTERS = ["anthropic", "fireworks"];
|
|
39
|
+
export class RouteConfigError extends Error {
|
|
40
|
+
constructor(problems) {
|
|
41
|
+
super(`Fest routing config is invalid:\n` +
|
|
42
|
+
problems.map((p) => ` - ${p}`).join("\n") +
|
|
43
|
+
`\n\nFest will not start with a routing table it cannot honour: a route that\n` +
|
|
44
|
+
`silently does not apply sends traffic somewhere the operator did not intend.`);
|
|
45
|
+
this.name = "RouteConfigError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function isRecord(v) {
|
|
49
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
50
|
+
}
|
|
51
|
+
/** Cheap, stable content hash. Not cryptographic — it identifies, not protects. */
|
|
52
|
+
function hashOf(text) {
|
|
53
|
+
let h1 = 0x811c9dc5;
|
|
54
|
+
let h2 = 0x01000193;
|
|
55
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
56
|
+
const c = text.charCodeAt(i);
|
|
57
|
+
h1 = Math.imul(h1 ^ c, 16777619) >>> 0;
|
|
58
|
+
h2 = Math.imul(h2 + c, 2654435761) >>> 0;
|
|
59
|
+
}
|
|
60
|
+
return (h1.toString(16) + h2.toString(16)).padStart(16, "0");
|
|
61
|
+
}
|
|
62
|
+
export function parseRouteTable(text) {
|
|
63
|
+
const problems = [];
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = JSON.parse(text);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
throw new RouteConfigError([`not valid JSON: ${err.message}`]);
|
|
70
|
+
}
|
|
71
|
+
if (!isRecord(raw))
|
|
72
|
+
throw new RouteConfigError(["top level must be an object"]);
|
|
73
|
+
// ── upstreams ───────────────────────────────────────────────────────────────
|
|
74
|
+
const upstreams = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Upstreams that were declared but failed validation.
|
|
77
|
+
*
|
|
78
|
+
* Tracked separately so one root cause produces one message. Without this, a
|
|
79
|
+
* mistyped credential on `fireworks` reports both "bad credential" AND
|
|
80
|
+
* "unknown upstream fireworks" for every route that referenced it — and an
|
|
81
|
+
* operator reading a wall of cascading errors is materially more likely to
|
|
82
|
+
* fix the wrong one.
|
|
83
|
+
*/
|
|
84
|
+
const declared = new Set();
|
|
85
|
+
const rawUpstreams = raw["upstreams"];
|
|
86
|
+
if (rawUpstreams !== undefined) {
|
|
87
|
+
if (!isRecord(rawUpstreams)) {
|
|
88
|
+
problems.push("`upstreams` must be an object keyed by upstream id");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
for (const [id, value] of Object.entries(rawUpstreams)) {
|
|
92
|
+
const at = `upstream ${JSON.stringify(id)}`;
|
|
93
|
+
declared.add(id);
|
|
94
|
+
if (!isRecord(value)) {
|
|
95
|
+
problems.push(`${at}: must be an object`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const adapter = value["adapter"];
|
|
99
|
+
if (typeof adapter !== "string" || !ADAPTERS.includes(adapter)) {
|
|
100
|
+
problems.push(`${at}: adapter must be one of ${ADAPTERS.join(", ")} (got ${JSON.stringify(adapter)})`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const baseUrl = value["baseUrl"];
|
|
104
|
+
if (typeof baseUrl !== "string") {
|
|
105
|
+
problems.push(`${at}: baseUrl is required`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = new URL(baseUrl);
|
|
110
|
+
// A credential sent over plaintext HTTP is a credential disclosed.
|
|
111
|
+
// Loopback is exempt so a local mock upstream stays testable.
|
|
112
|
+
if (parsed.protocol !== "https:" &&
|
|
113
|
+
!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
|
|
114
|
+
problems.push(`${at}: baseUrl must be https (a server-held credential would otherwise cross the network in plaintext)`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
problems.push(`${at}: baseUrl is not a valid URL: ${JSON.stringify(baseUrl)}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const credential = value["credential"];
|
|
122
|
+
if (typeof credential !== "string") {
|
|
123
|
+
problems.push(`${at}: credential is required, as a reference like {env:NAME}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const why = explainSecretRef(credential);
|
|
127
|
+
if (why !== null) {
|
|
128
|
+
problems.push(`${at}: ${why}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
upstreams.set(id, {
|
|
132
|
+
id,
|
|
133
|
+
adapter: adapter,
|
|
134
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
135
|
+
credential: parseSecretRef(credential),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── routes ──────────────────────────────────────────────────────────────────
|
|
141
|
+
const routes = [];
|
|
142
|
+
const rawRoutes = raw["routes"];
|
|
143
|
+
const seenIds = new Set();
|
|
144
|
+
const exposed = new Set();
|
|
145
|
+
if (rawRoutes !== undefined) {
|
|
146
|
+
if (!Array.isArray(rawRoutes)) {
|
|
147
|
+
problems.push("`routes` must be an array");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
rawRoutes.forEach((value, index) => {
|
|
151
|
+
const at = `route #${index}`;
|
|
152
|
+
if (!isRecord(value)) {
|
|
153
|
+
problems.push(`${at}: must be an object`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const id = value["id"];
|
|
157
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
158
|
+
problems.push(`${at}: id is required (it names this decision in every usage record)`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (seenIds.has(id)) {
|
|
162
|
+
problems.push(`${at}: duplicate route id ${JSON.stringify(id)}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
seenIds.add(id);
|
|
166
|
+
const match = value["match"];
|
|
167
|
+
if (typeof match !== "string" || match.trim() === "") {
|
|
168
|
+
problems.push(`route ${JSON.stringify(id)}: match is required`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (match.indexOf("*") !== -1 && !match.endsWith("*")) {
|
|
172
|
+
problems.push(`route ${JSON.stringify(id)}: '*' is only supported as a trailing wildcard (got ${JSON.stringify(match)})`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const upstream = value["upstream"] ?? null;
|
|
176
|
+
if (upstream !== null && typeof upstream !== "string") {
|
|
177
|
+
problems.push(`route ${JSON.stringify(id)}: upstream must be a string or null`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (typeof upstream === "string" && !upstreams.has(upstream)) {
|
|
181
|
+
if (!declared.has(upstream)) {
|
|
182
|
+
problems.push(`route ${JSON.stringify(id)}: unknown upstream ${JSON.stringify(upstream)}. ` +
|
|
183
|
+
`Known: ${[...declared].join(", ") || "(none)"}`);
|
|
184
|
+
}
|
|
185
|
+
// Declared-but-invalid: its own error is already reported above, and
|
|
186
|
+
// repeating it per referencing route buries the root cause.
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const model = value["model"] ?? null;
|
|
190
|
+
if (model !== null && typeof model !== "string") {
|
|
191
|
+
problems.push(`route ${JSON.stringify(id)}: model must be a string or null`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const expose = value["expose"] ?? null;
|
|
195
|
+
if (expose !== null && typeof expose !== "string") {
|
|
196
|
+
problems.push(`route ${JSON.stringify(id)}: expose must be a string or null`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (typeof expose === "string") {
|
|
200
|
+
if (expose.includes("*")) {
|
|
201
|
+
problems.push(`route ${JSON.stringify(id)}: expose must be a concrete id, not a pattern — it is what a developer selects`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Publishing an id the client filters out is worse than not
|
|
205
|
+
// publishing: the entry vanishes rather than erroring, and the
|
|
206
|
+
// operator has no way to tell the difference.
|
|
207
|
+
if (!/(claude|anthropic)/i.test(expose)) {
|
|
208
|
+
problems.push(`route ${JSON.stringify(id)}: expose ${JSON.stringify(expose)} does not match /(claude|anthropic)/i, ` +
|
|
209
|
+
`so Claude Code would silently drop it from the model menu`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (exposed.has(expose)) {
|
|
213
|
+
problems.push(`route ${JSON.stringify(id)}: expose ${JSON.stringify(expose)} is already used by another route`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
exposed.add(expose);
|
|
217
|
+
}
|
|
218
|
+
routes.push({ id, match, upstream, model, expose });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// An upstream nothing routes to is dead config. It is a warning-shaped
|
|
223
|
+
// problem, but it is raised as an error for the same reason as the rest: the
|
|
224
|
+
// operator believes traffic is going there, and it is not.
|
|
225
|
+
for (const [id, upstream] of upstreams) {
|
|
226
|
+
// An `anthropic` upstream is reachable with no route pointing at it: it is
|
|
227
|
+
// the default destination for Anthropic models in the key posture, where
|
|
228
|
+
// there is no caller credential to pass through. See
|
|
229
|
+
// `resolveWithoutCallerCredential`. Demanding a route per model id here
|
|
230
|
+
// would be config whose only purpose is to satisfy this check.
|
|
231
|
+
if (upstream.adapter === "anthropic")
|
|
232
|
+
continue;
|
|
233
|
+
if (!routes.some((r) => r.upstream === id)) {
|
|
234
|
+
problems.push(`upstream ${JSON.stringify(id)} is defined but no route targets it, so nothing will ever reach it`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (problems.length > 0)
|
|
238
|
+
throw new RouteConfigError(problems);
|
|
239
|
+
return { upstreams, routes, version: hashOf(text) };
|
|
240
|
+
}
|
|
241
|
+
/** The table used when no routing config is present: everything passes through. */
|
|
242
|
+
export const EMPTY_ROUTE_TABLE = {
|
|
243
|
+
upstreams: new Map(),
|
|
244
|
+
routes: [],
|
|
245
|
+
version: "empty",
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.js","sourceRoot":"","sources":["../../../server/routes/table.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot-reload the routing table when its file changes.
|
|
3
|
+
*
|
|
4
|
+
* Routing config is the thing an operator iterates on — add a route, try it,
|
|
5
|
+
* adjust the model id — and a restart per edit is both slow and, under
|
|
6
|
+
* `node --watch`, invisible: the watcher only tracks imported `.ts` files, so
|
|
7
|
+
* editing `routes.json` appeared to do nothing at all. That is a worse failure
|
|
8
|
+
* than a slow one, because it looks like the config is wrong.
|
|
9
|
+
*
|
|
10
|
+
* The rule that makes this safe: **a table that fails to validate is REJECTED,
|
|
11
|
+
* and the previous one stays in force.** Never degrade to "no routing" on a bad
|
|
12
|
+
* edit — that would silently send substituted traffic back onto developers'
|
|
13
|
+
* subscriptions, which is the exact failure the whole project is designed
|
|
14
|
+
* against. A typo should change nothing, loudly.
|
|
15
|
+
*/
|
|
16
|
+
import { watch } from "node:fs";
|
|
17
|
+
import { readFile, stat } from "node:fs/promises";
|
|
18
|
+
import { parseRouteTable } from "./table.js";
|
|
19
|
+
import { log } from "../log.js";
|
|
20
|
+
/** Editors write-then-rename, so one save can emit several events. */
|
|
21
|
+
const SETTLE_MS = 150;
|
|
22
|
+
/**
|
|
23
|
+
* Backstop poll interval.
|
|
24
|
+
*
|
|
25
|
+
* `fs.watch` is not reliable everywhere: it drops events fired immediately
|
|
26
|
+
* after the watcher attaches, and on several container and network mounts it
|
|
27
|
+
* never fires at all. Since the whole point is that an edit takes effect
|
|
28
|
+
* without a restart, "usually notices" is not good enough — a cheap `stat`
|
|
29
|
+
* every few seconds turns a missed event into a few seconds of delay rather
|
|
30
|
+
* than silence.
|
|
31
|
+
*/
|
|
32
|
+
const POLL_MS = 2_000;
|
|
33
|
+
export function watchRoutes(path, initial) {
|
|
34
|
+
let table = initial;
|
|
35
|
+
let timer = null;
|
|
36
|
+
let watcher = null;
|
|
37
|
+
let lastMtimeMs = null;
|
|
38
|
+
const reload = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const next = parseRouteTable(await readFile(path, "utf8"));
|
|
41
|
+
if (next.version === table.version)
|
|
42
|
+
return;
|
|
43
|
+
table = next;
|
|
44
|
+
log.info("routing table reloaded", {
|
|
45
|
+
path,
|
|
46
|
+
version: next.version,
|
|
47
|
+
routes: next.routes.length,
|
|
48
|
+
upstreams: [...next.upstreams.keys()],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// Loudly, and without changing anything.
|
|
53
|
+
log.error("routing table reload REJECTED; keeping the previous table", {
|
|
54
|
+
path,
|
|
55
|
+
version: table.version,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Backstop: notice a change even when no watch event arrives.
|
|
61
|
+
const poll = setInterval(() => {
|
|
62
|
+
void stat(path)
|
|
63
|
+
.then((st) => {
|
|
64
|
+
if (lastMtimeMs !== null && st.mtimeMs === lastMtimeMs)
|
|
65
|
+
return;
|
|
66
|
+
lastMtimeMs = st.mtimeMs;
|
|
67
|
+
return reload();
|
|
68
|
+
})
|
|
69
|
+
.catch(() => {
|
|
70
|
+
// The file may be mid-rename by an editor; the next tick will catch it.
|
|
71
|
+
});
|
|
72
|
+
}, POLL_MS);
|
|
73
|
+
poll.unref();
|
|
74
|
+
try {
|
|
75
|
+
watcher = watch(path, () => {
|
|
76
|
+
if (timer !== null)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
timer = setTimeout(() => void reload(), SETTLE_MS);
|
|
79
|
+
});
|
|
80
|
+
// Never hold the process open just to watch config.
|
|
81
|
+
watcher.unref();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Not fatal: the gateway runs fine, it just will not notice edits. Some
|
|
85
|
+
// filesystems (and some container mounts) do not support watching.
|
|
86
|
+
log.warn("could not watch routing table; edits need a restart", {
|
|
87
|
+
path,
|
|
88
|
+
error: String(err).slice(0, 120),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
current: () => table,
|
|
93
|
+
stop: () => {
|
|
94
|
+
if (timer !== null)
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
clearInterval(poll);
|
|
97
|
+
watcher?.close();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../../../server/routes/watch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhC,sEAAsE;AACtE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;;;;GASG;AACH,MAAM,OAAO,GAAG,KAAK,CAAC;AAOtB,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAAmB;IAC3D,IAAI,KAAK,GAAG,OAAO,CAAC;IACpB,IAAI,KAAK,GAA0B,IAAI,CAAC;IACxC,IAAI,OAAO,GAAqB,IAAI,CAAC;IACrC,IAAI,WAAW,GAAkB,IAAI,CAAC;IAEtC,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC3D,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO;gBAAE,OAAO;YAC3C,KAAK,GAAG,IAAI,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,wBAAwB,EAAE;gBACjC,IAAI;gBACJ,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;gBAC1B,SAAS,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;aACtC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,yCAAyC;YACzC,GAAG,CAAC,KAAK,CAAC,2DAA2D,EAAE;gBACrE,IAAI;gBACJ,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;IAEF,8DAA8D;IAC9D,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,KAAK,IAAI,CAAC,IAAI,CAAC;aACZ,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE;YACX,IAAI,WAAW,KAAK,IAAI,IAAI,EAAE,CAAC,OAAO,KAAK,WAAW;gBAAE,OAAO;YAC/D,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC;YACzB,OAAO,MAAM,EAAE,CAAC;QAClB,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,wEAAwE;QAC1E,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,OAAO,CAAC,CAAC;IACZ,IAAI,CAAC,KAAK,EAAE,CAAC;IAEb,IAAI,CAAC;QACH,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE;YACzB,IAAI,KAAK,KAAK,IAAI;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YACxC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,EAAE,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QACH,oDAAoD;QACpD,OAAO,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wEAAwE;QACxE,mEAAmE;QACnE,GAAG,CAAC,IAAI,CAAC,qDAAqD,EAAE;YAC9D,IAAI;YACJ,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;SACjC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK;QACpB,IAAI,EAAE,GAAG,EAAE;YACT,IAAI,KAAK,KAAK,IAAI;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YACxC,aAAa,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO,EAAE,KAAK,EAAE,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential classification and fingerprinting.
|
|
3
|
+
*
|
|
4
|
+
* Fest never persists a provider credential. On the subscription path it has no
|
|
5
|
+
* store to put one in: a bearer arrives on a request, is forwarded to Anthropic,
|
|
6
|
+
* and is forgotten. What we keep instead is a *fingerprint* — enough to
|
|
7
|
+
* correlate requests and detect one identity token being shared by two people,
|
|
8
|
+
* and not enough to reconstruct the secret.
|
|
9
|
+
*
|
|
10
|
+
* Used by the request path, the log redactor, and the Phase 0 capture server, so
|
|
11
|
+
* there is exactly one definition of "what is a secret".
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
/** Headers whose values are credentials. Never log or persist these raw. */
|
|
15
|
+
export const SECRET_HEADERS = new Set([
|
|
16
|
+
"authorization",
|
|
17
|
+
"x-api-key",
|
|
18
|
+
"proxy-authorization",
|
|
19
|
+
"x-anthropic-api-key",
|
|
20
|
+
"x-fireworks-api-key",
|
|
21
|
+
"cookie",
|
|
22
|
+
"set-cookie",
|
|
23
|
+
// Fest's own identity token is a revocable bearer too.
|
|
24
|
+
"x-fest-token",
|
|
25
|
+
]);
|
|
26
|
+
/** Hop-by-hop headers, never relayed to an upstream. */
|
|
27
|
+
export const HOP_BY_HOP = new Set([
|
|
28
|
+
"host",
|
|
29
|
+
"connection",
|
|
30
|
+
"keep-alive",
|
|
31
|
+
"transfer-encoding",
|
|
32
|
+
"upgrade",
|
|
33
|
+
"proxy-connection",
|
|
34
|
+
"te",
|
|
35
|
+
"trailer",
|
|
36
|
+
]);
|
|
37
|
+
const PREFIX_CHARS = 11;
|
|
38
|
+
const FINGERPRINT_CHARS = 12;
|
|
39
|
+
/** Stable, non-reversible short fingerprint. Safe to log and store. */
|
|
40
|
+
export function fingerprint(value) {
|
|
41
|
+
return createHash("sha256").update(value).digest("hex").slice(0, FINGERPRINT_CHARS);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Identify a credential family from its prefix without revealing the secret.
|
|
45
|
+
*
|
|
46
|
+
* The distinction that matters most: `sk-ant-oat…` is a subscription OAuth
|
|
47
|
+
* token (the developer's own, forwarded and forgotten) while `sk-ant-api…` is an
|
|
48
|
+
* API key (metered, possibly server-held). They are billed differently and must
|
|
49
|
+
* never be conflated in reporting.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyCredential(header, rawValue) {
|
|
52
|
+
const value = rawValue.trim();
|
|
53
|
+
const bearer = /^Bearer\s+(.*)$/i.exec(value);
|
|
54
|
+
const token = bearer ? (bearer[1] ?? "") : value;
|
|
55
|
+
let kind = "UNKNOWN";
|
|
56
|
+
if (token.length === 0)
|
|
57
|
+
kind = "EMPTY";
|
|
58
|
+
else if (token.startsWith("sk-ant-oat"))
|
|
59
|
+
kind = "ANTHROPIC_OAUTH_SUBSCRIPTION";
|
|
60
|
+
else if (token.startsWith("sk-ant-api"))
|
|
61
|
+
kind = "ANTHROPIC_API_KEY";
|
|
62
|
+
else if (token.startsWith("sk-ant-"))
|
|
63
|
+
kind = "ANTHROPIC_OTHER";
|
|
64
|
+
else if (token.startsWith("fw_"))
|
|
65
|
+
kind = "FIREWORKS_KEY";
|
|
66
|
+
else if (token.startsWith("fpk_"))
|
|
67
|
+
kind = "FIREPASS_KEY";
|
|
68
|
+
else if (token.startsWith("fest_"))
|
|
69
|
+
kind = "FEST_IDENTITY_TOKEN";
|
|
70
|
+
return {
|
|
71
|
+
header,
|
|
72
|
+
scheme: bearer ? "Bearer" : "raw",
|
|
73
|
+
kind,
|
|
74
|
+
prefix: token.slice(0, Math.min(PREFIX_CHARS, token.length)),
|
|
75
|
+
length: token.length,
|
|
76
|
+
fingerprint: fingerprint(token),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** True when this credential is a developer's own subscription (pass-through path). */
|
|
80
|
+
export function isSubscriptionCredential(info) {
|
|
81
|
+
return info.kind === "ANTHROPIC_OAUTH_SUBSCRIPTION";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Last-resort log scrubber. Redaction that relies on programmer discipline
|
|
85
|
+
* fails, so every log line passes through this regardless of where it came from.
|
|
86
|
+
*/
|
|
87
|
+
const SECRET_PATTERN = /\b(?:sk-ant|sk|pk|ghp|gho|ghs|ghu|github_pat|xox[bpoars]|fw|fpk|fest)[-_][\w-]{8,}\b/gi;
|
|
88
|
+
export function redactSecrets(text) {
|
|
89
|
+
return text.replace(SECRET_PATTERN, "<redacted>");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Split request headers into credentials (fingerprinted) and the rest (kept).
|
|
93
|
+
*/
|
|
94
|
+
export function partitionHeaders(headers) {
|
|
95
|
+
const credentials = [];
|
|
96
|
+
const plain = {};
|
|
97
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
98
|
+
const lower = name.toLowerCase();
|
|
99
|
+
const joined = Array.isArray(value) ? value.join(", ") : (value ?? "");
|
|
100
|
+
if (SECRET_HEADERS.has(lower))
|
|
101
|
+
credentials.push(classifyCredential(lower, joined));
|
|
102
|
+
else
|
|
103
|
+
plain[lower] = joined;
|
|
104
|
+
}
|
|
105
|
+
return { credentials, plain };
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../server/secret/fingerprint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value that cannot be written down by accident.
|
|
3
|
+
*
|
|
4
|
+
* Fest holds provider credentials in memory on the substitute path. The risk is
|
|
5
|
+
* not that someone deliberately logs one — it is that a secret rides along
|
|
6
|
+
* inside a context object that someone later passes to `JSON.stringify`, a log
|
|
7
|
+
* line, a template string, or an error message. Every one of those is a
|
|
8
|
+
* reasonable thing to write, and every one of them leaks.
|
|
9
|
+
*
|
|
10
|
+
* So the value is wrapped in a type whose every stringification path returns
|
|
11
|
+
* `"[redacted]"`:
|
|
12
|
+
*
|
|
13
|
+
* `${secret}` -> "[redacted]" (toString / Symbol.toPrimitive)
|
|
14
|
+
* JSON.stringify({secret}) -> {"...":"[redacted]"}
|
|
15
|
+
* console.log(secret) -> NonPersistable [redacted] (inspect.custom)
|
|
16
|
+
* String(secret) -> "[redacted]"
|
|
17
|
+
*
|
|
18
|
+
* Reading the real value requires calling `.expose()`, which is greppable. That
|
|
19
|
+
* is the whole design: leaking becomes something you have to type on purpose,
|
|
20
|
+
* and a reviewer can find every place it happens with one search.
|
|
21
|
+
*
|
|
22
|
+
* This is a guard rail, not a boundary. It does not defend against code that
|
|
23
|
+
* WANTS the value — nothing in-process can. It defends against the accident.
|
|
24
|
+
*/
|
|
25
|
+
export const REDACTED = "[redacted]";
|
|
26
|
+
export class NonPersistable {
|
|
27
|
+
/**
|
|
28
|
+
* `#value` and not `_value`: a true private field is invisible to
|
|
29
|
+
* `Object.keys`, to spreads, and to `JSON.stringify` even without the
|
|
30
|
+
* `toJSON` below. Two independent mechanisms have to fail before a secret
|
|
31
|
+
* escapes.
|
|
32
|
+
*/
|
|
33
|
+
#value;
|
|
34
|
+
constructor(value) {
|
|
35
|
+
this.#value = value;
|
|
36
|
+
}
|
|
37
|
+
/** The real value. Greppable on purpose — every call site should be reviewable. */
|
|
38
|
+
expose() {
|
|
39
|
+
return this.#value;
|
|
40
|
+
}
|
|
41
|
+
toString() {
|
|
42
|
+
return REDACTED;
|
|
43
|
+
}
|
|
44
|
+
toJSON() {
|
|
45
|
+
return REDACTED;
|
|
46
|
+
}
|
|
47
|
+
/** Covers `${secret}` and `secret + ""`, which do not go through toString alone. */
|
|
48
|
+
[Symbol.toPrimitive]() {
|
|
49
|
+
return REDACTED;
|
|
50
|
+
}
|
|
51
|
+
get [Symbol.toStringTag]() {
|
|
52
|
+
return "NonPersistable";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `util.inspect` — which is what `console.log` and most loggers use — ignores
|
|
56
|
+
* `toString` entirely and would happily print the private field.
|
|
57
|
+
*
|
|
58
|
+
* The symbol is looked up by name rather than imported from `node:util` so
|
|
59
|
+
* this module stays dependency-free and usable anywhere.
|
|
60
|
+
*/
|
|
61
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
62
|
+
return `NonPersistable ${REDACTED}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** True for a wrapped value; narrows for callers that accept either. */
|
|
66
|
+
export function isNonPersistable(value) {
|
|
67
|
+
return value instanceof NonPersistable;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=non-persistable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"non-persistable.js","sourceRoot":"","sources":["../../../server/secret/non-persistable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,YAAY,CAAC;AAErC,MAAM,OAAO,cAAc;IACzB;;;;;OAKG;IACM,MAAM,CAAI;IAEnB,YAAY,KAAQ;QAClB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,mFAAmF;IACnF,MAAM;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,QAAQ;QACN,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM;QACJ,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,oFAAoF;IACpF,CAAC,MAAM,CAAC,WAAW,CAAC;QAClB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QACtB,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED;;;;;;OAMG;IACH,CAAC,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QACxC,OAAO,kBAAkB,QAAQ,EAAE,CAAC;IACtC,CAAC;CACF;AAED,wEAAwE;AACxE,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,KAAK,YAAY,cAAc,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit log: administrative actions, as a record rather than a debug log.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `log.ts` on purpose. Application logs rotate, are filtered by
|
|
5
|
+
* level, and are the first thing lost when a container is replaced — but "who
|
|
6
|
+
* minted this token, and when" is asked months later, often by someone who was
|
|
7
|
+
* not there. So it is a table, it is queried by the dashboard, and it survives
|
|
8
|
+
* exactly as long as the database does.
|
|
9
|
+
*
|
|
10
|
+
* What must never appear here: a credential, a prompt, a completion. `detail`
|
|
11
|
+
* is for identifiers and decisions. There is a test that greps the whole table
|
|
12
|
+
* for secret-shaped strings.
|
|
13
|
+
*/
|
|
14
|
+
export function recordAudit(store, entry) {
|
|
15
|
+
store.db
|
|
16
|
+
.prepare(`INSERT INTO audit_log
|
|
17
|
+
(org_id, at, actor_user_id, actor_label, action, target, outcome, detail_json, ip)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
19
|
+
.run(entry.orgId, entry.at ?? Date.now(), entry.actorUserId ?? null, entry.actorLabel.slice(0, 200), entry.action, entry.target ?? "", entry.outcome, JSON.stringify(entry.detail ?? {}), (entry.ip ?? "").slice(0, 64));
|
|
20
|
+
}
|
|
21
|
+
export function listAudit(store, orgId, opts = {}) {
|
|
22
|
+
const limit = Math.min(Math.max(opts.limit ?? 100, 1), 500);
|
|
23
|
+
const rows = store.db
|
|
24
|
+
.prepare(`SELECT seq, at, actor_user_id, actor_label, action, target, outcome, detail_json, ip
|
|
25
|
+
FROM audit_log
|
|
26
|
+
WHERE org_id = ? AND (? IS NULL OR seq < ?)
|
|
27
|
+
ORDER BY seq DESC
|
|
28
|
+
LIMIT ?`)
|
|
29
|
+
.all(orgId, opts.beforeSeq ?? null, opts.beforeSeq ?? null, limit);
|
|
30
|
+
return rows.map((r) => ({
|
|
31
|
+
seq: Number(r["seq"]),
|
|
32
|
+
at: Number(r["at"]),
|
|
33
|
+
actorUserId: r["actor_user_id"] === null ? null : String(r["actor_user_id"]),
|
|
34
|
+
actorLabel: String(r["actor_label"] ?? ""),
|
|
35
|
+
action: String(r["action"]),
|
|
36
|
+
target: String(r["target"] ?? ""),
|
|
37
|
+
outcome: String(r["outcome"]),
|
|
38
|
+
detail: parseDetail(r["detail_json"]),
|
|
39
|
+
ip: String(r["ip"] ?? ""),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
function parseDetail(raw) {
|
|
43
|
+
if (typeof raw !== "string")
|
|
44
|
+
return {};
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../server/store/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAgCH,MAAM,UAAU,WAAW,CAAC,KAAY,EAAE,KAAiB;IACzD,KAAK,CAAC,EAAE;SACL,OAAO,CACN;;0CAEoC,CACrC;SACA,GAAG,CACF,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,EACtB,KAAK,CAAC,WAAW,IAAI,IAAI,EACzB,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAC9B,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,MAAM,IAAI,EAAE,EAClB,KAAK,CAAC,OAAO,EACb,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,EAClC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAC9B,CAAC;AACN,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,KAAY,EACZ,KAAa,EACb,OAAuE,EAAE;IAEzE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,EAAE;SAClB,OAAO,CACN;;;;gBAIU,CACX;SACA,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE,KAAK,CAAmC,CAAC;IAEvG,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtB,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACrB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACnB,WAAW,EAAE,CAAC,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;QAC5E,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAiB;QAC7C,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;QACrC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;KAC1B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAE,MAAkC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|