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,300 @@
1
+ /**
2
+ * `/api/auth/oauth/*`: Google/GitHub sign-in, and `/api/auth/identity`.
3
+ *
4
+ * A GET-only surface, unlike the rest of `/api/auth/*` — the whole flow is a
5
+ * sequence of browser redirects, so there is no JSON body to receive and no
6
+ * Origin header to check (the provider's redirect back is a cross-site
7
+ * top-level navigation by definition, which is also why the state/verifier
8
+ * cookies below are `SameSite=Lax`, not `Strict`: a Strict cookie would not
9
+ * survive that navigation and every callback would fail state validation).
10
+ *
11
+ * One flow serves two callers, distinguished by whether `start` was called
12
+ * with `cli_redirect_uri`:
13
+ * - dashboard: `start` -> provider -> `callback` -> session cookie -> `/`.
14
+ * - CLI (`fest login`): `start?cli_redirect_uri=http://127.0.0.1:<port>/callback`
15
+ * -> provider -> `callback` -> a freshly minted identity token -> redirect
16
+ * to the CLI's own loopback server, which is what captures it.
17
+ *
18
+ * `/api/auth/identity` is separate from both: it's how `fest whoami` asks
19
+ * "is my token still good", authenticated the same way the proxy path is
20
+ * (`Authorization: Bearer` or `X-Fest-Token`), not by cookie.
21
+ */
22
+ import { generateState, generateCodeVerifier } from "arctic";
23
+ import { googleProvider, githubProvider, emailDomainAllowed } from "../auth/oauth.js";
24
+ import { ensureUser } from "../store/bootstrap.js";
25
+ import { normaliseEmail, hasAnyOwner } from "../auth/accounts.js";
26
+ import { createSession, serializeCookie, parseCookies, ABSOLUTE_MS } from "../auth/session.js";
27
+ import { createToken, resolveToken } from "../store/tokens.js";
28
+ import { recordAudit } from "../store/audit.js";
29
+ import { clientIp } from "../auth/guard.js";
30
+ import { log } from "../log.js";
31
+ const STATE_COOKIE = "fest_oauth_state";
32
+ const VERIFIER_COOKIE = "fest_oauth_verifier";
33
+ const CLI_REDIRECT_COOKIE = "fest_oauth_cli_redirect";
34
+ const OAUTH_COOKIE_PATH = "/api/auth/oauth";
35
+ const OAUTH_COOKIE_MAX_AGE_S = 600;
36
+ function json(res, status, body) {
37
+ res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
38
+ res.end(JSON.stringify(body));
39
+ }
40
+ function oauthCookie(name, value, secure) {
41
+ const parts = [
42
+ `${name}=${encodeURIComponent(value)}`,
43
+ `Path=${OAUTH_COOKIE_PATH}`,
44
+ "HttpOnly",
45
+ "SameSite=Lax",
46
+ `Max-Age=${OAUTH_COOKIE_MAX_AGE_S}`,
47
+ ];
48
+ if (secure)
49
+ parts.push("Secure");
50
+ return parts.join("; ");
51
+ }
52
+ function clearOauthCookie(name, secure) {
53
+ const parts = [`${name}=`, `Path=${OAUTH_COOKIE_PATH}`, "HttpOnly", "SameSite=Lax", "Max-Age=0"];
54
+ if (secure)
55
+ parts.push("Secure");
56
+ return parts.join("; ");
57
+ }
58
+ /** Only ever a loopback URL: this cookie controls where a live identity token gets redirected. */
59
+ function isLoopbackRedirect(raw) {
60
+ try {
61
+ const u = new URL(raw);
62
+ return u.protocol === "http:" && (u.hostname === "127.0.0.1" || u.hostname === "localhost");
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ async function fetchGoogleEmail(accessToken) {
69
+ const res = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
70
+ headers: { authorization: `Bearer ${accessToken}` },
71
+ });
72
+ if (!res.ok)
73
+ throw new Error(`google userinfo: HTTP ${res.status}`);
74
+ const body = (await res.json());
75
+ if (typeof body.email !== "string" || body.email_verified !== true) {
76
+ throw new Error("google account has no verified email");
77
+ }
78
+ return body.email;
79
+ }
80
+ async function fetchGithubEmail(accessToken) {
81
+ const res = await fetch("https://api.github.com/user/emails", {
82
+ headers: {
83
+ authorization: `Bearer ${accessToken}`,
84
+ "user-agent": "fest",
85
+ accept: "application/vnd.github+json",
86
+ },
87
+ });
88
+ if (!res.ok)
89
+ throw new Error(`github emails: HTTP ${res.status}`);
90
+ const rows = (await res.json());
91
+ const primary = rows.find((r) => r.primary && r.verified);
92
+ if (primary === undefined)
93
+ throw new Error("github account has no verified primary email");
94
+ return primary.email;
95
+ }
96
+ async function start(req, res, provider, deps) {
97
+ const cfg = deps.config;
98
+ if (cfg.allowedEmailDomains.length === 0) {
99
+ json(res, 503, { error: "OAuth sign-in is disabled: FEST_ALLOWED_EMAIL_DOMAINS is unset" });
100
+ return;
101
+ }
102
+ const url = new URL(req.url ?? "/", "http://internal");
103
+ const cliRedirect = url.searchParams.get("cli_redirect_uri");
104
+ if (cliRedirect !== null && !isLoopbackRedirect(cliRedirect)) {
105
+ json(res, 400, { error: "cli_redirect_uri must be http://127.0.0.1 or http://localhost" });
106
+ return;
107
+ }
108
+ const state = generateState();
109
+ const cookies = [oauthCookie(STATE_COOKIE, state, cfg.secureCookies)];
110
+ let authUrl;
111
+ if (provider === "google") {
112
+ const built = googleProvider(cfg);
113
+ if (built === null) {
114
+ json(res, 404, { error: "google sign-in is not configured" });
115
+ return;
116
+ }
117
+ const verifier = generateCodeVerifier();
118
+ cookies.push(oauthCookie(VERIFIER_COOKIE, verifier, cfg.secureCookies));
119
+ authUrl = built.createAuthorizationURL(state, verifier, ["openid", "profile", "email"]);
120
+ }
121
+ else {
122
+ const built = githubProvider(cfg);
123
+ if (built === null) {
124
+ json(res, 404, { error: "github sign-in is not configured" });
125
+ return;
126
+ }
127
+ authUrl = built.createAuthorizationURL(state, ["user:email"]);
128
+ }
129
+ if (cliRedirect !== null)
130
+ cookies.push(oauthCookie(CLI_REDIRECT_COOKIE, cliRedirect, cfg.secureCookies));
131
+ res.setHeader("set-cookie", cookies);
132
+ res.writeHead(302, { location: authUrl.toString() });
133
+ res.end();
134
+ }
135
+ async function callback(req, res, provider, deps) {
136
+ const cfg = deps.config;
137
+ const cookies = parseCookies(req.headers.cookie);
138
+ const cliRedirect = cookies[CLI_REDIRECT_COOKIE] ?? null;
139
+ const clearCookies = [
140
+ clearOauthCookie(STATE_COOKIE, cfg.secureCookies),
141
+ clearOauthCookie(VERIFIER_COOKIE, cfg.secureCookies),
142
+ clearOauthCookie(CLI_REDIRECT_COOKIE, cfg.secureCookies),
143
+ ];
144
+ const fail = (message) => {
145
+ res.setHeader("set-cookie", clearCookies);
146
+ const location = cliRedirect !== null ? `${cliRedirect}?error=${encodeURIComponent(message)}` : `/?auth_error=${encodeURIComponent(message)}`;
147
+ res.writeHead(302, { location });
148
+ res.end();
149
+ };
150
+ const url = new URL(req.url ?? "/", "http://internal");
151
+ const code = url.searchParams.get("code");
152
+ const state = url.searchParams.get("state");
153
+ const expectedState = cookies[STATE_COOKIE];
154
+ if (code === null || state === null || expectedState === undefined || state !== expectedState) {
155
+ fail("invalid or expired sign-in attempt");
156
+ return;
157
+ }
158
+ let email;
159
+ try {
160
+ if (provider === "google") {
161
+ const built = googleProvider(cfg);
162
+ if (built === null) {
163
+ fail("google sign-in is not configured");
164
+ return;
165
+ }
166
+ const verifier = cookies[VERIFIER_COOKIE];
167
+ if (verifier === undefined) {
168
+ fail("invalid or expired sign-in attempt");
169
+ return;
170
+ }
171
+ const tokens = await built.validateAuthorizationCode(code, verifier);
172
+ email = await fetchGoogleEmail(tokens.accessToken());
173
+ }
174
+ else {
175
+ const built = githubProvider(cfg);
176
+ if (built === null) {
177
+ fail("github sign-in is not configured");
178
+ return;
179
+ }
180
+ const tokens = await built.validateAuthorizationCode(code);
181
+ email = await fetchGithubEmail(tokens.accessToken());
182
+ }
183
+ }
184
+ catch (err) {
185
+ log.warn("oauth callback failed", { provider, error: String(err).slice(0, 200) });
186
+ fail("sign-in failed");
187
+ return;
188
+ }
189
+ const normalised = normaliseEmail(email);
190
+ const ip = clientIp(req);
191
+ if (!emailDomainAllowed(cfg, normalised)) {
192
+ recordAudit(deps.store, {
193
+ orgId: deps.orgId,
194
+ actorLabel: normalised,
195
+ action: "auth.oauth_login",
196
+ outcome: "denied",
197
+ detail: { provider, reason: "domain_not_allowed" },
198
+ ip,
199
+ });
200
+ fail("this email domain is not permitted to sign in");
201
+ return;
202
+ }
203
+ const user = ensureUser(deps.store, { orgId: deps.orgId, email: normalised });
204
+ if (cliRedirect !== null) {
205
+ const created = createToken(deps.store, { orgId: deps.orgId, userId: user.id, name: "cli-login" });
206
+ recordAudit(deps.store, {
207
+ orgId: deps.orgId,
208
+ actorUserId: user.id,
209
+ actorLabel: user.email,
210
+ action: "auth.oauth_login",
211
+ target: created.id,
212
+ outcome: "ok",
213
+ detail: { provider, mode: "cli" },
214
+ ip,
215
+ });
216
+ res.setHeader("set-cookie", clearCookies);
217
+ res.writeHead(302, {
218
+ location: `${cliRedirect}?token=${encodeURIComponent(created.raw)}&email=${encodeURIComponent(user.email)}`,
219
+ });
220
+ res.end();
221
+ return;
222
+ }
223
+ // Dashboard, not CLI: the first person to complete a sign-in while nobody
224
+ // has claimed this deployment yet claims it, exactly as if they had run
225
+ // `fest admin create` from the host — this OAuth path exists specifically
226
+ // so that command is no longer the only way in. Never done on the CLI
227
+ // token-minting branch above: a `fest login` should never be able to grant
228
+ // dashboard ownership as a side effect.
229
+ let owner = user;
230
+ if (!hasAnyOwner(deps.store, deps.orgId)) {
231
+ deps.store.db.prepare(`UPDATE users SET role = 'owner' WHERE id = ?`).run(user.id);
232
+ owner = { ...user, role: "owner" };
233
+ }
234
+ const created = createSession(deps.store, {
235
+ orgId: deps.orgId,
236
+ userId: owner.id,
237
+ userAgent: String(req.headers["user-agent"] ?? ""),
238
+ ip,
239
+ });
240
+ recordAudit(deps.store, {
241
+ orgId: deps.orgId,
242
+ actorUserId: owner.id,
243
+ actorLabel: owner.email,
244
+ action: "auth.oauth_login",
245
+ target: created.sessionId,
246
+ outcome: "ok",
247
+ detail: { provider, mode: "dashboard", role: owner.role },
248
+ ip,
249
+ });
250
+ res.setHeader("set-cookie", [
251
+ ...clearCookies,
252
+ serializeCookie(created.raw, { secure: cfg.secureCookies, maxAgeSeconds: Math.floor(ABSOLUTE_MS / 1000) }),
253
+ ]);
254
+ // The dashboard root as the browser sees it, which is not `/` when Fest is
255
+ // mounted under a path — redirecting there would leave the prefix behind.
256
+ res.writeHead(302, { location: `${cfg.basePath}/` });
257
+ res.end();
258
+ }
259
+ async function identity(req, res, deps) {
260
+ if ((req.method ?? "GET") !== "GET") {
261
+ json(res, 405, { error: "method not allowed" });
262
+ return;
263
+ }
264
+ const auth = req.headers.authorization;
265
+ const bearer = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : null;
266
+ const headerToken = req.headers["x-fest-token"];
267
+ const raw = bearer ?? (typeof headerToken === "string" ? headerToken : null);
268
+ const resolved = resolveToken(deps.store, raw);
269
+ if (resolved === null) {
270
+ json(res, 401, { error: "invalid or revoked token" });
271
+ return;
272
+ }
273
+ const row = deps.store.db.prepare(`SELECT email FROM users WHERE id = ?`).get(resolved.userId);
274
+ json(res, 200, { email: row?.email ?? "", orgId: resolved.orgId });
275
+ }
276
+ const ROUTE = /^\/api\/auth\/oauth\/(google|github)\/(start|callback)$/;
277
+ /** Handles `/api/auth/oauth/*` and `/api/auth/identity`. Returns false when the path is not ours. */
278
+ export async function handleOauth(req, res, path, deps) {
279
+ if (path === "/api/auth/identity") {
280
+ await identity(req, res, deps);
281
+ return true;
282
+ }
283
+ const m = ROUTE.exec(path);
284
+ if (m === null)
285
+ return false;
286
+ if ((req.method ?? "GET") !== "GET") {
287
+ json(res, 405, { error: "method not allowed" });
288
+ return true;
289
+ }
290
+ const provider = m[1];
291
+ const step = m[2];
292
+ if (step === "start") {
293
+ await start(req, res, provider, deps);
294
+ }
295
+ else {
296
+ await callback(req, res, provider, deps);
297
+ }
298
+ return true;
299
+ }
300
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../server/api/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * JSON API for the dashboard.
3
+ *
4
+ * Phase 3 (Vite + React + TanStack Query) consumes these. Kept deliberately
5
+ * thin: every endpoint is a direct projection of one function in
6
+ * `store/queries.ts`, which is where org/member scoping is enforced. No handler
7
+ * builds SQL, so a forgotten filter in a route cannot leak another tenant's
8
+ * data.
9
+ *
10
+ * Authentication is decided before this file runs (see auth/guard.ts) and the
11
+ * result arrives as `scope`. Every query takes that scope explicitly, so there
12
+ * is no ambient "current org" a handler could forget to apply.
13
+ */
14
+ import { listRequests, usageTotals, usageByUser, usageByModel, usageByCredentialOrigin, usageSeries, errorBreakdown, latencySummary, latestQuotaByUser, } from "../store/queries.js";
15
+ import { explainRoutes } from "./routing-view.js";
16
+ import { ADAPTER_TRANSFORMS } from "../adapters/registry.js";
17
+ import { handleLive } from "./live.js";
18
+ import { listAudit } from "../store/audit.js";
19
+ const DEFAULT_RANGE_MS = 24 * 3_600_000;
20
+ const MAX_RANGE_MS = 400 * 86_400_000;
21
+ function intParam(params, name) {
22
+ const raw = params.get(name);
23
+ if (raw === null || raw.trim() === "")
24
+ return undefined;
25
+ const n = Number(raw);
26
+ return Number.isFinite(n) ? Math.trunc(n) : undefined;
27
+ }
28
+ /**
29
+ * Ranges come from a query string, so they are clamped rather than trusted: an
30
+ * absurd `fromMs` would otherwise turn a dashboard refresh into a full-table
31
+ * scan, and on single-writer SQLite that stalls the metering flush behind it.
32
+ */
33
+ export function parseRange(params, now = Date.now()) {
34
+ const toMs = intParam(params, "to") ?? now;
35
+ const fromRaw = intParam(params, "from") ?? toMs - DEFAULT_RANGE_MS;
36
+ const fromMs = Math.max(fromRaw, toMs - MAX_RANGE_MS);
37
+ return { fromMs: Math.min(fromMs, toMs), toMs };
38
+ }
39
+ function strParam(params, name) {
40
+ const raw = params.get(name);
41
+ return raw === null || raw.trim() === "" ? undefined : raw;
42
+ }
43
+ function json(res, status, body) {
44
+ const payload = JSON.stringify(body);
45
+ res.writeHead(status, {
46
+ "content-type": "application/json",
47
+ "cache-control": "no-store",
48
+ });
49
+ res.end(payload);
50
+ }
51
+ /**
52
+ * Handle an `/api/*` request. Returns false when the path is not ours, so the
53
+ * caller can fall through to its own 404.
54
+ */
55
+ export function handleApi(req, res, path, deps) {
56
+ if (!path.startsWith("/api/"))
57
+ return false;
58
+ if ((req.method ?? "GET") !== "GET") {
59
+ json(res, 405, { error: "method not allowed" });
60
+ return true;
61
+ }
62
+ const url = new URL(req.url ?? "/", "http://fest.local");
63
+ const params = url.searchParams;
64
+ const range = parseRange(params);
65
+ const scope = deps.scope;
66
+ // SSE, so it owns its own response lifecycle and must not fall through to
67
+ // the JSON writer below.
68
+ if (path === "/api/live") {
69
+ handleLive(req, res, deps.bus);
70
+ return true;
71
+ }
72
+ switch (path) {
73
+ case "/api/overview":
74
+ json(res, 200, {
75
+ range,
76
+ totals: usageTotals(deps.store, scope, range),
77
+ series: usageSeries(deps.store, scope, range),
78
+ byCredentialOrigin: usageByCredentialOrigin(deps.store, scope, range),
79
+ latency: latencySummary(deps.store, scope, range),
80
+ sink: deps.sink.stats(),
81
+ });
82
+ return true;
83
+ case "/api/requests": {
84
+ const filter = {
85
+ ...(strParam(params, "userId") !== undefined ? { userId: strParam(params, "userId") } : {}),
86
+ ...(strParam(params, "model") !== undefined ? { servedModel: strParam(params, "model") } : {}),
87
+ ...(strParam(params, "credentialOrigin") !== undefined
88
+ ? { credentialOrigin: strParam(params, "credentialOrigin") }
89
+ : {}),
90
+ ...(strParam(params, "sessionId") !== undefined ? { sessionId: strParam(params, "sessionId") } : {}),
91
+ ...(params.get("errorsOnly") === "1" ? { errorsOnly: true } : {}),
92
+ ...(intParam(params, "before") !== undefined ? { beforeSeq: intParam(params, "before") } : {}),
93
+ ...(intParam(params, "limit") !== undefined ? { limit: intParam(params, "limit") } : {}),
94
+ };
95
+ json(res, 200, listRequests(deps.store, scope, filter));
96
+ return true;
97
+ }
98
+ case "/api/users":
99
+ json(res, 200, { range, rows: usageByUser(deps.store, scope, range) });
100
+ return true;
101
+ case "/api/models":
102
+ json(res, 200, { range, rows: usageByModel(deps.store, scope, range) });
103
+ return true;
104
+ case "/api/errors":
105
+ json(res, 200, {
106
+ range,
107
+ rows: errorBreakdown(deps.store, scope, range),
108
+ latency: latencySummary(deps.store, scope, range),
109
+ });
110
+ return true;
111
+ // For a subscription developer this, not cost, is the scarce resource.
112
+ // The routing table, so an operator can see what Fest will do without
113
+ // reading the config file on the server. No secret can appear here: the
114
+ // table holds references, not values.
115
+ case "/api/routing":
116
+ json(res, 200, {
117
+ version: deps.routes.version,
118
+ enabled: deps.routes.routes.length > 0,
119
+ upstreams: [...deps.routes.upstreams.values()].map((u) => ({
120
+ id: u.id,
121
+ adapter: u.adapter,
122
+ baseUrl: u.baseUrl,
123
+ credentialSource: u.credential.source,
124
+ credentialPresent: deps.secrets.resolve(u.credential) !== null,
125
+ transforms: ADAPTER_TRANSFORMS[u.adapter] ?? [],
126
+ })),
127
+ // Already in evaluation order, each row carrying what it claims and
128
+ // what it adds — see `routing-view.ts` for why the dashboard is not
129
+ // left to infer either from the pattern.
130
+ routes: explainRoutes(deps.routes).map(({ route, shadows, outranked, menuIds }) => ({
131
+ id: route.id,
132
+ match: route.match,
133
+ upstream: route.upstream,
134
+ model: route.model,
135
+ expose: route.expose,
136
+ shadows,
137
+ outranked,
138
+ menuIds,
139
+ })),
140
+ });
141
+ return true;
142
+ // Admin-only. A member can see the traffic they are part of; who was
143
+ // granted console access, and whose login failed, is not that.
144
+ case "/api/audit": {
145
+ if (scope.role === "member") {
146
+ json(res, 403, { error: "admin access required" });
147
+ return true;
148
+ }
149
+ const opts = {
150
+ ...(intParam(params, "limit") !== undefined ? { limit: intParam(params, "limit") } : {}),
151
+ ...(intParam(params, "before") !== undefined ? { beforeSeq: intParam(params, "before") } : {}),
152
+ };
153
+ json(res, 200, { rows: listAudit(deps.store, scope.orgId, opts) });
154
+ return true;
155
+ }
156
+ case "/api/quota":
157
+ json(res, 200, { rows: latestQuotaByUser(deps.store, scope) });
158
+ return true;
159
+ default:
160
+ json(res, 404, { error: `no such endpoint: ${path}` });
161
+ return true;
162
+ }
163
+ }
164
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../server/api/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The routing table, explained for a reader rather than for the router.
3
+ *
4
+ * A routing file does two quite different things with the same syntax, and
5
+ * conflating them is how an operator misreads their own config:
6
+ *
7
+ * - it **substitutes** — a model the developer already has a name for now
8
+ * comes from somewhere else, on a credential the server holds;
9
+ * - it **adds** — an id that exists nowhere but this gateway appears in the
10
+ * `/model` menu, and choosing it is a deliberate act.
11
+ *
12
+ * The first is the one that needs watching. The second is a feature. A single
13
+ * table of `match → upstream` rows shows both as the same kind of line.
14
+ *
15
+ * Both classifications are derived by ASKING the real resolver and the real
16
+ * menu builder, never by re-reading the pattern here. Pattern-reading would
17
+ * ignore precedence: `claude-*` looks like it claims Opus right up until an
18
+ * exact `claude-opus-5` rule sits above it, and a dashboard that said
19
+ * otherwise would be confidently wrong in exactly the case an operator checks.
20
+ */
21
+ import { claimsModel, evaluationOrder, resolveRoute } from "../routes/resolve.js";
22
+ import { buildModelMenu } from "./models.js";
23
+ import { BASE_MODELS } from "../../shared/base-models.js";
24
+ /** Routes in evaluation order, each with what it claims and what it adds. */
25
+ export function explainRoutes(table) {
26
+ const baseIds = new Set(BASE_MODELS.map((m) => m.id));
27
+ // What the client would actually be shown. An id Fest cannot serve in the key
28
+ // posture is not in here, and must not be reported as a model this gateway
29
+ // adds — the menu is a promise, and so is this screen.
30
+ const menu = new Set(buildModelMenu(table).map((e) => e.id));
31
+ return evaluationOrder(table).map((route) => {
32
+ const shadows = [];
33
+ const outranked = [];
34
+ for (const { id } of BASE_MODELS) {
35
+ if (!claimsModel(route, id))
36
+ continue;
37
+ const winner = resolveRoute(table, id).route;
38
+ if (winner === route)
39
+ shadows.push(id);
40
+ else if (winner !== null)
41
+ outranked.push({ model: id, takenBy: winner.id });
42
+ }
43
+ const menuIds = [route.match.includes("*") ? null : route.match, route.expose].filter((id) => id !== null && !baseIds.has(id) && menu.has(id));
44
+ return { route, shadows, outranked, menuIds };
45
+ });
46
+ }
47
+ //# sourceMappingURL=routing-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-view.js","sourceRoot":"","sources":["../../../server/api/routing-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAoB1D,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtD,8EAA8E;IAC9E,2EAA2E;IAC3E,uDAAuD;IACvD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE7D,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1C,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAyC,EAAE,CAAC;QAC3D,KAAK,MAAM,EAAE,EAAE,EAAE,IAAI,WAAW,EAAE,CAAC;YACjC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,CAAC;gBAAE,SAAS;YACtC,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC;YAC7C,IAAI,MAAM,KAAK,KAAK;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;iBAClC,IAAI,MAAM,KAAK,IAAI;gBAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CACnF,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CACtE,CAAC;QAEF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Dashboard accounts: granting sign-in, and checking a sign-in attempt.
3
+ *
4
+ * A `users` row is an attribution target — `fest token create` makes one for
5
+ * every developer so their usage has somewhere to land. Being able to sign in
6
+ * to the console is a *separate* grant, represented by a non-null
7
+ * `password_hash`. Nobody acquires an admin console account as a side effect of
8
+ * being metered.
9
+ */
10
+ import { ensureUser, findUserByEmail } from "../store/bootstrap.js";
11
+ import { hashPassword, verifyPassword, needsRehash, validatePassword } from "./password.js";
12
+ import { revokeUserSessions } from "./session.js";
13
+ /** Normalised so `Ada@Corp.test` and `ada@corp.test` are the same account. */
14
+ export const normaliseEmail = (email) => email.trim().toLowerCase();
15
+ /**
16
+ * Is this deployment claimed?
17
+ *
18
+ * Deliberately not "has a password": an owner claimed via OAuth (see
19
+ * `server/api/oauth.ts`) never gets a `password_hash` at all, and the whole
20
+ * point of that self-service path is to satisfy this same check — so tying it
21
+ * to password auth specifically would leave Fest refusing to bind on any
22
+ * non-loopback interface even after someone had legitimately claimed it.
23
+ * Ownership is a role, not an auth method.
24
+ */
25
+ export function hasAnyOwner(store, orgId) {
26
+ const row = store.db
27
+ .prepare(`SELECT 1 AS present FROM users
28
+ WHERE org_id = ? AND role = 'owner' AND disabled_at IS NULL
29
+ LIMIT 1`)
30
+ .get(orgId);
31
+ return row !== undefined;
32
+ }
33
+ export function countDashboardAccounts(store, orgId) {
34
+ const row = store.db
35
+ .prepare(`SELECT COUNT(*) AS n FROM users WHERE org_id = ? AND password_hash IS NOT NULL`)
36
+ .get(orgId);
37
+ return Number(row?.n ?? 0);
38
+ }
39
+ /**
40
+ * Grant or re-grant console access. Idempotent on (org, email).
41
+ *
42
+ * Every password change revokes that user's existing sessions. A password is
43
+ * changed because it may be known to someone else; leaving their cookies
44
+ * working would make the change ceremonial.
45
+ */
46
+ export async function grantDashboardAccess(store, args) {
47
+ const problem = validatePassword(args.password);
48
+ if (problem !== null)
49
+ throw new Error(problem);
50
+ const email = normaliseEmail(args.email);
51
+ const user = ensureUser(store, {
52
+ orgId: args.orgId,
53
+ email,
54
+ role: args.role ?? "admin",
55
+ ...(args.displayName === undefined ? {} : { displayName: args.displayName }),
56
+ });
57
+ const encoded = await hashPassword(args.password);
58
+ store.db
59
+ .prepare(`UPDATE users SET password_hash = ?, password_set_at = ?, role = ? WHERE id = ?`)
60
+ .run(encoded, Date.now(), args.role ?? user.role, user.id);
61
+ revokeUserSessions(store, user.id);
62
+ return { ...user, role: args.role ?? user.role };
63
+ }
64
+ /**
65
+ * Check an email/password pair.
66
+ *
67
+ * A miss still runs one scrypt over a dummy hash. Without it, "no such account"
68
+ * returns in microseconds while a real account takes ~100ms, which enumerates
69
+ * valid emails from a stopwatch.
70
+ */
71
+ export async function authenticate(store, orgId, email, password) {
72
+ const row = store.db
73
+ .prepare(`SELECT id, org_id, email, role, password_hash, disabled_at
74
+ FROM users WHERE org_id = ? AND email = ?`)
75
+ .get(orgId, normaliseEmail(email));
76
+ if (row === undefined || row.password_hash === null) {
77
+ await verifyPassword(password, await timingEqualiser());
78
+ return { ok: false, user: null, reason: row === undefined ? "no_such_account" : "no_password" };
79
+ }
80
+ if (row.disabled_at !== null) {
81
+ await verifyPassword(password, await timingEqualiser());
82
+ return { ok: false, user: null, reason: "disabled" };
83
+ }
84
+ const ok = await verifyPassword(password, row.password_hash);
85
+ if (!ok)
86
+ return { ok: false, user: null, reason: "bad_password" };
87
+ // Opportunistic upgrade: the only moment the plaintext is available with the
88
+ // user's consent is a successful login.
89
+ if (needsRehash(row.password_hash)) {
90
+ const upgraded = await hashPassword(password);
91
+ store.db.prepare(`UPDATE users SET password_hash = ? WHERE id = ?`).run(upgraded, row.id);
92
+ }
93
+ return {
94
+ ok: true,
95
+ user: { id: row.id, orgId: row.org_id, email: row.email, role: row.role },
96
+ reason: "ok",
97
+ };
98
+ }
99
+ /**
100
+ * A real, valid encoding of a password nobody knows, so the miss path costs the
101
+ * same as the hit path. Computed on first use rather than at import, so the
102
+ * ~100ms scrypt lands on the first login attempt instead of on every CLI
103
+ * invocation that happens to import this module.
104
+ */
105
+ let dummyHash = null;
106
+ function timingEqualiser() {
107
+ dummyHash ??= hashPassword("fest-dummy-password-for-timing-equalisation");
108
+ return dummyHash;
109
+ }
110
+ export { findUserByEmail };
111
+ //# sourceMappingURL=accounts.js.map