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.
Files changed (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/dist/cli/claude.js +326 -0
  4. package/dist/cli/claude.js.map +1 -0
  5. package/dist/cli/config.js +59 -0
  6. package/dist/cli/config.js.map +1 -0
  7. package/dist/cli/login.js +130 -0
  8. package/dist/cli/login.js.map +1 -0
  9. package/dist/cli/logout.js +20 -0
  10. package/dist/cli/logout.js.map +1 -0
  11. package/dist/cli/whoami.js +34 -0
  12. package/dist/cli/whoami.js.map +1 -0
  13. package/dist/server/adapters/anthropic.js +74 -0
  14. package/dist/server/adapters/anthropic.js.map +1 -0
  15. package/dist/server/adapters/fireworks.js +54 -0
  16. package/dist/server/adapters/fireworks.js.map +1 -0
  17. package/dist/server/adapters/index.js +16 -0
  18. package/dist/server/adapters/index.js.map +1 -0
  19. package/dist/server/adapters/registry.js +38 -0
  20. package/dist/server/adapters/registry.js.map +1 -0
  21. package/dist/server/adapters/rewrite.js +152 -0
  22. package/dist/server/adapters/rewrite.js.map +1 -0
  23. package/dist/server/adapters/url.js +24 -0
  24. package/dist/server/adapters/url.js.map +1 -0
  25. package/dist/server/api/auth.js +213 -0
  26. package/dist/server/api/auth.js.map +1 -0
  27. package/dist/server/api/live.js +108 -0
  28. package/dist/server/api/live.js.map +1 -0
  29. package/dist/server/api/models.js +129 -0
  30. package/dist/server/api/models.js.map +1 -0
  31. package/dist/server/api/oauth.js +300 -0
  32. package/dist/server/api/oauth.js.map +1 -0
  33. package/dist/server/api/routes.js +164 -0
  34. package/dist/server/api/routes.js.map +1 -0
  35. package/dist/server/api/routing-view.js +47 -0
  36. package/dist/server/api/routing-view.js.map +1 -0
  37. package/dist/server/auth/accounts.js +111 -0
  38. package/dist/server/auth/accounts.js.map +1 -0
  39. package/dist/server/auth/gateway-401.js +85 -0
  40. package/dist/server/auth/gateway-401.js.map +1 -0
  41. package/dist/server/auth/guard.js +74 -0
  42. package/dist/server/auth/guard.js.map +1 -0
  43. package/dist/server/auth/oauth.js +43 -0
  44. package/dist/server/auth/oauth.js.map +1 -0
  45. package/dist/server/auth/password.js +104 -0
  46. package/dist/server/auth/password.js.map +1 -0
  47. package/dist/server/auth/posture.js +145 -0
  48. package/dist/server/auth/posture.js.map +1 -0
  49. package/dist/server/auth/session.js +161 -0
  50. package/dist/server/auth/session.js.map +1 -0
  51. package/dist/server/bin/fest.js +589 -0
  52. package/dist/server/bin/fest.js.map +1 -0
  53. package/dist/server/config.js +109 -0
  54. package/dist/server/config.js.map +1 -0
  55. package/dist/server/credentials/provider.js +70 -0
  56. package/dist/server/credentials/provider.js.map +1 -0
  57. package/dist/server/credentials/resolve.js +89 -0
  58. package/dist/server/credentials/resolve.js.map +1 -0
  59. package/dist/server/http/body.js +66 -0
  60. package/dist/server/http/body.js.map +1 -0
  61. package/dist/server/http/errors.js +144 -0
  62. package/dist/server/http/errors.js.map +1 -0
  63. package/dist/server/http/headers.js +189 -0
  64. package/dist/server/http/headers.js.map +1 -0
  65. package/dist/server/http/pipe.js +97 -0
  66. package/dist/server/http/pipe.js.map +1 -0
  67. package/dist/server/http/server.js +226 -0
  68. package/dist/server/http/server.js.map +1 -0
  69. package/dist/server/http/sse.js +167 -0
  70. package/dist/server/http/sse.js.map +1 -0
  71. package/dist/server/http/static.js +138 -0
  72. package/dist/server/http/static.js.map +1 -0
  73. package/dist/server/ingest/live-bus.js +67 -0
  74. package/dist/server/ingest/live-bus.js.map +1 -0
  75. package/dist/server/ingest/sink.js +141 -0
  76. package/dist/server/ingest/sink.js.map +1 -0
  77. package/dist/server/log.js +31 -0
  78. package/dist/server/log.js.map +1 -0
  79. package/dist/server/pipeline/count-tokens.js +48 -0
  80. package/dist/server/pipeline/count-tokens.js.map +1 -0
  81. package/dist/server/pipeline/dispatch.js +138 -0
  82. package/dist/server/pipeline/dispatch.js.map +1 -0
  83. package/dist/server/pipeline/passthrough.js +392 -0
  84. package/dist/server/pipeline/passthrough.js.map +1 -0
  85. package/dist/server/pipeline/preflight.js +66 -0
  86. package/dist/server/pipeline/preflight.js.map +1 -0
  87. package/dist/server/pipeline/record.js +49 -0
  88. package/dist/server/pipeline/record.js.map +1 -0
  89. package/dist/server/pipeline/substitute.js +287 -0
  90. package/dist/server/pipeline/substitute.js.map +1 -0
  91. package/dist/server/routes/resolve.js +176 -0
  92. package/dist/server/routes/resolve.js.map +1 -0
  93. package/dist/server/routes/table.js +247 -0
  94. package/dist/server/routes/table.js.map +1 -0
  95. package/dist/server/routes/watch.js +101 -0
  96. package/dist/server/routes/watch.js.map +1 -0
  97. package/dist/server/secret/fingerprint.js +107 -0
  98. package/dist/server/secret/fingerprint.js.map +1 -0
  99. package/dist/server/secret/non-persistable.js +69 -0
  100. package/dist/server/secret/non-persistable.js.map +1 -0
  101. package/dist/server/store/audit.js +53 -0
  102. package/dist/server/store/audit.js.map +1 -0
  103. package/dist/server/store/bootstrap.js +53 -0
  104. package/dist/server/store/bootstrap.js.map +1 -0
  105. package/dist/server/store/db.js +157 -0
  106. package/dist/server/store/db.js.map +1 -0
  107. package/dist/server/store/ids.js +14 -0
  108. package/dist/server/store/ids.js.map +1 -0
  109. package/dist/server/store/migrations/001-init.sql +213 -0
  110. package/dist/server/store/migrations/002-feed-and-error-indexes.sql +26 -0
  111. package/dist/server/store/migrations/003-routing.sql +42 -0
  112. package/dist/server/store/migrations/004-admin-auth.sql +72 -0
  113. package/dist/server/store/migrations/005-notional-cost.sql +28 -0
  114. package/dist/server/store/queries.js +904 -0
  115. package/dist/server/store/queries.js.map +1 -0
  116. package/dist/server/store/retention.js +79 -0
  117. package/dist/server/store/retention.js.map +1 -0
  118. package/dist/server/store/seed.js +203 -0
  119. package/dist/server/store/seed.js.map +1 -0
  120. package/dist/server/store/tokens.js +133 -0
  121. package/dist/server/store/tokens.js.map +1 -0
  122. package/dist/server/store/write.js +330 -0
  123. package/dist/server/store/write.js.map +1 -0
  124. package/dist/server/usage/accumulator.js +209 -0
  125. package/dist/server/usage/accumulator.js.map +1 -0
  126. package/dist/server/usage/cost.js +57 -0
  127. package/dist/server/usage/cost.js.map +1 -0
  128. package/dist/server/usage/prices/catalog.js +136 -0
  129. package/dist/server/usage/prices/catalog.js.map +1 -0
  130. package/dist/server/usage/prices/refresh.js +125 -0
  131. package/dist/server/usage/prices/refresh.js.map +1 -0
  132. package/dist/server/usage/prices/snapshot.json.gz +0 -0
  133. package/dist/server/usage/prices/table.js +102 -0
  134. package/dist/server/usage/prices/table.js.map +1 -0
  135. package/dist/server/usage/pricing.js +103 -0
  136. package/dist/server/usage/pricing.js.map +1 -0
  137. package/dist/shared/api.js +19 -0
  138. package/dist/shared/api.js.map +1 -0
  139. package/dist/shared/base-models.js +28 -0
  140. package/dist/shared/base-models.js.map +1 -0
  141. package/dist/shared/demotion-vars.js +17 -0
  142. package/dist/shared/demotion-vars.js.map +1 -0
  143. package/dist/shared/series.js +50 -0
  144. package/dist/shared/series.js.map +1 -0
  145. package/dist/shared/types.js +40 -0
  146. package/dist/shared/types.js.map +1 -0
  147. package/dist/web/dist/assets/index-8HlKsJqt.css +2 -0
  148. package/dist/web/dist/assets/index-DQ7fyPVm.js +56 -0
  149. package/dist/web/dist/assets/index-DQ7fyPVm.js.map +1 -0
  150. package/dist/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  151. package/dist/web/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  152. package/dist/web/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  153. package/dist/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  154. package/dist/web/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  155. package/dist/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  156. package/dist/web/dist/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  157. package/dist/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  158. package/dist/web/dist/index.html +44 -0
  159. package/dist/web/dist/nearform-icon.svg +4 -0
  160. package/package.json +82 -0
  161. package/routes.example.json +22 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Incremental server-sent-event parser for the proxy's observation path.
3
+ *
4
+ * This parser is a *passive observer*. Upstream bytes are written to the client
5
+ * before they are ever handed here, so nothing in this file may mutate a chunk,
6
+ * throw on malformed input, or retain more than the event currently being
7
+ * assembled. A metering bug must never be able to damage a proxied request.
8
+ *
9
+ * Two independent kinds of chunk boundary have to survive, and both have caused
10
+ * real flaky metering:
11
+ *
12
+ * 1. A boundary mid-line — `data: {"usa` / `ge":{...}}\n\n`. Handled by the
13
+ * `pending` line remainder.
14
+ * 2. A boundary mid-UTF-8-code-point — a 3-byte `→` arriving as 2 bytes then
15
+ * 1 byte. Handled by a *persistent* StringDecoder, which holds the partial
16
+ * code point across calls. Do not replace it with `Buffer.toString("utf8")`
17
+ * or `TextDecoder` without `{ stream: true }`: both would emit U+FFFD at
18
+ * the split and silently corrupt the JSON that follows.
19
+ */
20
+ import { StringDecoder } from "node:string_decoder";
21
+ import { USAGE_EVENT_TYPES } from "../../shared/types.js";
22
+ /**
23
+ * Cap on the data accumulated for a single event. A well-behaved Anthropic
24
+ * event is a few hundred bytes; anything past this is either a hostile upstream
25
+ * or a stream missing its blank-line separators. We drop the excess rather than
26
+ * throw, because losing usage for one request is strictly better than failing it.
27
+ */
28
+ const MAX_EVENT_DATA_CHARS = 1024 * 1024;
29
+ const DEFAULT_TYPE = "message";
30
+ export function createSseParser(opts) {
31
+ const interesting = opts?.interestingTypes ?? USAGE_EVENT_TYPES;
32
+ const decoder = new StringDecoder("utf8");
33
+ /** Tail of the last chunk that did not end on a newline. */
34
+ let pending = "";
35
+ // ── Current event state ──
36
+ let eventType = null;
37
+ let dataParts = [];
38
+ let dataChars = 0;
39
+ let sawAnyField = false;
40
+ /**
41
+ * Set once we know this event's type is uninteresting. From that point its
42
+ * `data:` lines are counted but never retained — content deltas are the
43
+ * overwhelming bulk of stream bytes, and joining them would make the observer
44
+ * cost scale with the response body rather than with the usage metadata.
45
+ *
46
+ * Note we cannot decide this before the `event:` line is seen, because an
47
+ * event with no `event:` field defaults to "message". Anthropic always sends
48
+ * `event:` first, so in practice the drop begins before any data is held.
49
+ */
50
+ let dropData = false;
51
+ function resetEvent() {
52
+ eventType = null;
53
+ dataParts = [];
54
+ dataChars = 0;
55
+ sawAnyField = false;
56
+ dropData = false;
57
+ }
58
+ function isInteresting(type) {
59
+ return interesting.has(type);
60
+ }
61
+ /** Emit the assembled event if it is one we care about, then reset. */
62
+ function dispatch(out) {
63
+ if (!sawAnyField) {
64
+ // A stray blank line between events, or leading whitespace. Not an event.
65
+ resetEvent();
66
+ return;
67
+ }
68
+ const type = eventType ?? DEFAULT_TYPE;
69
+ if (isInteresting(type))
70
+ out.push({ type, data: dataParts.join("\n") });
71
+ resetEvent();
72
+ }
73
+ function handleLine(line, out) {
74
+ if (line.length === 0) {
75
+ dispatch(out);
76
+ return;
77
+ }
78
+ // ":" prefix is an SSE comment (Anthropic sends none, but proxies inject
79
+ // heartbeat comments). Never treat it as a field.
80
+ if (line.charCodeAt(0) === 58 /* : */)
81
+ return;
82
+ const colon = line.indexOf(":");
83
+ const field = colon === -1 ? line : line.slice(0, colon);
84
+ let value = colon === -1 ? "" : line.slice(colon + 1);
85
+ // Exactly one optional leading space is part of the framing, not the value.
86
+ if (value.charCodeAt(0) === 32)
87
+ value = value.slice(1);
88
+ if (field === "event") {
89
+ sawAnyField = true;
90
+ eventType = value;
91
+ if (!isInteresting(value)) {
92
+ // Release anything buffered before the type was known.
93
+ dropData = true;
94
+ dataParts = [];
95
+ }
96
+ return;
97
+ }
98
+ if (field === "data") {
99
+ sawAnyField = true;
100
+ if (dropData)
101
+ return;
102
+ if (dataChars + value.length > MAX_EVENT_DATA_CHARS) {
103
+ // Past the cap we stop retaining. The event is still emitted, and the
104
+ // usage accumulator will reject the truncated JSON on its own.
105
+ dropData = true;
106
+ dataParts = [];
107
+ return;
108
+ }
109
+ dataChars += value.length;
110
+ dataParts.push(value);
111
+ return;
112
+ }
113
+ // `id:` and `retry:` are valid framing we have no use for. Marking the
114
+ // event as "seen" keeps an id-only event from leaking into the next one.
115
+ if (field === "id" || field === "retry") {
116
+ sawAnyField = true;
117
+ return;
118
+ }
119
+ // Unknown field: ignore per the SSE spec.
120
+ }
121
+ function consume(text, out) {
122
+ if (text.length === 0)
123
+ return;
124
+ let start = 0;
125
+ for (;;) {
126
+ const nl = text.indexOf("\n", start);
127
+ if (nl === -1)
128
+ break;
129
+ let line = pending.length > 0 ? pending + text.slice(start, nl) : text.slice(start, nl);
130
+ pending = "";
131
+ start = nl + 1;
132
+ // Accept CRLF as well as LF. The CR is stripped from the *assembled*
133
+ // line, not from the chunk, because a chunk boundary can fall between
134
+ // the CR and the LF — in which case the CR is the tail of `pending`.
135
+ if (line.charCodeAt(line.length - 1) === 13 /* \r */)
136
+ line = line.slice(0, -1);
137
+ handleLine(line, out);
138
+ }
139
+ if (start < text.length)
140
+ pending += text.slice(start);
141
+ }
142
+ return {
143
+ push(chunk) {
144
+ const out = [];
145
+ // Buffer.from here is a view-free copy of the caller's bytes; we never
146
+ // write through it, so the forwarded chunk cannot be disturbed.
147
+ consume(decoder.write(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)), out);
148
+ return out;
149
+ },
150
+ flush() {
151
+ const out = [];
152
+ // end() yields any bytes the decoder was holding. An incomplete code
153
+ // point at true end-of-stream becomes U+FFFD, which is correct: the
154
+ // stream really was truncated.
155
+ consume(decoder.end(), out);
156
+ if (pending.length > 0) {
157
+ const line = pending;
158
+ pending = "";
159
+ handleLine(line, out);
160
+ }
161
+ // A stream that ends without its final blank line still produced an event.
162
+ dispatch(out);
163
+ return out;
164
+ },
165
+ };
166
+ }
167
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../../../server/http/sse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Static hosting for the built dashboard (`web/dist`).
3
+ *
4
+ * Optional by design: if the bundle has not been built, this returns false for
5
+ * everything and the gateway runs headless. A proxy that refuses to start
6
+ * because a dashboard asset is missing would be a self-inflicted outage on the
7
+ * one path that must never go down.
8
+ *
9
+ * Path handling is the whole security surface here, so it is done once, at the
10
+ * top: resolve the candidate and require the result to still be inside the
11
+ * root. `..` in a URL, a percent-encoded `%2e%2e`, and an absolute path all
12
+ * collapse to the same check rather than three separate ones that can disagree.
13
+ *
14
+ * The SPA fallback deliberately does NOT catch everything. An unknown
15
+ * `/v1/whatever` must keep reaching the 404 in `server.ts`, because that warn
16
+ * line is how a new Claude Code endpoint gets noticed before users hit it —
17
+ * answering it with an HTML page would hide exactly the signal we built it for.
18
+ */
19
+ import { createReadStream, readFileSync, statSync } from "node:fs";
20
+ import { extname, join, normalize, resolve, sep } from "node:path";
21
+ const TYPES = {
22
+ ".html": "text/html; charset=utf-8",
23
+ ".js": "text/javascript; charset=utf-8",
24
+ ".css": "text/css; charset=utf-8",
25
+ ".json": "application/json; charset=utf-8",
26
+ ".svg": "image/svg+xml",
27
+ ".png": "image/png",
28
+ ".ico": "image/x-icon",
29
+ ".woff2": "font/woff2",
30
+ ".map": "application/json; charset=utf-8",
31
+ };
32
+ function fileSize(path) {
33
+ try {
34
+ const st = statSync(path);
35
+ return st.isFile() ? st.size : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * Point the document's `<base href>` at wherever Fest is actually mounted.
43
+ *
44
+ * The built bundle is emitted with `base: "./"`, so its asset URLs are already
45
+ * relative; what they resolve *against* is this tag. Rewriting it here rather
46
+ * than at build time is what keeps one artifact servable from `/` and from
47
+ * `/fest` — including when a reader arrives at `/fest` with no trailing slash,
48
+ * where the document's own URL would otherwise resolve `./assets/…` against
49
+ * the origin root and 404.
50
+ */
51
+ export function withBaseHref(html, basePath) {
52
+ const href = `${basePath}/`;
53
+ const tag = `<base href="${href}" />`;
54
+ // Test for the tag rather than comparing before/after: rewriting a document
55
+ // that already carries the right href is a no-op, and reading that as "no tag
56
+ // found" would append a second one on every serve.
57
+ if (/<base\b[^>]*>/i.test(html))
58
+ return html.replace(/<base\b[^>]*>/i, tag);
59
+ // No tag to rewrite (a hand-edited or third-party index.html). Insert one,
60
+ // since a sub-path deployment is broken without it.
61
+ return html.replace(/<head\b[^>]*>/i, (head) => `${head}\n ${tag}`);
62
+ }
63
+ export function createStaticHost(rootDir, basePath = "") {
64
+ const root = resolve(rootDir);
65
+ const index = join(root, "index.html");
66
+ const available = fileSize(index) !== null;
67
+ /**
68
+ * Read at request time, not cached at boot: `npm run dev` rebuilds the bundle
69
+ * under a running server, and a cached copy would serve the previous deploy's
70
+ * HTML until restart. It is ~1.5KB, and only the document hits this path.
71
+ */
72
+ function sendIndex(res) {
73
+ let html;
74
+ try {
75
+ html = readFileSync(index, "utf8");
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ const body = Buffer.from(withBaseHref(html, basePath), "utf8");
81
+ res.writeHead(200, {
82
+ "content-type": TYPES[".html"] ?? "text/html; charset=utf-8",
83
+ "content-length": String(body.byteLength),
84
+ "cache-control": "no-cache",
85
+ });
86
+ res.end(body);
87
+ return true;
88
+ }
89
+ function send(res, file, size, immutable) {
90
+ const ext = extname(file).toLowerCase();
91
+ res.writeHead(200, {
92
+ "content-type": TYPES[ext] ?? "application/octet-stream",
93
+ "content-length": String(size),
94
+ // Vite fingerprints everything under /assets/, so those are safe to cache
95
+ // forever. index.html must never be, or a deploy is invisible until the
96
+ // browser feels like revalidating.
97
+ "cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
98
+ });
99
+ createReadStream(file).pipe(res);
100
+ }
101
+ return {
102
+ available,
103
+ serve(res, path) {
104
+ if (!available)
105
+ return false;
106
+ let decoded;
107
+ try {
108
+ decoded = decodeURIComponent(path);
109
+ }
110
+ catch {
111
+ // A malformed escape is not a file request; let it 404 normally.
112
+ return false;
113
+ }
114
+ const candidate = resolve(join(root, normalize(decoded)));
115
+ // `startsWith(root + sep)` and not `startsWith(root)`: the latter would
116
+ // also accept a sibling directory named `dist-backup`.
117
+ const inRoot = candidate === root || candidate.startsWith(root + sep);
118
+ if (inRoot) {
119
+ // index.html goes through sendIndex wherever it is asked for, so a
120
+ // direct request for it cannot bypass the <base href> rewrite that the
121
+ // fallback below applies.
122
+ if (candidate === index)
123
+ return sendIndex(res);
124
+ const size = fileSize(candidate);
125
+ if (size !== null) {
126
+ send(res, candidate, size, candidate.startsWith(join(root, "assets") + sep));
127
+ return true;
128
+ }
129
+ }
130
+ // SPA fallback: extensionless, non-API, non-proxy paths are client routes.
131
+ if (extname(decoded) === "" && !decoded.startsWith("/v1/") && !decoded.startsWith("/api/")) {
132
+ return sendIndex(res);
133
+ }
134
+ return false;
135
+ },
136
+ };
137
+ }
138
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../../../server/http/static.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * In-process fan-out of usage records to connected dashboards.
3
+ *
4
+ * Deliberately fed from the sink's FLUSH, not from `record()`: the request path
5
+ * stays allocation-only, and a dashboard that is connected must never be able
6
+ * to add work to a developer's in-flight stream. The cost is that "live" means
7
+ * "within one flush interval" (250ms), which is indistinguishable from instant
8
+ * to a human watching a feed.
9
+ *
10
+ * A subscriber that cannot keep up is DROPPED FROM, not buffered for. Growing a
11
+ * buffer for a stalled browser tab would trade a developer's memory for a
12
+ * dashboard's completeness; the feed is a tail, and the paged `/api/requests`
13
+ * view is the authoritative history. Drops are counted so the gap is visible
14
+ * rather than silent — the same rule the sink's queue follows.
15
+ */
16
+ export function createLiveBus() {
17
+ const subscribers = new Set();
18
+ const closers = new Map();
19
+ let published = 0;
20
+ let dropped = 0;
21
+ return {
22
+ publish(records) {
23
+ if (records.length === 0 || subscribers.size === 0)
24
+ return;
25
+ published += records.length;
26
+ for (const fn of subscribers) {
27
+ let accepted = false;
28
+ try {
29
+ accepted = fn(records);
30
+ }
31
+ catch {
32
+ // A throwing subscriber is a broken socket, not our problem to retry.
33
+ // Unsubscribe it so one bad tab cannot be tried again on every flush.
34
+ subscribers.delete(fn);
35
+ continue;
36
+ }
37
+ if (!accepted)
38
+ dropped += records.length;
39
+ }
40
+ },
41
+ subscribe(fn, onClose) {
42
+ subscribers.add(fn);
43
+ if (onClose !== undefined)
44
+ closers.set(fn, onClose);
45
+ return () => {
46
+ subscribers.delete(fn);
47
+ closers.delete(fn);
48
+ };
49
+ },
50
+ closeAll() {
51
+ for (const close of [...closers.values()]) {
52
+ try {
53
+ close();
54
+ }
55
+ catch {
56
+ // A socket that is already gone is the outcome we wanted anyway.
57
+ }
58
+ }
59
+ subscribers.clear();
60
+ closers.clear();
61
+ },
62
+ stats() {
63
+ return { subscribers: subscribers.size, published, dropped };
64
+ },
65
+ };
66
+ }
67
+ //# sourceMappingURL=live-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-bus.js","sourceRoot":"","sources":["../../../server/ingest/live-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAgCH,MAAM,UAAU,aAAa;IAC3B,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8B,CAAC;IACtD,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,OAAO;QACL,OAAO,CAAC,OAA+B;YACrC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,IAAI,KAAK,CAAC;gBAAE,OAAO;YAC3D,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;YAC5B,KAAK,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;gBAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;gBACrB,IAAI,CAAC;oBACH,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;gBACzB,CAAC;gBAAC,MAAM,CAAC;oBACP,sEAAsE;oBACtE,sEAAsE;oBACtE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;oBACvB,SAAS;gBACX,CAAC;gBACD,IAAI,CAAC,QAAQ;oBAAE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;YAC3C,CAAC;QACH,CAAC;QAED,SAAS,CAAC,EAAkB,EAAE,OAAoB;YAChD,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpB,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YACpD,OAAO,GAAG,EAAE;gBACV,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACvB,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC,CAAC;QACJ,CAAC;QAED,QAAQ;YACN,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;gBAC1C,IAAI,CAAC;oBACH,KAAK,EAAE,CAAC;gBACV,CAAC;gBAAC,MAAM,CAAC;oBACP,iEAAiE;gBACnE,CAAC;YACH,CAAC;YACD,WAAW,CAAC,KAAK,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;QAED,KAAK;YACH,OAAO,EAAE,WAAW,EAAE,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;QAC/D,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Usage record sink: bounded queue in front of batched appends.
3
+ *
4
+ * `record()` is called from the request path, so it is allocation-only: push
5
+ * onto an array and return. No I/O, no JSON, no pricing, no awaiting. Pricing
6
+ * happens in the flush, where a slow lookup cannot add latency to a stream.
7
+ *
8
+ * On overload the queue drops the OLDEST record and increments a counter. That
9
+ * is deliberate: we degrade the metric rather than the product. The counter is
10
+ * exposed so the loss can never be silent — a dashboard that quietly
11
+ * under-reports is worse than one that admits it dropped 4,102 records.
12
+ *
13
+ * Phase 1 writes JSONL. The queue/flush seam is where SQLite lands in Phase 2
14
+ * without the request path changing.
15
+ */
16
+ import { appendFile } from "node:fs/promises";
17
+ import { log } from "../log.js";
18
+ export function createUsageSink(opts) {
19
+ const flushMs = opts.flushMs ?? 250;
20
+ const maxQueue = opts.maxQueue ?? 10_000;
21
+ const maxBatch = opts.maxBatch ?? 200;
22
+ const ringSize = opts.ringSize ?? 500;
23
+ let queue = [];
24
+ const ring = [];
25
+ let written = 0;
26
+ let dropped = 0;
27
+ let writeErrors = 0;
28
+ let trailErrors = 0;
29
+ let lastFlushMs = null;
30
+ /**
31
+ * The in-flight flush, if any. Held as a promise rather than a boolean so
32
+ * that `await flush()` awaits the *actual work* even when a fire-and-forget
33
+ * flush is already running. With a boolean guard, `close()` could return
34
+ * while a flush was mid-append and silently drop whatever was still queued.
35
+ */
36
+ let inFlight = null;
37
+ let closed = false;
38
+ let timer = setInterval(() => {
39
+ void flush();
40
+ }, flushMs);
41
+ // Never hold the process open just to flush metrics.
42
+ timer.unref();
43
+ function flush() {
44
+ // Overlapping flushes would interleave appends, so callers join the
45
+ // in-flight one instead of starting a second.
46
+ if (inFlight)
47
+ return inFlight;
48
+ if (queue.length === 0)
49
+ return Promise.resolve();
50
+ inFlight = doFlush().finally(() => {
51
+ inFlight = null;
52
+ });
53
+ return inFlight;
54
+ }
55
+ async function doFlush() {
56
+ const startedAt = Date.now();
57
+ const batch = queue.splice(0, maxBatch);
58
+ // The store is the system of record, so it goes first and its outcome
59
+ // decides whether the batch is safe to let go of.
60
+ let persisted = true;
61
+ if (opts.onBatch !== undefined) {
62
+ try {
63
+ opts.onBatch(batch);
64
+ }
65
+ catch (err) {
66
+ persisted = false;
67
+ writeErrors += 1;
68
+ log.warn("usage store write failed", { error: String(err).slice(0, 200), writeErrors });
69
+ }
70
+ }
71
+ // The JSONL file is a convenience trail, not the record. Its failure is
72
+ // logged and counted but never requeues the batch: retrying purely for the
73
+ // trail's sake would re-run onBatch and duplicate rows in the store.
74
+ try {
75
+ const payload = batch.map((r) => JSON.stringify(r)).join("\n") + "\n";
76
+ await appendFile(opts.path, payload, "utf8");
77
+ }
78
+ catch (err) {
79
+ trailErrors += 1;
80
+ log.warn("usage trail write failed", { error: String(err).slice(0, 200), trailErrors });
81
+ }
82
+ if (persisted) {
83
+ written += batch.length;
84
+ }
85
+ else if (queue.length + batch.length <= maxQueue) {
86
+ // Requeue once at the front. Beyond that, drop rather than grow without
87
+ // bound against a persistently failing store.
88
+ queue = [...batch, ...queue];
89
+ }
90
+ else {
91
+ dropped += batch.length;
92
+ }
93
+ lastFlushMs = Date.now() - startedAt;
94
+ }
95
+ return {
96
+ record(rec) {
97
+ if (closed)
98
+ return;
99
+ if (queue.length >= maxQueue) {
100
+ queue.shift();
101
+ dropped += 1;
102
+ }
103
+ queue.push(rec);
104
+ ring.push(rec);
105
+ if (ring.length > ringSize)
106
+ ring.shift();
107
+ // Size trigger, so a burst is not held for the full timer interval.
108
+ if (queue.length >= maxBatch)
109
+ void flush();
110
+ },
111
+ flush,
112
+ async close() {
113
+ closed = true;
114
+ if (timer) {
115
+ clearInterval(timer);
116
+ timer = null;
117
+ }
118
+ // Let any fire-and-forget flush settle first. Joining it would make no
119
+ // progress on our own backlog, because it has already taken its batch —
120
+ // which would make the no-progress check below break out immediately and
121
+ // silently discard the remainder.
122
+ if (inFlight)
123
+ await inFlight;
124
+ // Now drain in fresh batches. A batch that fails to shrink the queue
125
+ // means the disk is not accepting writes; stop rather than spin forever.
126
+ while (queue.length > 0) {
127
+ const before = queue.length;
128
+ await flush();
129
+ if (queue.length >= before)
130
+ break;
131
+ }
132
+ },
133
+ stats() {
134
+ return { queued: queue.length, written, dropped, writeErrors, trailErrors, lastFlushMs };
135
+ },
136
+ recent(limit = 100) {
137
+ return ring.slice(-limit).reverse();
138
+ },
139
+ };
140
+ }
141
+ //# sourceMappingURL=sink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink.js","sourceRoot":"","sources":["../../../server/ingest/sink.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Structured logging with mandatory redaction.
3
+ *
4
+ * Every line passes through `redactSecrets` regardless of origin. Redaction
5
+ * that depends on each call site remembering to sanitise its own input fails
6
+ * eventually, and in Fest the thing that leaks would be a developer's live
7
+ * subscription credential. So the scrubber is applied here, once, to the
8
+ * serialised output — belt and braces over the structured fingerprinting that
9
+ * should already have happened upstream.
10
+ */
11
+ import { redactSecrets } from "./secret/fingerprint.js";
12
+ const LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
13
+ let threshold = LEVEL_ORDER.info;
14
+ export function setLogLevel(level) {
15
+ threshold = LEVEL_ORDER[level];
16
+ }
17
+ function emit(level, msg, fields) {
18
+ if (LEVEL_ORDER[level] < threshold)
19
+ return;
20
+ const line = JSON.stringify({ at: new Date().toISOString(), level, msg, ...fields });
21
+ const safe = redactSecrets(line);
22
+ // Logs go to stderr so stdout stays free for machine-readable output.
23
+ process.stderr.write(safe + "\n");
24
+ }
25
+ export const log = {
26
+ debug: (msg, fields) => emit("debug", msg, fields),
27
+ info: (msg, fields) => emit("info", msg, fields),
28
+ warn: (msg, fields) => emit("warn", msg, fields),
29
+ error: (msg, fields) => emit("error", msg, fields),
30
+ };
31
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../server/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAIxD,MAAM,WAAW,GAA6B,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAE3F,IAAI,SAAS,GAAW,WAAW,CAAC,IAAI,CAAC;AAEzC,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,IAAI,CAAC,KAAe,EAAE,GAAW,EAAE,MAAgC;IAC1E,IAAI,WAAW,CAAC,KAAK,CAAC,GAAG,SAAS;QAAE,OAAO;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IACrF,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,sEAAsE;IACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,KAAK,EAAE,CAAC,GAAW,EAAE,MAAgC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC;IACpF,IAAI,EAAE,CAAC,GAAW,EAAE,MAAgC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC;IAClF,IAAI,EAAE,CAAC,GAAW,EAAE,MAAgC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC;IAClF,KAAK,EAAE,CAAC,GAAW,EAAE,MAAgC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC;CACrF,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `POST /v1/messages/count_tokens`.
3
+ *
4
+ * Relayed, but deliberately NOT metered. It consumes no tokens and produces no
5
+ * usage, so recording it would inflate request counts and drag every per-request
6
+ * average toward zero — a dashboard that counts questions about work as work.
7
+ *
8
+ * It is relayed rather than answered locally because only the provider knows its
9
+ * own tokenizer. A plausible local estimate would be worse than no answer: it
10
+ * feeds Claude Code's context-window accounting, and a wrong number there causes
11
+ * either premature compaction or an overflow at the worst moment.
12
+ */
13
+ import { anthropicError } from "../http/errors.js";
14
+ import { readBodyBytes, isTooLarge } from "../http/body.js";
15
+ import { buildUpstreamHeaders } from "../http/headers.js";
16
+ import { log } from "../log.js";
17
+ const MAX_BODY_BYTES = 8 * 1024 * 1024;
18
+ export async function handleCountTokens(req, res, opts) {
19
+ const body = await readBodyBytes(req, MAX_BODY_BYTES);
20
+ if (isTooLarge(body)) {
21
+ res.writeHead(400, { "content-type": "application/json" });
22
+ res.end(anthropicError("invalid_request_error", "Fest: count_tokens body too large."));
23
+ return;
24
+ }
25
+ const target = new URL(opts.path, opts.upstreamBaseUrl);
26
+ try {
27
+ const upstream = await fetch(target, {
28
+ method: "POST",
29
+ headers: buildUpstreamHeaders(req.headers, {
30
+ posture: opts.posture,
31
+ upstreamHost: target.host,
32
+ }),
33
+ body,
34
+ redirect: "error",
35
+ });
36
+ const text = await upstream.text();
37
+ res.writeHead(upstream.status, { "content-type": "application/json" });
38
+ res.end(text);
39
+ }
40
+ catch (err) {
41
+ log.warn("count_tokens relay failed", { error: String(err).slice(0, 200) });
42
+ // Claude Code treats a failure here as "estimate unavailable" and carries
43
+ // on, so failing cleanly is better than failing loudly.
44
+ res.writeHead(502, { "content-type": "application/json" });
45
+ res.end(anthropicError("api_error", "Fest: could not reach upstream for count_tokens."));
46
+ }
47
+ }
48
+ //# sourceMappingURL=count-tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"count-tokens.js","sourceRoot":"","sources":["../../../server/pipeline/count-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhC,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAoB,EACpB,GAAmB,EACnB,IAAiE;IAEjE,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACtD,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC3D,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,uBAAuB,EAAE,oCAAoC,CAAC,CAAC,CAAC;QACvF,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,EAAE;YACnC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,oBAAoB,CAAC,GAAG,CAAC,OAAO,EAAE;gBACzC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,YAAY,EAAE,MAAM,CAAC,IAAI;aAC1B,CAAC;YACF,IAAI;YACJ,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;QACvE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5E,0EAA0E;QAC1E,wDAAwD;QACxD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC3D,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,EAAE,kDAAkD,CAAC,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC"}