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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clifton Cunningham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # Fest
2
+
3
+ A self-hosted gateway for Claude Code. Teams point their Claude Code at Fest the
4
+ way they'd point at Bedrock, so one place can monitor requests, tokens and cost —
5
+ and route to different models without Claude Code knowing the difference.
6
+
7
+ **The distinguishing goal: Claude Max / Team subscription support.** Each
8
+ developer authenticates with their *own* subscription. Fest relays their own
9
+ credential and never stores it. Per-user pass-through, never pooling.
10
+
11
+ > **Status: feature complete for a team trial.** A real Claude Code session on a
12
+ > Max subscription runs through Fest, is attributed to its developer, metered
13
+ > into SQLite, and shown on a dashboard behind a login. Model routing and
14
+ > substitution work and are visible. Phase 0 cleared both gates
15
+ > ([results](docs/PHASE0-RESULTS.md)), and `npm run canary` re-checks them
16
+ > against each new Claude Code release.
17
+
18
+ ```bash
19
+ docker compose run --rm fest admin create you@corp.test # password shown once
20
+ docker compose up -d
21
+ docker compose run --rm fest token create dev@corp.test laptop # shown once
22
+ ```
23
+
24
+ Each developer then points Claude Code at the printed URL:
25
+
26
+ ```bash
27
+ env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN \
28
+ ANTHROPIC_BASE_URL=http://fest.corp:8787/t/<token> claude
29
+ ```
30
+
31
+ Or, if the operator has configured Google/GitHub OAuth (see below), a developer
32
+ can skip all of that and self-serve a token from their own machine:
33
+
34
+ ```bash
35
+ npm install -g fest-gateway # the command it installs is `fest`
36
+
37
+ fest login --server http://fest.corp:8787 # opens a browser, stores a token in ~/.fest
38
+ fest claude # runs `claude`, env set up for you
39
+ fest whoami # what's logged in, and whether it's still live
40
+ ```
41
+
42
+ The package is `fest-gateway` — plain `fest` was already taken on npm — but the
43
+ binary it installs is `fest`. Without installing anything, `npx fest-gateway
44
+ login …` does the same job.
45
+
46
+ From a clone of this repo the CLI runs straight off the TypeScript instead:
47
+ `npm run cli -- login …` (note the `--`), or `node server/bin/fest.ts login …`.
48
+
49
+ Or without Docker:
50
+
51
+ ```bash
52
+ node server/bin/fest.ts admin create you@corp.test
53
+ node server/bin/fest.ts token create dev@corp.test laptop
54
+ FEST_REQUIRE_IDENTITY=1 node server/bin/fest.ts serve
55
+ ```
56
+
57
+ ## Why this is possible
58
+
59
+ Claude Code's inference client attaches the subscription OAuth bearer with **no
60
+ host check** against `ANTHROPIC_BASE_URL`. Point it at Fest while logged in with
61
+ Max/Team and Fest receives `Authorization: Bearer sk-ant-oat…`. Confirmed both
62
+ by reading the binary and by running it — see
63
+ [docs/PHASE0-RESULTS.md](docs/PHASE0-RESULTS.md).
64
+
65
+ Token refresh happens client-side against `api.anthropic.com`, outside
66
+ `ANTHROPIC_BASE_URL`. So Fest **forwards and forgets**: it never stores,
67
+ refreshes or holds a subscription credential. That is a genuine security
68
+ advantage over any "store the team's key" design, not just a side effect.
69
+
70
+ ## Two things that will bite you
71
+
72
+ **1. `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` silently kill the
73
+ subscription.** Claude Code demotes off OAuth if *any* Anthropic-issued
74
+ credential is present — including an `apiKeyHelper`. So Fest's identity token
75
+ never goes in those variables; it rides in the URL path prefix or a custom
76
+ header. A design that ignores this silently moves a whole team off their
77
+ subscriptions onto a server-held key, and the failure is invisible.
78
+
79
+ **2. This is supported-by-absence, not by design.** There is no *documented*
80
+ path for relaying a subscription bearer through a third-party gateway. It works
81
+ because no guard exists. Treat it as a version-pinned dependency, and settle the
82
+ policy question with Anthropic before a team-wide rollout.
83
+
84
+ The version dependency is enforced rather than remembered: `npm run canary`
85
+ re-runs both load-bearing Phase 0 experiments against the installed release and
86
+ refuses the rollout if either has changed. See [docs/CANARY.md](docs/CANARY.md);
87
+ verdicts are recorded per version in `docs/canary-history.jsonl`.
88
+
89
+ Worth knowing: Anthropic ships its own self-hostable `claude gateway` (see
90
+ `claude gateway --help`) with central metering, pricing and managed policies. It
91
+ authenticates via OIDC and holds provider credentials itself, so it cannot do
92
+ subscription pass-through — but if that requirement ever softens, it is the
93
+ supported product.
94
+
95
+ ## Design commitments
96
+
97
+ - **Byte-for-byte pass-through.** Subscription tokens are validated against
98
+ request shape, so the body is relayed as opaque bytes — never parsed and
99
+ re-serialised. No model rewriting or prompt shaping on that path.
100
+ - **`cache_control` forwarded verbatim.** Stripping it silently destroys prompt
101
+ caching and inflates every developer's token bill.
102
+ - **Metadata only.** Token counts, models, latency, status — never prompt or
103
+ completion content.
104
+ - **No credential is persistable.** Not by policy, by construction: there is no
105
+ store to put one in. Only fingerprints, enforced by tests.
106
+
107
+ ## Layout
108
+
109
+ ```
110
+ server/bin/fest.ts CLI: serve | migrate | token | admin | seed |
111
+ login | whoami | claude | logout
112
+ server/pipeline/ passthrough (byte-for-byte) — the subscription path
113
+ server/http/ sse parser, tee, header discipline, errors, routing
114
+ server/store/ SQLite schema, write path, identity tokens
115
+ server/usage/ accumulator, lean pricing, nullable cost algebra
116
+ server/secret/ credential classification + redaction
117
+ server/auth/oauth.ts Google/GitHub provider construction
118
+ server/api/oauth.ts /api/auth/oauth/* and /api/auth/identity
119
+ cli/ the developer half of the CLI — no DB access, ever.
120
+ login (loopback OAuth flow) | whoami | claude | logout,
121
+ and the ~/.fest config they share
122
+ shared/types.ts the contracts both halves agree on
123
+ shared/demotion-vars.ts env vars that silently kill subscription auth
124
+ tools/capture-server.ts Phase 0 diagnostic (throwaway)
125
+ docs/PHASE0.md the gating experiment runbook
126
+ docs/CLI-AUTH.md OAuth setup and the fest login/whoami/claude flow
127
+ test/ node --test
128
+ ```
129
+
130
+ ## Accounting rules that are easy to get wrong
131
+
132
+ - **The four token buckets are disjoint.** `input_tokens` excludes cache reads
133
+ and cache writes; context size is the sum. Folding cache reads into input is
134
+ an order-of-magnitude error on a cache-heavy agent workload — and Claude Code
135
+ is one (a trivial `-p` call here showed ~16k cache reads and ~12k one-hour
136
+ cache writes).
137
+ - **`null` cost means unavailable, never zero.** A subscription request is real
138
+ usage with *no org spend*; an unknown model is unpriced, not free. Rollups
139
+ therefore carry a priced sum plus `unpriced_requests` and
140
+ `subscription_requests` separately, so a total renders honestly as
141
+ `$12.3456 (+3 n/a)`.
142
+ - **For a subscription developer, quota is the scarce resource, not dollars.**
143
+ Anthropic returns 5h/7d utilisation on every response, so that is what the
144
+ dashboard shows rather than an invented dollar figure.
145
+
146
+ ## What Fest can see, and what it keeps
147
+
148
+ This is a gateway every prompt a team writes passes through, so it is worth
149
+ being exact about what that means rather than reassuring.
150
+
151
+ **Fest sees everything, because it has to.** A request body arrives at this
152
+ process in full: prompts, file contents you asked Claude about, tool results,
153
+ the lot. That is unavoidable for anything sitting on the wire, and no wording
154
+ changes it.
155
+
156
+ **Fest keeps none of it.** Request bodies are relayed as opaque bytes and never
157
+ parsed for storage. What is written down is metadata: who, when, which model,
158
+ how many tokens in each of the four buckets, latency, HTTP status, error type,
159
+ and the session id Claude Code already sends. There is no column anywhere in
160
+ the schema for prompt or completion text — not disabled, absent — so "turn off
161
+ content capture" is not a setting that could be misconfigured.
162
+
163
+ **No credential is persistable, by construction rather than by policy.** A
164
+ subscription bearer is forwarded and forgotten; there is no table to put one in.
165
+ What is stored of a Fest identity token or a dashboard password is a hash. The
166
+ leak tests grep the entire database, every log line and every API response for
167
+ secret-shaped strings.
168
+
169
+ **Raw per-request rows age out after 30 days**; the hourly rollups, which are
170
+ aggregate, are kept for 400 so a year of trends does not require a year of
171
+ per-developer detail.
172
+
173
+ **An admin can see who used what, and when.** That is the point of a team
174
+ gateway, and the developers pointing their editors at it should be told so
175
+ plainly. They cannot see each other: a `member` account sees only its own
176
+ traffic, enforced in the query layer rather than in the UI.
177
+
178
+ ## Deploying it
179
+
180
+ `docker compose up -d` after creating an owner account. The image has no runtime
181
+ dependencies — Node runs the TypeScript directly — so it is Node plus this
182
+ repository.
183
+
184
+ The dashboard needs a sign-in: scrypt passwords, an opaque session cookie
185
+ hashed at rest, an origin check on the two endpoints that mutate anything, and
186
+ an audit log of administrative actions. Accounts are granted from the host with
187
+ `fest admin create`; there is no self-registration and no email reset, because a
188
+ gateway should not need mail credentials to run.
189
+
190
+ Before anyone has run `fest admin create`, Fest serves the dashboard **open on
191
+ loopback and refuses to start on any other interface**. A single-developer trial
192
+ needs no setup, and "bound it to 0.0.0.0 and forgot the password" cannot happen
193
+ quietly.
194
+
195
+ Put it behind your own TLS proxy and set `FEST_SECURE_COOKIES=1` when you do:
196
+ the server only ever sees plain HTTP and cannot detect this for itself.
197
+
198
+ | | |
199
+ | --- | --- |
200
+ | `FEST_PORT`, `FEST_HOST` | listen address. Default `127.0.0.1:8787`. |
201
+ | `FEST_DB`, `FEST_USAGE_LOG` | SQLite path, and the greppable JSONL trail. |
202
+ | `FEST_REQUIRE_IDENTITY` | refuse unattributed requests. Off by default; **on** for a team. |
203
+ | `FEST_SECURE_COOKIES` | set behind HTTPS. |
204
+ | `FEST_ROUTES` | routing table path. Absent means everything passes through. |
205
+ | `FEST_UPSTREAM_BASE_URL`, `FEST_LOG_LEVEL` | |
206
+ | `FEST_PRICING_REFRESH` | set `0` to never fetch prices. Rates then come only from the vendored snapshot, which is always sufficient. |
207
+ | `FEST_PRICING_URL` | internal mirror of litellm's price file. |
208
+ | `FEST_GOOGLE_CLIENT_ID`, `FEST_GOOGLE_CLIENT_SECRET` | Google OAuth, for `fest login` and the dashboard. Absent means Google sign-in is off. |
209
+ | `FEST_GITHUB_CLIENT_ID`, `FEST_GITHUB_CLIENT_SECRET` | GitHub OAuth, same idea. |
210
+ | `FEST_ALLOWED_EMAIL_DOMAINS` | comma-separated. **Required to enable OAuth at all** — empty refuses every OAuth login rather than allowing any Google/GitHub account. |
211
+ | `FEST_PUBLIC_URL` | base URL Fest is reachable at, for building the OAuth redirect URI. Defaults to `http://<host>:<port>`. |
212
+ | `FEST_BASE_PATH` | mount path, if Fest is not at the root of its origin. Defaults to the path of `FEST_PUBLIC_URL`, so setting that is usually enough. |
213
+
214
+ See [docs/CLI-AUTH.md](docs/CLI-AUTH.md) for the full OAuth setup and how
215
+ `fest login`'s loopback flow works.
216
+
217
+ ### Behind Caddy, on a path
218
+
219
+ Fest can be mounted under a path rather than on its own hostname —
220
+ `https://lastlight.nearform.dev/fest`. Set `FEST_PUBLIC_URL` to the full URL
221
+ including that path and Fest takes the mount point from it: the dashboard's
222
+ `<base href>` is rewritten to match, and the OAuth redirect URI is built
223
+ against it.
224
+
225
+ ```caddyfile
226
+ lastlight.nearform.dev {
227
+ handle_path /fest/* {
228
+ reverse_proxy 127.0.0.1:8787 {
229
+ # SSE: /api/live is a long-lived event stream, and a buffering
230
+ # proxy turns the live screen into a screen that updates once.
231
+ flush_interval -1
232
+ }
233
+ }
234
+ }
235
+ ```
236
+
237
+ ```bash
238
+ FEST_PUBLIC_URL=https://lastlight.nearform.dev/fest
239
+ FEST_SECURE_COOKIES=1
240
+ ```
241
+
242
+ `handle_path` strips the `/fest` prefix before proxying. A plain `handle` +
243
+ `reverse_proxy`, which does not strip it, works too — Fest removes its own
244
+ mount path from an incoming request when it is still there, so either spelling
245
+ routes. Caddy passes the original `Host` upstream by default, which the CSRF
246
+ origin check needs; on nginx that is `proxy_set_header Host $host`, and
247
+ `proxy_buffering off` for the event stream.
248
+
249
+ The registered OAuth redirect URI must include the path —
250
+ `https://lastlight.nearform.dev/fest/api/auth/oauth/google/callback`. Developers
251
+ point both Claude Code and the CLI at the full prefixed URL
252
+ (`fest login --server https://lastlight.nearform.dev/fest`).
253
+
254
+ ## Requirements
255
+
256
+ Node >= 22.18 (TypeScript runs directly via native type stripping — no build
257
+ step for the server). Vite + React are used only for the dashboard, later.
258
+
259
+ ## Commands
260
+
261
+ ```bash
262
+ npm test # node --test
263
+ npm run canary # version gate: re-run before rolling out a Claude Code release
264
+ npm run capture # Phase 0 capture server
265
+ npm run typecheck # tsc --noEmit (needs npm install first)
266
+ npm run dev # gateway + dashboard with hot reload
267
+ npm run demo # synthetic traffic in a scratch database, then both of the above
268
+ ```
269
+
270
+ ```bash
271
+ node server/bin/fest.ts admin create <email> [--role R] [--password-stdin]
272
+ node server/bin/fest.ts admin list | admin passwd <email> | admin disable <email>
273
+ node server/bin/fest.ts token create <email> [name] | token list | token revoke <id>
274
+ ```
@@ -0,0 +1,326 @@
1
+ /**
2
+ * `fest claude`: run the real `claude` binary, pointed at this developer's
3
+ * Fest gateway, in whichever of two postures will actually work here.
4
+ *
5
+ * **Subscription posture** sets `ANTHROPIC_BASE_URL` to the path-prefixed
6
+ * identity carrier (`server/auth/posture.ts`'s `parseIdentityPath`) and sets no
7
+ * credential, so Claude Code's own OAuth login pays. **Gateway posture** sets a
8
+ * bare base URL plus `ANTHROPIC_AUTH_TOKEN=<fest token>`, so the server's own
9
+ * credentials pay and the server-published `/model` menu becomes reachable.
10
+ *
11
+ * The two are mutually exclusive in the client: gateway model discovery needs a
12
+ * credential in the environment, and any such credential disables subscription
13
+ * auth. So Fest picks — OAuth login present → subscription, absent → gateway —
14
+ * and `fest claude gw` forces gateway when a developer wants the menu anyway.
15
+ *
16
+ * Both postures strip every demotion var first — the same reasoning
17
+ * `tools/canary.ts` applies to its test client, from the same
18
+ * `shared/demotion-vars.ts` list. Gateway posture then sets its token *after*
19
+ * that strip rather than exempting it, so an inherited `ANTHROPIC_API_KEY` can
20
+ * never survive alongside the Fest token.
21
+ */
22
+ import { spawn } from "node:child_process";
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { join } from "node:path";
26
+ import { DEMOTION_VARS } from "../shared/demotion-vars.js";
27
+ import { BASE_MODELS } from "../shared/base-models.js";
28
+ import { readCliConfig } from "./config.js";
29
+ /**
30
+ * Pure, so it's testable without spawning a real `claude` binary.
31
+ *
32
+ * Defaults to subscription posture: that is what `fest claude` did before there
33
+ * was a choice, and it remains the answer for a developer with their own login.
34
+ */
35
+ export function buildClaudeEnv(base, cfg, posture = "subscription") {
36
+ const env = { ...base };
37
+ for (const v of DEMOTION_VARS)
38
+ delete env[v];
39
+ if (posture === "key") {
40
+ env["ANTHROPIC_BASE_URL"] = cfg.serverUrl;
41
+ // Set after the strip, never by exempting it from the strip list.
42
+ env["ANTHROPIC_AUTH_TOKEN"] = cfg.identityToken;
43
+ }
44
+ else {
45
+ env["ANTHROPIC_BASE_URL"] = `${cfg.serverUrl}/t/${cfg.identityToken}`;
46
+ }
47
+ return env;
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Detection — metadata and keys only, never a credential value
51
+ // ---------------------------------------------------------------------------
52
+ /** The four layers Claude Code merges settings from, most general first. */
53
+ function settingsFiles() {
54
+ return [
55
+ join(homedir(), ".claude", "settings.json"),
56
+ join(homedir(), ".claude", "settings.local.json"),
57
+ join(process.cwd(), ".claude", "settings.json"),
58
+ join(process.cwd(), ".claude", "settings.local.json"),
59
+ ];
60
+ }
61
+ function readSettings(path) {
62
+ if (!existsSync(path))
63
+ return null;
64
+ try {
65
+ return JSON.parse(readFileSync(path, "utf8"));
66
+ }
67
+ catch {
68
+ return "unparseable";
69
+ }
70
+ }
71
+ /**
72
+ * Is this machine on an Anthropic OAuth subscription login?
73
+ *
74
+ * Read exactly as `tools/canary.ts::subscriptionLogin()` reads it — account
75
+ * metadata only. No credential lives in this function, and the reason string is
76
+ * printed to a developer's terminal, so nothing identifying goes in it either.
77
+ */
78
+ export function detectSubscriptionLogin() {
79
+ const path = join(homedir(), ".claude.json");
80
+ if (!existsSync(path))
81
+ return { ok: false, detail: "no Anthropic subscription login found" };
82
+ const parsed = readSettings(path);
83
+ if (parsed === null || parsed === "unparseable") {
84
+ return { ok: false, detail: "~/.claude.json is unparseable" };
85
+ }
86
+ if (!parsed["oauthAccount"]) {
87
+ return { ok: false, detail: "no Anthropic subscription login found" };
88
+ }
89
+ return { ok: true, detail: "Anthropic OAuth login found" };
90
+ }
91
+ /**
92
+ * Settings-layer demotion triggers, by KEY only.
93
+ *
94
+ * `buildClaudeEnv` can strip a poisoned env var; it cannot touch these, because
95
+ * the client reads its own settings files. In subscription posture that is the
96
+ * silent failure this whole project exists to prevent: the developer believes
97
+ * they are on their own plan, and the session is quietly billed to someone
98
+ * else's key. Values are never read — `env.ANTHROPIC_API_KEY` in a settings
99
+ * file is a live secret.
100
+ */
101
+ export function settingsDemotionTriggers() {
102
+ const found = [];
103
+ for (const path of settingsFiles()) {
104
+ const parsed = readSettings(path);
105
+ if (parsed === null)
106
+ continue;
107
+ if (parsed === "unparseable") {
108
+ found.push(`${path}: unparseable, cannot check it`);
109
+ continue;
110
+ }
111
+ if (parsed["apiKeyHelper"])
112
+ found.push(`${path}: apiKeyHelper`);
113
+ for (const v of DEMOTION_VARS) {
114
+ if (parsed["env"] && v in parsed["env"])
115
+ found.push(`${path}: env.${v}`);
116
+ }
117
+ }
118
+ return found;
119
+ }
120
+ /**
121
+ * The model the session will open on, as far as the CLI can observe it.
122
+ *
123
+ * Precedence follows the client's own: an explicit `--model` beats
124
+ * `ANTHROPIC_MODEL`, which beats a `model` key in the settings layers (most
125
+ * specific layer wins). `null` means *unknown* — Claude Code's built-in default
126
+ * or the developer's last selection, neither of which is visible from here.
127
+ */
128
+ export function resolveIntendedModel(argv, env) {
129
+ for (let i = 0; i < argv.length; i++) {
130
+ const arg = argv[i];
131
+ if (arg === "--model" && i + 1 < argv.length)
132
+ return argv[i + 1];
133
+ if (arg.startsWith("--model="))
134
+ return arg.slice("--model=".length);
135
+ }
136
+ const fromEnv = env["ANTHROPIC_MODEL"];
137
+ if (fromEnv !== undefined && fromEnv !== "")
138
+ return fromEnv;
139
+ let fromSettings = null;
140
+ for (const path of settingsFiles()) {
141
+ const parsed = readSettings(path);
142
+ if (parsed === null || parsed === "unparseable")
143
+ continue;
144
+ if (typeof parsed["model"] === "string" && parsed["model"] !== "")
145
+ fromSettings = parsed["model"];
146
+ }
147
+ return fromSettings;
148
+ }
149
+ /**
150
+ * Does the model this session will open on have a substitute route?
151
+ *
152
+ * "The menu is non-empty" is not the question. In gateway posture there is no
153
+ * caller credential to fall back on, so an unrouted model 401s on the first
154
+ * message — and against a typical routing table the menu is full while Opus and
155
+ * Haiku are both unrouted. This turns that first-message 401 into a sentence
156
+ * read before the session starts.
157
+ *
158
+ * Pure: the fetch lives in `fetchServableModels`, so the cliff logic is
159
+ * testable without a socket.
160
+ */
161
+ export function checkServable(menu, intended) {
162
+ const servable = [...menu].sort();
163
+ const list = servable.join(", ");
164
+ if (intended !== null) {
165
+ if (servable.includes(intended))
166
+ return { kind: "ok", message: "" };
167
+ return {
168
+ kind: "fatal",
169
+ message: `${intended} has no substitute route on this gateway; in gateway posture it would fail on the first message.\n` +
170
+ `Servable: ${list}`,
171
+ };
172
+ }
173
+ // Unknown: the client will open on its own default or last selection. Name
174
+ // precisely which of Anthropic's base models would fail if it picks one.
175
+ const missing = BASE_MODELS.filter((m) => !servable.includes(m.id));
176
+ if (missing.length === 0)
177
+ return { kind: "ok", message: "" };
178
+ const names = missing.map((m) => m.name);
179
+ const subject = names.length === 1 ? `${names[0]} is` : `${names.slice(0, -1).join(", ")} and ${names.at(-1)} are`;
180
+ const object = names.length === 1 ? "it" : names.length === 2 ? "either" : "any of them";
181
+ return {
182
+ kind: "warn",
183
+ message: `${subject} not routed on this gateway; selecting ${object} will fail.\nServable: ${list}`,
184
+ };
185
+ }
186
+ /** How long to wait for the menu. The client's own discovery timeout is 3s. */
187
+ const MENU_TIMEOUT_MS = 3_000;
188
+ /**
189
+ * `GET /v1/models` — served without identity resolution, so no auth header is
190
+ * needed here.
191
+ *
192
+ * A 404 or an empty list is fatal: gateway posture with no substitute routes
193
+ * cannot serve anything. A network error is only a warning — a flaky link must
194
+ * not brick a known-good gateway.
195
+ */
196
+ export async function fetchServableModels(serverUrl) {
197
+ let res;
198
+ try {
199
+ res = await fetch(`${serverUrl}/v1/models?limit=1000`, {
200
+ signal: AbortSignal.timeout(MENU_TIMEOUT_MS),
201
+ });
202
+ }
203
+ catch (err) {
204
+ return {
205
+ ok: false,
206
+ fatal: false,
207
+ message: `could not reach ${serverUrl}/v1/models (${err instanceof Error ? err.message : String(err)}) — continuing unchecked.`,
208
+ };
209
+ }
210
+ if (res.status === 404) {
211
+ return {
212
+ ok: false,
213
+ fatal: true,
214
+ message: `${serverUrl} publishes no gateway models, so gateway posture has nothing to serve.\n` +
215
+ "The gateway needs substitute routes — check FEST_ROUTES on the server.",
216
+ };
217
+ }
218
+ if (!res.ok) {
219
+ return { ok: false, fatal: false, message: `GET /v1/models returned ${res.status} — continuing unchecked.` };
220
+ }
221
+ let body;
222
+ try {
223
+ body = (await res.json());
224
+ }
225
+ catch {
226
+ return { ok: false, fatal: false, message: "GET /v1/models returned unparseable JSON — continuing unchecked." };
227
+ }
228
+ const ids = (body.data ?? []).map((m) => m.id).filter((id) => typeof id === "string");
229
+ if (ids.length === 0) {
230
+ return {
231
+ ok: false,
232
+ fatal: true,
233
+ message: `${serverUrl} publishes an empty model menu, so gateway posture has nothing to serve.\n` +
234
+ "The gateway needs substitute routes — check FEST_ROUTES on the server.",
235
+ };
236
+ }
237
+ return { ok: true, ids };
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // Running it
241
+ // ---------------------------------------------------------------------------
242
+ const note = (s) => void process.stderr.write(`fest claude: ${s}\n`);
243
+ /**
244
+ * Warnings about the *chosen* posture. Collected separately from the notice
245
+ * line because the posture is now something Fest asserts rather than something
246
+ * the developer typed — so every consequence of that assertion is said out loud.
247
+ */
248
+ export function postureWarnings(posture, env, triggers) {
249
+ const warnings = [];
250
+ const discovery = env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"];
251
+ if (posture === "subscription") {
252
+ for (const trigger of triggers) {
253
+ warnings.push(`${trigger} will demote this session off your subscription. Clear it or expect org billing.`);
254
+ }
255
+ if (discovery !== undefined && discovery !== "") {
256
+ warnings.push("Gateway model discovery is unavailable in subscription posture. Use `fest claude gw`.");
257
+ }
258
+ return warnings;
259
+ }
260
+ if (discovery === undefined || discovery === "") {
261
+ warnings.push("Export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 to see gateway models in /model.");
262
+ }
263
+ if (env["_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL"]) {
264
+ // An explicit override: warn, don't strip.
265
+ warnings.push("_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL disables gateway discovery. Unset it.");
266
+ }
267
+ return warnings;
268
+ }
269
+ export async function runClaude(argv, requested = "auto") {
270
+ const cfg = await readCliConfig();
271
+ if (cfg === null) {
272
+ throw new Error("not logged in. Run `fest login` first.");
273
+ }
274
+ const login = requested === "auto" ? detectSubscriptionLogin() : null;
275
+ const posture = requested === "auto" ? (login.ok ? "subscription" : "key") : requested;
276
+ // Who pays, every time — not only on `gw`. Auto-detection changes the answer
277
+ // for a developer with no subscription login, so it must never be implicit.
278
+ if (requested !== "auto") {
279
+ note("gateway posture forced — org credentials pay.");
280
+ }
281
+ else if (posture === "subscription") {
282
+ note(`subscription posture (${login.detail}) — your own plan pays.`);
283
+ }
284
+ else {
285
+ note(`gateway posture (${login.detail}) — org credentials pay.`);
286
+ }
287
+ for (const w of postureWarnings(posture, process.env, posture === "subscription" ? settingsDemotionTriggers() : [])) {
288
+ note(w);
289
+ }
290
+ if (posture === "key") {
291
+ const menu = await fetchServableModels(cfg.serverUrl);
292
+ if (!menu.ok) {
293
+ if (menu.fatal)
294
+ throw new Error(menu.message);
295
+ note(menu.message);
296
+ }
297
+ else {
298
+ const verdict = checkServable(menu.ids, resolveIntendedModel(argv, process.env));
299
+ if (verdict.kind === "fatal")
300
+ throw new Error(verdict.message);
301
+ if (verdict.kind === "warn")
302
+ note(verdict.message);
303
+ }
304
+ }
305
+ const env = buildClaudeEnv(process.env, cfg, posture);
306
+ const child = spawn("claude", argv, { stdio: "inherit", env });
307
+ const code = await new Promise((resolve, reject) => {
308
+ child.on("error", (err) => {
309
+ if (err.code === "ENOENT") {
310
+ reject(new Error("`claude` was not found on PATH. Install Claude Code first."));
311
+ }
312
+ else {
313
+ reject(err);
314
+ }
315
+ });
316
+ child.on("exit", (exitCode, signal) => {
317
+ if (signal !== null) {
318
+ process.kill(process.pid, signal);
319
+ return;
320
+ }
321
+ resolve(exitCode ?? 1);
322
+ });
323
+ });
324
+ process.exitCode = code;
325
+ }
326
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../cli/claude.ts"],"names":[],"mappings":"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"}