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,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header translation between Claude Code and Anthropic.
|
|
3
|
+
*
|
|
4
|
+
* This module is where a subscription pass-through is most easily broken, and
|
|
5
|
+
* the failures are silent: Anthropic validates an OAuth token against the shape
|
|
6
|
+
* of the request that carries it, and Claude Code reports "empty or malformed
|
|
7
|
+
* response" rather than anything actionable. So the rules here are hard
|
|
8
|
+
* requirements, not style preferences, and each one has a comment saying which
|
|
9
|
+
* observed failure it prevents.
|
|
10
|
+
*/
|
|
11
|
+
import { HOP_BY_HOP, SECRET_HEADERS } from "../secret/fingerprint.js";
|
|
12
|
+
/** Default when the client omits it entirely. Never an override. */
|
|
13
|
+
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
14
|
+
/**
|
|
15
|
+
* Headers Fest must never relay upstream regardless of posture.
|
|
16
|
+
*
|
|
17
|
+
* `content-length` is the caller's to set — it rewrites the body framing and a
|
|
18
|
+
* stale value from the inbound request truncates or hangs the upstream POST.
|
|
19
|
+
* `transfer-encoding` is hop-by-hop and undici sets its own.
|
|
20
|
+
*/
|
|
21
|
+
const NEVER_UPSTREAM = new Set(["content-length", "transfer-encoding"]);
|
|
22
|
+
/**
|
|
23
|
+
* Build the upstream request headers.
|
|
24
|
+
*
|
|
25
|
+
* ON `posture === "subscription"` EVERYTHING NOT EXPLICITLY DROPPED IS FORWARDED
|
|
26
|
+
* BYTE-FOR-BYTE. Do not "tidy" this:
|
|
27
|
+
*
|
|
28
|
+
* - Do NOT filter, dedupe, sort or canonicalise `anthropic-beta`. Claude Code
|
|
29
|
+
* sends 15 comma-separated values (including `oauth-2025-04-20`) and the set
|
|
30
|
+
* is part of what the OAuth token is validated against.
|
|
31
|
+
* - Do NOT rewrite `user-agent` (`claude-cli/2.1.278 (external, sdk-cli)`) or
|
|
32
|
+
* `x-app`, and do NOT drop the `x-stainless-*` telemetry.
|
|
33
|
+
* - Do NOT inject attribution or telemetry headers of our own — no `x-title`,
|
|
34
|
+
* no `http-referer`, no `x-fest-*`. Those are OpenRouter-style conventions
|
|
35
|
+
* and Anthropic rejects the request shape. A future reader will be tempted to
|
|
36
|
+
* add them "so usage is attributable"; attribution belongs in Fest's own
|
|
37
|
+
* usage records, not on the wire.
|
|
38
|
+
*
|
|
39
|
+
* `posture` is taken rather than assumed because the key posture is where model
|
|
40
|
+
* routing and rewriting will later be permitted; today both postures forward
|
|
41
|
+
* the same way, which keeps the subscription guarantee the default.
|
|
42
|
+
*/
|
|
43
|
+
export function buildUpstreamHeaders(inbound, opts) {
|
|
44
|
+
const out = new Headers();
|
|
45
|
+
let sawAnthropicVersion = false;
|
|
46
|
+
for (const [name, value] of Object.entries(inbound)) {
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
if (HOP_BY_HOP.has(lower))
|
|
51
|
+
continue;
|
|
52
|
+
if (NEVER_UPSTREAM.has(lower))
|
|
53
|
+
continue;
|
|
54
|
+
// Fest's identity carriers are ours alone; Anthropic must never see them.
|
|
55
|
+
if (lower.startsWith("x-fest-"))
|
|
56
|
+
continue;
|
|
57
|
+
const values = Array.isArray(value) ? value : [value];
|
|
58
|
+
for (const one of values)
|
|
59
|
+
out.append(lower, one);
|
|
60
|
+
if (lower === "anthropic-version")
|
|
61
|
+
sawAnthropicVersion = true;
|
|
62
|
+
}
|
|
63
|
+
// `host` is hop-by-hop above, so it is re-set here for the real upstream.
|
|
64
|
+
out.set("host", opts.upstreamHost);
|
|
65
|
+
if (!sawAnthropicVersion)
|
|
66
|
+
out.set("anthropic-version", DEFAULT_ANTHROPIC_VERSION);
|
|
67
|
+
// Metering tees the SSE stream to count tokens; a gzip/zstd-framed body would
|
|
68
|
+
// have to be inflated first, which means buffering a stream whose whole point
|
|
69
|
+
// is not being buffered. Verified against production: asking for `identity`
|
|
70
|
+
// does not change upstream acceptance of a subscription request.
|
|
71
|
+
if (opts.forceIdentityEncoding !== false)
|
|
72
|
+
out.set("accept-encoding", "identity");
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Headers worth relaying back to Claude Code.
|
|
77
|
+
*
|
|
78
|
+
* An allowlist, not a denylist: anything new that appears upstream is more
|
|
79
|
+
* likely to confuse the client (or leak) than to help, and the client reads
|
|
80
|
+
* only these.
|
|
81
|
+
*/
|
|
82
|
+
const DOWNSTREAM_ALLOW = new Set([
|
|
83
|
+
"content-type",
|
|
84
|
+
"request-id",
|
|
85
|
+
"anthropic-request-id",
|
|
86
|
+
"retry-after",
|
|
87
|
+
]);
|
|
88
|
+
export function buildDownstreamHeaders(upstream, opts) {
|
|
89
|
+
const out = {};
|
|
90
|
+
upstream.forEach((value, name) => {
|
|
91
|
+
const lower = name.toLowerCase();
|
|
92
|
+
// `set-cookie` and friends are in SECRET_HEADERS; never echo a credential.
|
|
93
|
+
if (SECRET_HEADERS.has(lower))
|
|
94
|
+
return;
|
|
95
|
+
// We decode/re-frame the body (and tee it), so the upstream's encoding and
|
|
96
|
+
// length no longer describe what the client will actually receive.
|
|
97
|
+
if (lower === "content-encoding" || lower === "content-length")
|
|
98
|
+
return;
|
|
99
|
+
if (HOP_BY_HOP.has(lower))
|
|
100
|
+
return;
|
|
101
|
+
// The quota headers are not diagnostics: Claude Code's own `/status` reads
|
|
102
|
+
// them to show the developer their remaining 5h/7d window. Dropping them
|
|
103
|
+
// makes Fest look like it broke the client's quota display.
|
|
104
|
+
if (lower.startsWith("anthropic-ratelimit-")) {
|
|
105
|
+
out[lower] = value;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (DOWNSTREAM_ALLOW.has(lower))
|
|
109
|
+
out[lower] = value;
|
|
110
|
+
});
|
|
111
|
+
// A non-streaming reply served as `text/plain` makes Claude Code report
|
|
112
|
+
// "empty or malformed response" — it content-type-switches before parsing.
|
|
113
|
+
if (out["content-type"] === undefined)
|
|
114
|
+
out["content-type"] = "application/json";
|
|
115
|
+
if (opts.stream) {
|
|
116
|
+
// Any intermediary that buffers turns a token-by-token stream into one
|
|
117
|
+
// late blob. `no-transform` stops proxies re-compressing; `x-accel-buffering`
|
|
118
|
+
// is nginx's opt-out and is harmless elsewhere.
|
|
119
|
+
out["cache-control"] = "no-cache, no-transform";
|
|
120
|
+
out["x-accel-buffering"] = "no";
|
|
121
|
+
// Length is unknowable for a stream, and setting it truncates the response.
|
|
122
|
+
delete out["content-length"];
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Coerce defensively: a malformed upstream value must omit the field, not poison it. */
|
|
127
|
+
function num(raw) {
|
|
128
|
+
if (raw === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
const trimmed = raw.trim();
|
|
131
|
+
if (trimmed === "")
|
|
132
|
+
return undefined;
|
|
133
|
+
const value = Number(trimmed);
|
|
134
|
+
return Number.isFinite(value) ? value : undefined;
|
|
135
|
+
}
|
|
136
|
+
function str(raw) {
|
|
137
|
+
if (raw === null)
|
|
138
|
+
return undefined;
|
|
139
|
+
const trimmed = raw.trim();
|
|
140
|
+
return trimmed === "" ? undefined : trimmed;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read Anthropic's unified quota headers into a snapshot.
|
|
144
|
+
*
|
|
145
|
+
* Returns `null` when none are present, so callers can distinguish "upstream
|
|
146
|
+
* said nothing about quota" from "quota is at zero". `exactOptionalPropertyTypes`
|
|
147
|
+
* is on, so absent fields are omitted rather than set to `undefined`.
|
|
148
|
+
*/
|
|
149
|
+
export function parseRateLimit(upstream) {
|
|
150
|
+
const status = str(upstream.get("anthropic-ratelimit-unified-status"));
|
|
151
|
+
const fiveHourUtilization = num(upstream.get("anthropic-ratelimit-unified-5h-utilization"));
|
|
152
|
+
const fiveHourStatus = str(upstream.get("anthropic-ratelimit-unified-5h-status"));
|
|
153
|
+
const fiveHourResetAt = num(upstream.get("anthropic-ratelimit-unified-5h-reset"));
|
|
154
|
+
const sevenDayUtilization = num(upstream.get("anthropic-ratelimit-unified-7d-utilization"));
|
|
155
|
+
const sevenDayStatus = str(upstream.get("anthropic-ratelimit-unified-7d-status"));
|
|
156
|
+
const sevenDayResetAt = num(upstream.get("anthropic-ratelimit-unified-7d-reset"));
|
|
157
|
+
const representativeClaim = str(upstream.get("anthropic-ratelimit-unified-representative-claim"));
|
|
158
|
+
const overageStatus = str(upstream.get("anthropic-ratelimit-unified-overage-status"));
|
|
159
|
+
const overageDisabledReason = str(upstream.get("anthropic-ratelimit-unified-overage-disabled-reason"));
|
|
160
|
+
// Mutable mirror of the readonly snapshot, so fields can be omitted rather
|
|
161
|
+
// than assigned `undefined` (exactOptionalPropertyTypes).
|
|
162
|
+
const snapshot = {};
|
|
163
|
+
if (status !== undefined)
|
|
164
|
+
snapshot.status = status;
|
|
165
|
+
if (fiveHourUtilization !== undefined)
|
|
166
|
+
snapshot.fiveHourUtilization = fiveHourUtilization;
|
|
167
|
+
if (fiveHourStatus !== undefined)
|
|
168
|
+
snapshot.fiveHourStatus = fiveHourStatus;
|
|
169
|
+
if (fiveHourResetAt !== undefined)
|
|
170
|
+
snapshot.fiveHourResetAt = fiveHourResetAt;
|
|
171
|
+
if (sevenDayUtilization !== undefined)
|
|
172
|
+
snapshot.sevenDayUtilization = sevenDayUtilization;
|
|
173
|
+
if (sevenDayStatus !== undefined)
|
|
174
|
+
snapshot.sevenDayStatus = sevenDayStatus;
|
|
175
|
+
if (sevenDayResetAt !== undefined)
|
|
176
|
+
snapshot.sevenDayResetAt = sevenDayResetAt;
|
|
177
|
+
if (representativeClaim !== undefined)
|
|
178
|
+
snapshot.representativeClaim = representativeClaim;
|
|
179
|
+
if (overageStatus !== undefined)
|
|
180
|
+
snapshot.overageStatus = overageStatus;
|
|
181
|
+
if (overageDisabledReason !== undefined) {
|
|
182
|
+
snapshot.overageDisabledReason = overageDisabledReason;
|
|
183
|
+
}
|
|
184
|
+
// Nothing recognised — including the case where every value was garbage.
|
|
185
|
+
if (Object.keys(snapshot).length === 0)
|
|
186
|
+
return null;
|
|
187
|
+
return snapshot;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../../server/http/headers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming relay with a read-only observation tee.
|
|
3
|
+
*
|
|
4
|
+
* The ordering here is the whole point: bytes go to the client FIRST, then the
|
|
5
|
+
* same buffer is handed to the observer. Metering must never sit between the
|
|
6
|
+
* upstream and the developer's terminal, because first-token latency is the
|
|
7
|
+
* thing people feel.
|
|
8
|
+
*
|
|
9
|
+
* Correspondingly, the observer is never allowed to break a request. It runs
|
|
10
|
+
* inside a try/catch and its failures are counted, not thrown: an accounting
|
|
11
|
+
* record is worth less than a developer's in-flight session.
|
|
12
|
+
*/
|
|
13
|
+
import { once } from "node:events";
|
|
14
|
+
/**
|
|
15
|
+
* Relay an upstream body to the client, teeing each chunk to `observe`.
|
|
16
|
+
*
|
|
17
|
+
* Backpressure is honoured: if the socket buffer is full we await `drain`
|
|
18
|
+
* rather than queueing indefinitely, otherwise one slow client becomes
|
|
19
|
+
* unbounded memory growth in a shared gateway.
|
|
20
|
+
*/
|
|
21
|
+
export async function pipeWithTee(body, res, opts) {
|
|
22
|
+
const startedAt = opts.startedAt;
|
|
23
|
+
let bytesOut = 0;
|
|
24
|
+
let ttfbMs = null;
|
|
25
|
+
let observerErrors = 0;
|
|
26
|
+
let clientAborted = false;
|
|
27
|
+
let completed = false;
|
|
28
|
+
// A client that hangs up must not leave the upstream request running: on a
|
|
29
|
+
// subscription that would keep consuming the developer's own quota for output
|
|
30
|
+
// nobody will ever read.
|
|
31
|
+
const onClose = () => {
|
|
32
|
+
if (!completed) {
|
|
33
|
+
clientAborted = true;
|
|
34
|
+
opts.abort.abort();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
res.on("close", onClose);
|
|
38
|
+
// The explicit reader API rather than `for await`: Node's web ReadableStream
|
|
39
|
+
// is async-iterable at runtime but not in the DOM type surface, and casting
|
|
40
|
+
// to AsyncIterable to paper over that hides a real difference. The reader also
|
|
41
|
+
// gives us `cancel()`, which is how we release the upstream on client abort.
|
|
42
|
+
const reader = body.getReader();
|
|
43
|
+
try {
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done || clientAborted)
|
|
47
|
+
break;
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
if (ttfbMs === null)
|
|
51
|
+
ttfbMs = Date.now() - startedAt;
|
|
52
|
+
bytesOut += value.byteLength;
|
|
53
|
+
// Client first, always.
|
|
54
|
+
const flushed = res.write(value);
|
|
55
|
+
if (!flushed)
|
|
56
|
+
await once(res, "drain");
|
|
57
|
+
try {
|
|
58
|
+
opts.observe(value);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
observerErrors += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
completed = !clientAborted;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// Upstream died mid-stream. Headers are already sent, so the status cannot
|
|
68
|
+
// be changed; the caller decides whether to synthesise a terminal SSE error
|
|
69
|
+
// frame. Never retry here — the client has already rendered partial text.
|
|
70
|
+
if (!clientAborted)
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
res.off("close", onClose);
|
|
75
|
+
// Release the upstream body so an aborted client does not leave us holding
|
|
76
|
+
// a half-read stream (which on a subscription would keep burning the
|
|
77
|
+
// developer's own quota for output nobody will read).
|
|
78
|
+
if (!completed)
|
|
79
|
+
await reader.cancel().catch(() => undefined);
|
|
80
|
+
reader.releaseLock();
|
|
81
|
+
}
|
|
82
|
+
return { bytesOut, ttfbMs, completed, clientAborted, observerErrors };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Prepare a response for streaming.
|
|
86
|
+
*
|
|
87
|
+
* Every line here is defensive against something buffering the stream and
|
|
88
|
+
* destroying perceived latency. `setNoDelay` disables Nagle, which otherwise
|
|
89
|
+
* coalesces small SSE frames and adds tens of milliseconds of jitter per token
|
|
90
|
+
* batch; `x-accel-buffering` and `no-transform` are for intermediaries.
|
|
91
|
+
*/
|
|
92
|
+
export function beginStream(res, status, headers) {
|
|
93
|
+
res.writeHead(status, headers);
|
|
94
|
+
res.socket?.setNoDelay(true);
|
|
95
|
+
res.flushHeaders();
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=pipe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipe.js","sourceRoot":"","sources":["../../../server/http/pipe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AA8BnC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAgC,EAChC,GAAmB,EACnB,IAAiB;IAEjB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACjC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,2EAA2E;IAC3E,8EAA8E;IAC9E,yBAAyB;IACzB,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,aAAa,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IACF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAEzB,6EAA6E;IAC7E,4EAA4E;IAC5E,+EAA+E;IAC/E,6EAA6E;IAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAEhC,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI,IAAI,aAAa;gBAAE,MAAM;YACjC,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAS;YAElC,IAAI,MAAM,KAAK,IAAI;gBAAE,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;YACrD,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC;YAE7B,wBAAwB;YACxB,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACjC,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAEvC,IAAI,CAAC;gBACH,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,cAAc,IAAI,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,SAAS,GAAG,CAAC,aAAa,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,2EAA2E;QAC3E,4EAA4E;QAC5E,0EAA0E;QAC1E,IAAI,CAAC,aAAa;YAAE,MAAM,GAAG,CAAC;IAChC,CAAC;YAAS,CAAC;QACT,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1B,2EAA2E;QAC3E,qEAAqE;QACrE,sDAAsD;QACtD,IAAI,CAAC,SAAS;YAAE,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC7D,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AACxE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,GAAmB,EAAE,MAAc,EAAE,OAA+B;IAC9F,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IAC7B,GAAG,CAAC,YAAY,EAAE,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP surface.
|
|
3
|
+
*
|
|
4
|
+
* Routing works off the path with any Fest identity prefix removed, so
|
|
5
|
+
* `/t/<token>/v1/messages?beta=true` and `/v1/messages?beta=true` reach the
|
|
6
|
+
* same handler. The query string is preserved throughout: Phase 0 showed Claude
|
|
7
|
+
* Code calls `/v1/messages?beta=true`, not `/v1/messages`.
|
|
8
|
+
*/
|
|
9
|
+
import http from "node:http";
|
|
10
|
+
import { parseIdentityPath } from "../auth/posture.js";
|
|
11
|
+
import { anthropicError } from "./errors.js";
|
|
12
|
+
import { dispatchMessages } from "../pipeline/dispatch.js";
|
|
13
|
+
import { EMPTY_ROUTE_TABLE } from "../routes/table.js";
|
|
14
|
+
import { createEnvResolver } from "../credentials/provider.js";
|
|
15
|
+
import { handleApi } from "../api/routes.js";
|
|
16
|
+
import { handleAuth } from "../api/auth.js";
|
|
17
|
+
import { authorizeApi } from "../auth/guard.js";
|
|
18
|
+
import { handleModels } from "../api/models.js";
|
|
19
|
+
import { handleCountTokens } from "../pipeline/count-tokens.js";
|
|
20
|
+
import { detectInbound } from "../auth/posture.js";
|
|
21
|
+
import { createStaticHost } from "./static.js";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { dirname, resolve } from "node:path";
|
|
24
|
+
import { log } from "../log.js";
|
|
25
|
+
function pathnameOf(url) {
|
|
26
|
+
const q = url.indexOf("?");
|
|
27
|
+
return q === -1 ? url : url.slice(0, q);
|
|
28
|
+
}
|
|
29
|
+
/** `web/dist`, relative to this file, so it resolves from any cwd. */
|
|
30
|
+
const WEB_DIST = resolve(dirname(fileURLToPath(import.meta.url)), "../../web/dist");
|
|
31
|
+
/**
|
|
32
|
+
* Drop the mount prefix from an incoming path, if it is still on it.
|
|
33
|
+
*
|
|
34
|
+
* Whether it is depends on the proxy: Caddy's `handle_path` and nginx's
|
|
35
|
+
* `proxy_pass` with a trailing slash strip it, a bare `reverse_proxy` inside a
|
|
36
|
+
* `handle` block does not. Accepting both means a working deployment does not
|
|
37
|
+
* hinge on which of those the operator reached for. `/fest` with nothing after
|
|
38
|
+
* it becomes `/`, not the empty string, because everything downstream matches
|
|
39
|
+
* on a leading slash.
|
|
40
|
+
*/
|
|
41
|
+
export function stripBasePath(url, basePath) {
|
|
42
|
+
if (basePath === "" || !url.startsWith(basePath))
|
|
43
|
+
return url;
|
|
44
|
+
const rest = url.slice(basePath.length);
|
|
45
|
+
if (rest === "")
|
|
46
|
+
return "/";
|
|
47
|
+
if (rest.startsWith("?"))
|
|
48
|
+
return `/${rest}`;
|
|
49
|
+
// Only a real segment boundary counts: `/festival` must not be read as
|
|
50
|
+
// `/fest` + `ival`.
|
|
51
|
+
return rest.startsWith("/") ? rest : url;
|
|
52
|
+
}
|
|
53
|
+
export function createServer(deps) {
|
|
54
|
+
const basePath = deps.config.basePath;
|
|
55
|
+
const web = createStaticHost(WEB_DIST, basePath);
|
|
56
|
+
if (!web.available) {
|
|
57
|
+
log.info("dashboard bundle not built; serving API only", { expected: WEB_DIST });
|
|
58
|
+
}
|
|
59
|
+
const configured = deps.routes;
|
|
60
|
+
const routesOf = typeof configured === "function" ? configured : () => configured ?? EMPTY_ROUTE_TABLE;
|
|
61
|
+
const ctx = {
|
|
62
|
+
upstreamBaseUrl: deps.config.upstreamBaseUrl,
|
|
63
|
+
sink: deps.sink,
|
|
64
|
+
requireIdentity: deps.config.requireIdentity,
|
|
65
|
+
orgId: deps.orgId,
|
|
66
|
+
resolveIdentity: deps.resolveIdentity,
|
|
67
|
+
touchToken: deps.touchToken,
|
|
68
|
+
routes: EMPTY_ROUTE_TABLE,
|
|
69
|
+
secrets: deps.secrets ?? createEnvResolver(),
|
|
70
|
+
};
|
|
71
|
+
const server = http.createServer((req, res) => {
|
|
72
|
+
const url = stripBasePath(req.url ?? "/", basePath);
|
|
73
|
+
const { remainder } = parseIdentityPath(url);
|
|
74
|
+
const path = pathnameOf(remainder);
|
|
75
|
+
const method = req.method ?? "GET";
|
|
76
|
+
void (async () => {
|
|
77
|
+
try {
|
|
78
|
+
// Claude Code's runtime probes this before any inference, with no
|
|
79
|
+
// credentials. Discovered empirically in Phase 0; it is not part of the
|
|
80
|
+
// documented Anthropic surface, so it would have been missed.
|
|
81
|
+
if (path === "/api/hello") {
|
|
82
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
83
|
+
res.end("{}");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Everything under /api/ is the dashboard, and needs a human session.
|
|
87
|
+
// The proxy path below is authenticated completely differently, by a
|
|
88
|
+
// developer's identity token — a developer must never have to sign in
|
|
89
|
+
// to a web page before their editor works.
|
|
90
|
+
if (path.startsWith("/api/")) {
|
|
91
|
+
const decision = authorizeApi(req, {
|
|
92
|
+
store: deps.store,
|
|
93
|
+
orgId: deps.orgId,
|
|
94
|
+
bindHost: deps.config.host,
|
|
95
|
+
secureCookies: deps.config.secureCookies,
|
|
96
|
+
});
|
|
97
|
+
// Login and "who am I" are reachable without a session, by
|
|
98
|
+
// definition. They still see the decision's session when there is one.
|
|
99
|
+
if (await handleAuth(req, res, path, {
|
|
100
|
+
store: deps.store,
|
|
101
|
+
orgId: deps.orgId,
|
|
102
|
+
secureCookies: deps.config.secureCookies,
|
|
103
|
+
config: deps.config,
|
|
104
|
+
}, decision.allow ? decision.session : null)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!decision.allow) {
|
|
108
|
+
res.writeHead(decision.status, {
|
|
109
|
+
"content-type": "application/json",
|
|
110
|
+
"cache-control": "no-store",
|
|
111
|
+
});
|
|
112
|
+
res.end(JSON.stringify({ error: decision.error }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Every endpoint is a projection of one function in
|
|
116
|
+
// store/queries.ts, where org and member scoping is enforced.
|
|
117
|
+
// A member sees their own traffic and nobody else's, so their scope
|
|
118
|
+
// carries the user id the query layer filters on. The query layer
|
|
119
|
+
// fails closed without it — a member scope with no userId throws
|
|
120
|
+
// rather than quietly widening to the whole org — which is how this
|
|
121
|
+
// wiring bug surfaced as a 500 in a test instead of as a data leak.
|
|
122
|
+
const scope = decision.session === null
|
|
123
|
+
// Unclaimed and on loopback: the guard already refused this case on
|
|
124
|
+
// any other interface.
|
|
125
|
+
? { orgId: deps.orgId, role: "admin" }
|
|
126
|
+
: {
|
|
127
|
+
orgId: decision.session.orgId,
|
|
128
|
+
role: decision.session.role,
|
|
129
|
+
...(decision.session.role === "member" ? { userId: decision.session.userId } : {}),
|
|
130
|
+
};
|
|
131
|
+
if (handleApi(req, res, path, {
|
|
132
|
+
store: deps.store,
|
|
133
|
+
sink: deps.sink,
|
|
134
|
+
bus: deps.bus,
|
|
135
|
+
routes: routesOf(),
|
|
136
|
+
secrets: ctx.secrets,
|
|
137
|
+
scope,
|
|
138
|
+
})) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (path === "/healthz") {
|
|
143
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
144
|
+
res.end(JSON.stringify({ ok: true, sink: deps.sink.stats() }));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Phase 1 stand-in for the dashboard: proves metering end to end
|
|
148
|
+
// without a database.
|
|
149
|
+
if (path === "/_admin/usage" && method === "GET") {
|
|
150
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
151
|
+
res.end(JSON.stringify({ stats: deps.sink.stats(), recent: deps.sink.recent(100) }, null, 2));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// The cheapest possible test for "is something between me and the
|
|
155
|
+
// client buffering my stream". If these arrive one per second, nothing
|
|
156
|
+
// is buffering; if they arrive in a clump at the end, something is.
|
|
157
|
+
if (path === "/_debug/slow-stream") {
|
|
158
|
+
res.writeHead(200, {
|
|
159
|
+
"content-type": "text/event-stream",
|
|
160
|
+
"cache-control": "no-cache, no-transform",
|
|
161
|
+
"x-accel-buffering": "no",
|
|
162
|
+
});
|
|
163
|
+
res.socket?.setNoDelay(true);
|
|
164
|
+
res.flushHeaders();
|
|
165
|
+
for (let i = 1; i <= 10; i += 1) {
|
|
166
|
+
if (res.writableEnded)
|
|
167
|
+
return;
|
|
168
|
+
res.write(`event: tick\ndata: {"n":${i},"at":${Date.now()}}\n\n`);
|
|
169
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
170
|
+
}
|
|
171
|
+
res.end();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// The model menu Claude Code renders under `/model`, labelled "From
|
|
175
|
+
// gateway". Only ever reached in the key posture — discovery needs a
|
|
176
|
+
// credential in ANTHROPIC_AUTH_TOKEN, which is what disables
|
|
177
|
+
// subscription auth in the first place.
|
|
178
|
+
if (path === "/v1/models" && (method === "GET" || method === "HEAD")) {
|
|
179
|
+
const inm = req.headers["if-none-match"];
|
|
180
|
+
handleModels(res, routesOf(), Array.isArray(inm) ? inm[0] : inm);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Relayed, never metered: it consumes no tokens, and counting it would
|
|
184
|
+
// drag every per-request average toward zero.
|
|
185
|
+
if (path === "/v1/messages/count_tokens" && method === "POST") {
|
|
186
|
+
const inbound = detectInbound(url, req.headers);
|
|
187
|
+
await handleCountTokens(req, res, {
|
|
188
|
+
upstreamBaseUrl: deps.config.upstreamBaseUrl,
|
|
189
|
+
path: inbound.effectivePath,
|
|
190
|
+
posture: inbound.posture,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (path === "/v1/messages" && method === "POST") {
|
|
195
|
+
await dispatchMessages(req, res, { ...ctx, routes: routesOf() });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// The dashboard, when built. Last, so it can never shadow the proxy or
|
|
199
|
+
// the API, and narrow enough that an unknown /v1/ path still 404s.
|
|
200
|
+
if ((method === "GET" || method === "HEAD") && web.serve(res, path))
|
|
201
|
+
return;
|
|
202
|
+
// Unknown paths are the early-warning system for Claude Code releases
|
|
203
|
+
// adding endpoints: a warn line here is how we find out before users do.
|
|
204
|
+
log.warn("unhandled path", { method, path });
|
|
205
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
206
|
+
res.end(anthropicError("not_found_error", `Fest: ${method} ${path} is not implemented.`));
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
log.error("unhandled request error", { path, error: String(err).slice(0, 300) });
|
|
210
|
+
if (!res.headersSent) {
|
|
211
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
212
|
+
res.end(anthropicError("api_error", "Fest: internal error."));
|
|
213
|
+
}
|
|
214
|
+
else if (!res.writableEnded) {
|
|
215
|
+
res.end();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
});
|
|
220
|
+
// A long thinking turn must not be killed mid-stream.
|
|
221
|
+
server.requestTimeout = 0;
|
|
222
|
+
server.headersTimeout = 60_000;
|
|
223
|
+
server.keepAliveTimeout = 76_000;
|
|
224
|
+
return server;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../../server/http/server.ts"],"names":[],"mappings":"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"}
|