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,109 @@
1
+ /**
2
+ * Configuration, entirely from the environment.
3
+ *
4
+ * Validated once at boot and printed as a redacted table, so a
5
+ * misconfiguration fails loudly at startup rather than lazily on the first
6
+ * developer's request.
7
+ */
8
+ /**
9
+ * `/fest/`, `fest`, `//fest//` -> `/fest`; `/`, `""` and anything that is only
10
+ * slashes -> `""`. One leading slash, no trailing one, so callers can always
11
+ * write `${basePath}/rest` and `path.startsWith(basePath)` without a special
12
+ * case for the root deployment.
13
+ */
14
+ export function normalizeBasePath(raw) {
15
+ const trimmed = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
16
+ return trimmed === "" ? "" : `/${trimmed}`;
17
+ }
18
+ function basePathFrom(explicit, publicUrl) {
19
+ if (explicit !== null)
20
+ return normalizeBasePath(explicit);
21
+ try {
22
+ return normalizeBasePath(new URL(publicUrl).pathname);
23
+ }
24
+ catch {
25
+ // publicUrl is validated elsewhere; an unparseable one means no prefix
26
+ // rather than a boot failure in a code path about cosmetics.
27
+ return "";
28
+ }
29
+ }
30
+ function intFromEnv(name, fallback) {
31
+ const raw = process.env[name];
32
+ if (raw === undefined || raw.trim() === "")
33
+ return fallback;
34
+ const n = Number(raw);
35
+ if (!Number.isInteger(n) || n < 0) {
36
+ throw new Error(`${name} must be a non-negative integer, got ${JSON.stringify(raw)}`);
37
+ }
38
+ return n;
39
+ }
40
+ function boolFromEnv(name, fallback) {
41
+ const raw = process.env[name];
42
+ if (raw === undefined || raw.trim() === "")
43
+ return fallback;
44
+ const v = raw.trim().toLowerCase();
45
+ if (["1", "true", "yes", "on"].includes(v))
46
+ return true;
47
+ if (["0", "false", "no", "off"].includes(v))
48
+ return false;
49
+ throw new Error(`${name} must be a boolean, got ${JSON.stringify(raw)}`);
50
+ }
51
+ function stringFromEnv(name) {
52
+ const raw = process.env[name];
53
+ if (raw === undefined)
54
+ return null;
55
+ const trimmed = raw.trim();
56
+ return trimmed === "" ? null : trimmed;
57
+ }
58
+ function listFromEnv(name) {
59
+ const raw = process.env[name];
60
+ if (raw === undefined)
61
+ return [];
62
+ return raw
63
+ .split(",")
64
+ .map((s) => s.trim().toLowerCase())
65
+ .filter((s) => s !== "");
66
+ }
67
+ export function loadConfig() {
68
+ const level = (process.env.FEST_LOG_LEVEL ?? "info").trim();
69
+ if (!["debug", "info", "warn", "error"].includes(level)) {
70
+ throw new Error(`FEST_LOG_LEVEL must be debug|info|warn|error, got ${JSON.stringify(level)}`);
71
+ }
72
+ const upstream = (process.env.FEST_UPSTREAM_BASE_URL ?? "https://api.anthropic.com").trim();
73
+ try {
74
+ new URL(upstream);
75
+ }
76
+ catch {
77
+ throw new Error(`FEST_UPSTREAM_BASE_URL is not a valid URL: ${JSON.stringify(upstream)}`);
78
+ }
79
+ const port = intFromEnv("FEST_PORT", 8787);
80
+ const host = (process.env.FEST_HOST ?? "127.0.0.1").trim();
81
+ const publicUrl = (stringFromEnv("FEST_PUBLIC_URL") ?? `http://${host}:${port}`).replace(/\/+$/, "");
82
+ return {
83
+ port,
84
+ host,
85
+ upstreamBaseUrl: upstream.replace(/\/+$/, ""),
86
+ usageLogPath: (process.env.FEST_USAGE_LOG ?? "./data/usage.jsonl").trim(),
87
+ dbPath: (process.env.FEST_DB ?? "./data/fest.db").trim(),
88
+ logLevel: level,
89
+ requireIdentity: boolFromEnv("FEST_REQUIRE_IDENTITY", false),
90
+ routesPath: (process.env.FEST_ROUTES ?? "").trim() || null,
91
+ secureCookies: boolFromEnv("FEST_SECURE_COOKIES", false),
92
+ publicUrl,
93
+ basePath: basePathFrom(stringFromEnv("FEST_BASE_PATH"), publicUrl),
94
+ googleClientId: stringFromEnv("FEST_GOOGLE_CLIENT_ID"),
95
+ googleClientSecret: stringFromEnv("FEST_GOOGLE_CLIENT_SECRET"),
96
+ githubClientId: stringFromEnv("FEST_GITHUB_CLIENT_ID"),
97
+ githubClientSecret: stringFromEnv("FEST_GITHUB_CLIENT_SECRET"),
98
+ allowedEmailDomains: listFromEnv("FEST_ALLOWED_EMAIL_DOMAINS"),
99
+ };
100
+ }
101
+ /** Safe to log: secrets are redacted, never spread through verbatim. */
102
+ export function describeConfig(cfg) {
103
+ return {
104
+ ...cfg,
105
+ googleClientSecret: cfg.googleClientSecret === null ? null : "[redacted]",
106
+ githubClientSecret: cfg.githubClientSecret === null ? null : "[redacted]",
107
+ };
108
+ }
109
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../server/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Secret references, and the one place they are turned into values.
3
+ *
4
+ * A credential is configured as a REFERENCE — `{env:FIREWORKS_API_KEY}` — not
5
+ * as a literal. Three things follow from that, and all three are the reason it
6
+ * is worth the indirection:
7
+ *
8
+ * 1. The routing config can be committed, diffed and pasted into an issue.
9
+ * 2. The database holds no secrets by default, so a database dump is not a
10
+ * credential dump.
11
+ * 3. Everything Fest records about a credential is the reference, never the
12
+ * value. `source: "env:FIREWORKS_API_KEY"` is meaningful to an operator and
13
+ * useless to an attacker.
14
+ *
15
+ * The resolved value is wrapped in `NonPersistable` so it cannot be logged,
16
+ * serialised or stored by accident — see `secret/non-persistable.ts`.
17
+ */
18
+ import { NonPersistable } from "../secret/non-persistable.js";
19
+ /** `{env:NAME}` — the only reference form supported today. */
20
+ const ENV_REF = /^\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/;
21
+ export function parseSecretRef(raw) {
22
+ const match = ENV_REF.exec(raw.trim());
23
+ if (match === null)
24
+ return null;
25
+ const name = match[1];
26
+ return { kind: "env", name, source: `env:${name}` };
27
+ }
28
+ /**
29
+ * Describe why a raw string is not a usable reference.
30
+ *
31
+ * Split from `parseSecretRef` so config validation can explain itself. The
32
+ * common mistake is pasting the secret itself, and that deserves a message
33
+ * saying so rather than a generic parse failure — otherwise the fix people
34
+ * reach for is "quote it differently", and the key ends up in git.
35
+ */
36
+ export function explainSecretRef(raw) {
37
+ const value = raw.trim();
38
+ if (ENV_REF.test(value))
39
+ return null;
40
+ if (value === "")
41
+ return "credential is empty; expected {env:NAME}";
42
+ if (!value.startsWith("{")) {
43
+ return (`credential must be a reference like {env:NAME}, not a literal value. ` +
44
+ `Put the secret in an environment variable and reference it, so it stays out of config and out of the database.`);
45
+ }
46
+ return `credential ${JSON.stringify(value)} is not a valid reference; expected {env:NAME}`;
47
+ }
48
+ /**
49
+ * Resolve against the process environment.
50
+ *
51
+ * Reads on every call rather than snapshotting at boot: an operator rotating a
52
+ * key should not have to reason about whether Fest cached the old one. The read
53
+ * is a property lookup, so the cost is irrelevant next to a network round trip.
54
+ *
55
+ * An empty or whitespace-only variable counts as MISSING, not as a credential.
56
+ * `FIREWORKS_API_KEY=` in a .env file is overwhelmingly a mistake, and treating
57
+ * it as a real value turns a clear "not configured" into a confusing 401 from a
58
+ * provider.
59
+ */
60
+ export function createEnvResolver(env = process.env) {
61
+ return {
62
+ resolve(ref) {
63
+ const raw = env[ref.name];
64
+ if (raw === undefined || raw.trim() === "")
65
+ return null;
66
+ return { source: ref.source, value: new NonPersistable(raw.trim()) };
67
+ },
68
+ };
69
+ }
70
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../server/credentials/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,8DAA8D;AAC9D,MAAM,OAAO,GAAG,oCAAoC,CAAC;AASrD,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAW,CAAC;IAChC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;AACtD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,0CAA0C,CAAC;IACpE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,CACL,uEAAuE;YACvE,gHAAgH,CACjH,CAAC;IACJ,CAAC;IACD,OAAO,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,gDAAgD,CAAC;AAC7F,CAAC;AAYD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACpE,OAAO;QACL,OAAO,CAAC,GAAc;YACpB,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC1B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,OAAO,IAAI,CAAC;YACxD,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACvE,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Decide which credential serves a request, and record every candidate.
3
+ *
4
+ * The output is as much an audit trail as a decision. `credentialsConsidered`
5
+ * goes onto every usage record because silent credential substitution is a
6
+ * billing incident: the developer believes their subscription paid, the org is
7
+ * invoiced, and without this list nothing anywhere records that a choice was
8
+ * even made.
9
+ *
10
+ * Two rules this module exists to make structural rather than customary:
11
+ *
12
+ * 1. **An inbound subscription bearer is never used for a substitute route.**
13
+ * It was issued by Anthropic for Anthropic. Forwarding it to Fireworks
14
+ * would disclose a developer's personal credential to a third party. It is
15
+ * recorded as `skipped`, with the reason, so the decision is visible.
16
+ *
17
+ * 2. **A missing server credential fails the request.** It does not quietly
18
+ * fall back to the pass-through path. Falling back would silently serve a
19
+ * different model than the one asked for, on a credential the requester did
20
+ * not choose — the request succeeding is precisely what makes that bad.
21
+ */
22
+ export function resolveCredential(decision, inbound, secrets) {
23
+ const considered = [];
24
+ // ── pass-through ────────────────────────────────────────────────────────────
25
+ if (decision.pipeline === "passthrough" || decision.upstream === null) {
26
+ if (inbound === null) {
27
+ considered.push({
28
+ source: "inbound",
29
+ result: "missing",
30
+ reason: "no credential on the request",
31
+ });
32
+ // Not an error here: upstream decides. Anthropic's own 401 is a better
33
+ // message than anything Fest could invent, and the client's refresh
34
+ // latch is waiting for exactly that response.
35
+ return { ok: true, secret: null, source: "inbound", considered };
36
+ }
37
+ considered.push({
38
+ source: inbound.isSubscription ? "inbound_subscription" : "inbound_key",
39
+ result: "used",
40
+ });
41
+ return { ok: true, secret: null, source: considered[0]?.source ?? "inbound", considered };
42
+ }
43
+ // ── substitute ──────────────────────────────────────────────────────────────
44
+ const upstream = decision.upstream;
45
+ if (inbound !== null && inbound.isSubscription) {
46
+ considered.push({
47
+ source: "inbound_subscription",
48
+ result: "skipped",
49
+ reason: `not sent to ${upstream.id}: a Claude subscription credential is only valid at Anthropic, and forwarding it would disclose it to another vendor`,
50
+ });
51
+ }
52
+ else if (inbound !== null) {
53
+ considered.push({
54
+ source: "inbound_key",
55
+ result: "skipped",
56
+ reason: `not sent to ${upstream.id}: an inbound key is scoped to its own issuer`,
57
+ });
58
+ }
59
+ const resolvedSecret = secrets.resolve(upstream.credential);
60
+ if (resolvedSecret === null) {
61
+ considered.push({
62
+ source: upstream.credential.source,
63
+ result: "missing",
64
+ reason: "environment variable is unset or empty",
65
+ });
66
+ const model = JSON.stringify(decision.requestedModel ?? "?");
67
+ // No route means no route to name: this is the key-posture default for
68
+ // Anthropic models, not an entry in the file. Naming a route id of "?"
69
+ // sends the operator looking through routes.json for something that was
70
+ // never there.
71
+ const sends = decision.route === null
72
+ ? `Fest: ${model} defaults to upstream ${JSON.stringify(upstream.id)}`
73
+ : `Fest: route ${JSON.stringify(decision.route.id)} sends ${model} to upstream ${JSON.stringify(upstream.id)}`;
74
+ return {
75
+ ok: false,
76
+ considered,
77
+ message: `${sends}, whose credential ${upstream.credential.source} is not set on the Fest server. ` +
78
+ `This request was refused rather than served on a different credential.`,
79
+ };
80
+ }
81
+ considered.push({ source: resolvedSecret.source, result: "used" });
82
+ return {
83
+ ok: true,
84
+ secret: resolvedSecret.value,
85
+ source: resolvedSecret.source,
86
+ considered,
87
+ };
88
+ }
89
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/credentials/resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Request body handling for a byte-for-byte proxy.
3
+ *
4
+ * The body is read as opaque bytes and forwarded unchanged. It is never
5
+ * `JSON.parse`d and re-serialised, because a round-trip changes key order,
6
+ * whitespace, unicode escaping and number formatting — and on the subscription
7
+ * path Anthropic validates the request shape. Phase 0 showed the body even
8
+ * carries its own attribution block inside `system[0].text`
9
+ * (`x-anthropic-billing-header: cc_version=…`), so the payload is not ours to
10
+ * normalise.
11
+ *
12
+ * To route or report on a request we still need to know the model, so we parse
13
+ * a *copy* for inspection only. `peekRequest` is read-only by construction: it
14
+ * returns primitives and never hands back the parsed object.
15
+ */
16
+ /** Read the full body. Bodies are prompts: large, but bounded. */
17
+ export async function readBodyBytes(req, maxBytes) {
18
+ const chunks = [];
19
+ let total = 0;
20
+ for await (const chunk of req) {
21
+ total += chunk.byteLength;
22
+ if (total > maxBytes)
23
+ return { tooLarge: true };
24
+ chunks.push(chunk);
25
+ }
26
+ // Assembled into a Uint8Array backed by a plain ArrayBuffer, which is what
27
+ // `fetch` accepts as a BodyInit. `Buffer.concat` would copy just the same,
28
+ // so this costs nothing extra and avoids an unsound cast at the fetch call —
29
+ // where a mistake would mean sending the wrong bytes on a path whose whole
30
+ // contract is that the bytes are unchanged.
31
+ const out = new Uint8Array(total);
32
+ let offset = 0;
33
+ for (const chunk of chunks) {
34
+ out.set(chunk, offset);
35
+ offset += chunk.byteLength;
36
+ }
37
+ return out;
38
+ }
39
+ export function isTooLarge(v) {
40
+ return !(v instanceof Uint8Array);
41
+ }
42
+ /**
43
+ * Inspect a body copy for the few fields Fest needs. Tolerant by design: a
44
+ * body we cannot parse is still forwarded verbatim, we just report less about
45
+ * it. Failing a request because our own telemetry could not read it would be
46
+ * the wrong trade.
47
+ */
48
+ export function peekRequest(bytes) {
49
+ try {
50
+ const parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
51
+ if (parsed === null || typeof parsed !== "object") {
52
+ return { model: null, stream: false, maxTokens: null };
53
+ }
54
+ const obj = parsed;
55
+ const maxTokens = obj["max_tokens"];
56
+ return {
57
+ model: typeof obj["model"] === "string" ? obj["model"] : null,
58
+ stream: obj["stream"] === true,
59
+ maxTokens: typeof maxTokens === "number" && Number.isFinite(maxTokens) ? maxTokens : null,
60
+ };
61
+ }
62
+ catch {
63
+ return { model: null, stream: false, maxTokens: null };
64
+ }
65
+ }
66
+ //# sourceMappingURL=body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../../../server/http/body.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAoB,EACpB,QAAgB;IAEhB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAA4B,EAAE,CAAC;QACvD,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;QAC1B,IAAI,KAAK,GAAG,QAAQ;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAChD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,6EAA6E;IAC7E,2EAA2E;IAC3E,4CAA4C;IAC5C,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAyC;IAClE,OAAO,CAAC,CAAC,CAAC,YAAY,UAAU,CAAC,CAAC;AACpC,CAAC;AAaD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACxE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QACzD,CAAC;QACD,MAAM,GAAG,GAAG,MAAiC,CAAC;QAC9C,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;QACpC,OAAO;YACL,KAAK,EAAE,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;YAC7D,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI;YAC9B,SAAS,EAAE,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;SAC1F,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC;AACH,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Anthropic-shaped error responses.
3
+ *
4
+ * Fest's errors are rendered by a client it does not control, so the wire shape
5
+ * matters twice over: Claude Code parses `error.type` to decide whether to
6
+ * retry, and prints `error.message` verbatim to the developer. That message IS
7
+ * the user-facing UI — write it as something a developer can act on ("Fest: no
8
+ * identity token; set ANTHROPIC_BASE_URL to your /t/<token> URL"), not as an
9
+ * internal condition.
10
+ */
11
+ import { redactSecrets } from "../secret/fingerprint.js";
12
+ /** The exact envelope Anthropic uses, so the client's own parser accepts it. */
13
+ export function anthropicError(type, message) {
14
+ return JSON.stringify({ type: "error", error: { type, message } });
15
+ }
16
+ /**
17
+ * Map an error type to its HTTP status.
18
+ *
19
+ * Learned the hard way: Claude Code retries 5xx aggressively — a single 501
20
+ * produced ten retries of one request, i.e. ten times the upstream load and a
21
+ * developer watching a hang. Any condition that will NOT improve on retry
22
+ * (missing/unknown identity token, unroutable model, malformed body) must be
23
+ * given a 4xx type. Reserve `api_error`/`overloaded_error` for genuinely
24
+ * transient failures where a retry is the correct client behaviour.
25
+ */
26
+ export function statusForErrorType(type) {
27
+ switch (type) {
28
+ case "invalid_request_error":
29
+ return 400;
30
+ case "authentication_error":
31
+ return 401;
32
+ case "permission_error":
33
+ return 403;
34
+ case "not_found_error":
35
+ return 404;
36
+ case "rate_limit_error":
37
+ return 429;
38
+ case "api_error":
39
+ return 500;
40
+ case "overloaded_error":
41
+ return 529;
42
+ }
43
+ }
44
+ /**
45
+ * How much of an upstream's error message to keep.
46
+ *
47
+ * Longer than the 200 chars used for exception strings, deliberately. An
48
+ * exception string is self-describing by its first clause; a provider's
49
+ * validation error is not — the actionable part is a field path that can sit
50
+ * well into the text ("context_management.edits.0: Extra inputs are not
51
+ * permitted"), and a provider may echo a chunk of the offending request before
52
+ * saying what was wrong with it. Truncating to 200 reliably cut the answer off.
53
+ */
54
+ const MAX_UPSTREAM_MESSAGE = 500;
55
+ /**
56
+ * Best-effort type for an upstream that sent no parseable error envelope.
57
+ *
58
+ * Derived from the status so `error_type` is never null on a failed request:
59
+ * a row that knows only "400" is barely more useful than no row, and the
60
+ * dashboard already groups by this column.
61
+ */
62
+ function typeForStatus(status) {
63
+ switch (status) {
64
+ case 400:
65
+ return "invalid_request_error";
66
+ case 401:
67
+ return "authentication_error";
68
+ case 403:
69
+ return "permission_error";
70
+ case 404:
71
+ return "not_found_error";
72
+ case 429:
73
+ return "rate_limit_error";
74
+ case 529:
75
+ return "overloaded_error";
76
+ default:
77
+ return status >= 500 ? "api_error" : "invalid_request_error";
78
+ }
79
+ }
80
+ function firstString(...candidates) {
81
+ for (const c of candidates) {
82
+ if (typeof c === "string" && c.trim() !== "")
83
+ return c.trim();
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Read a failed upstream response into something worth storing.
89
+ *
90
+ * Fest relayed these bodies to the client and recorded nothing but the status,
91
+ * so the one field a developer needs — WHY the provider refused — existed only
92
+ * in their terminal, and only until it scrolled. Every non-2xx landed in the
93
+ * database with `error_type` and `error_message` null.
94
+ *
95
+ * Shapes handled: Anthropic and Fireworks both nest `{ error: { type, message } }`;
96
+ * some gateways flatten it, or send `error` as a bare string, or send no JSON at
97
+ * all. An unparseable body is not a failure to report — the raw text is the
98
+ * message, truncated. The result is always populated.
99
+ */
100
+ export function describeUpstreamFailure(status, text) {
101
+ const fallbackType = typeForStatus(status);
102
+ let type = null;
103
+ let message = null;
104
+ try {
105
+ const parsed = JSON.parse(text);
106
+ if (typeof parsed === "object" && parsed !== null) {
107
+ const top = parsed;
108
+ const err = top["error"];
109
+ if (typeof err === "object" && err !== null) {
110
+ const e = err;
111
+ type = firstString(e["type"], e["code"]);
112
+ message = firstString(e["message"], e["detail"]);
113
+ }
114
+ else {
115
+ message = firstString(err);
116
+ }
117
+ type ??= firstString(top["type"] === "error" ? null : top["type"], top["code"]);
118
+ message ??= firstString(top["message"], top["detail"]);
119
+ }
120
+ }
121
+ catch {
122
+ // Not JSON. The body itself is the most informative thing we have.
123
+ }
124
+ return {
125
+ text,
126
+ type: type ?? fallbackType,
127
+ // Redacted on the way in, not on the way out: this string is about to be
128
+ // persisted, and an upstream that echoes the offending request back could
129
+ // echo a credential header with it.
130
+ message: redactSecrets(message ?? text).slice(0, MAX_UPSTREAM_MESSAGE),
131
+ };
132
+ }
133
+ /**
134
+ * A complete SSE `error` frame, terminated by the blank line.
135
+ *
136
+ * Used when an upstream dies mid-stream: the status line and headers are long
137
+ * gone, so the only way left to tell the client anything is an in-band error
138
+ * event. Note there is no status to choose here, which is another reason to
139
+ * fail fast with a 4xx before the first byte whenever that is possible.
140
+ */
141
+ export function sseErrorEvent(type, message) {
142
+ return `event: error\ndata: ${anthropicError(type, message)}\n\n`;
143
+ }
144
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../server/http/errors.ts"],"names":[],"mappings":"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"}