llm-cli-gateway 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -9
- package/README.md +3 -1
- package/dist/auth.d.ts +44 -1
- package/dist/auth.js +60 -13
- package/dist/config.d.ts +19 -0
- package/dist/config.js +235 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +22 -11
- package/dist/executor.js +17 -21
- package/dist/flight-recorder.d.ts +2 -1
- package/dist/http-transport.js +74 -12
- package/dist/index.d.ts +42 -7
- package/dist/index.js +1161 -82
- package/dist/metrics.d.ts +3 -3
- package/dist/metrics.js +8 -8
- package/dist/oauth.d.ts +38 -0
- package/dist/oauth.js +441 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +8 -0
- package/dist/request-helpers.d.ts +8 -8
- package/dist/resources.js +56 -7
- package/dist/session-manager-pg.d.ts +6 -6
- package/dist/session-manager-pg.js +1 -0
- package/dist/session-manager.d.ts +16 -12
- package/dist/session-manager.js +4 -1
- package/dist/upstream-contracts.d.ts +84 -0
- package/dist/upstream-contracts.js +714 -6
- package/dist/workspace-registry.d.ts +63 -0
- package/dist/workspace-registry.js +417 -0
- package/dist/xai-api-provider.d.ts +43 -0
- package/dist/xai-api-provider.js +191 -0
- package/migrations/001_initial_schema.sql +65 -0
- package/migrations/002_session_ids_as_text.sql +26 -0
- package/migrations/003_provider_type_sessions.sql +20 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/setup/status.schema.json +42 -1
package/dist/metrics.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ProviderType } from "./session-manager.js";
|
|
2
2
|
export interface ToolMetricsSnapshot {
|
|
3
3
|
requestCount: number;
|
|
4
4
|
successCount: number;
|
|
@@ -13,11 +13,11 @@ export interface PerformanceMetricsSnapshot {
|
|
|
13
13
|
totalFailures: number;
|
|
14
14
|
overallSuccessRate: number;
|
|
15
15
|
overallFailureRate: number;
|
|
16
|
-
byTool: Record<
|
|
16
|
+
byTool: Record<ProviderType, ToolMetricsSnapshot>;
|
|
17
17
|
generatedAt: string;
|
|
18
18
|
}
|
|
19
19
|
export declare class PerformanceMetrics {
|
|
20
20
|
private metrics;
|
|
21
|
-
recordRequest(
|
|
21
|
+
recordRequest(provider: ProviderType, durationMs: number, success: boolean): void;
|
|
22
22
|
snapshot(): PerformanceMetricsSnapshot;
|
|
23
23
|
}
|
package/dist/metrics.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const createEmptyMetrics = () => Object.fromEntries(
|
|
3
|
-
|
|
1
|
+
import { PROVIDER_TYPES } from "./session-manager.js";
|
|
2
|
+
const createEmptyMetrics = () => Object.fromEntries(PROVIDER_TYPES.map(provider => [
|
|
3
|
+
provider,
|
|
4
4
|
{ requestCount: 0, successCount: 0, failureCount: 0, totalResponseTimeMs: 0 },
|
|
5
5
|
]));
|
|
6
6
|
export class PerformanceMetrics {
|
|
7
7
|
metrics = createEmptyMetrics();
|
|
8
|
-
recordRequest(
|
|
9
|
-
const metrics = this.metrics[
|
|
8
|
+
recordRequest(provider, durationMs, success) {
|
|
9
|
+
const metrics = this.metrics[provider];
|
|
10
10
|
metrics.requestCount += 1;
|
|
11
11
|
const normalizedDurationMs = Number.isFinite(durationMs) ? Math.max(0, durationMs) : 0;
|
|
12
12
|
metrics.totalResponseTimeMs += normalizedDurationMs;
|
|
@@ -22,12 +22,12 @@ export class PerformanceMetrics {
|
|
|
22
22
|
let totalRequests = 0;
|
|
23
23
|
let totalSuccesses = 0;
|
|
24
24
|
let totalFailures = 0;
|
|
25
|
-
for (const
|
|
26
|
-
const metrics = this.metrics[
|
|
25
|
+
for (const provider of PROVIDER_TYPES) {
|
|
26
|
+
const metrics = this.metrics[provider];
|
|
27
27
|
const averageResponseTimeMs = metrics.requestCount > 0 ? metrics.totalResponseTimeMs / metrics.requestCount : 0;
|
|
28
28
|
const successRate = metrics.requestCount > 0 ? metrics.successCount / metrics.requestCount : 0;
|
|
29
29
|
const failureRate = metrics.requestCount > 0 ? metrics.failureCount / metrics.requestCount : 0;
|
|
30
|
-
byTool[
|
|
30
|
+
byTool[provider] = {
|
|
31
31
|
requestCount: metrics.requestCount,
|
|
32
32
|
successCount: metrics.successCount,
|
|
33
33
|
failureCount: metrics.failureCount,
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Logger } from "./logger.js";
|
|
3
|
+
import { type RemoteOAuthConfig } from "./auth.js";
|
|
4
|
+
export interface OAuthServerOptions {
|
|
5
|
+
protectedPath: string;
|
|
6
|
+
config: RemoteOAuthConfig;
|
|
7
|
+
logger?: Logger;
|
|
8
|
+
}
|
|
9
|
+
export interface OAuthRequestContext {
|
|
10
|
+
req: IncomingMessage;
|
|
11
|
+
res: ServerResponse;
|
|
12
|
+
url: URL;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const OAUTH_CODE_TTL_MS: number;
|
|
16
|
+
export declare function generateSecret(bytes?: number): string;
|
|
17
|
+
export declare function hashSecret(secret: string): string;
|
|
18
|
+
export declare function isSecretHash(value: string): boolean;
|
|
19
|
+
export declare function verifySecret(secret: string, encodedHash: string): boolean;
|
|
20
|
+
export declare function redactSecret(value: string | null | undefined): string | null;
|
|
21
|
+
export declare function isLocalHost(host: string): boolean;
|
|
22
|
+
export declare function oauthBaseUrlFromRequest(req: IncomingMessage, config: RemoteOAuthConfig): string | null;
|
|
23
|
+
export declare class OAuthServer {
|
|
24
|
+
private readonly opts;
|
|
25
|
+
private readonly codes;
|
|
26
|
+
private readonly clients;
|
|
27
|
+
constructor(opts: OAuthServerOptions);
|
|
28
|
+
resourceMetadataUrl(baseUrl: string): string;
|
|
29
|
+
isOAuthPath(pathname: string): boolean;
|
|
30
|
+
handle(ctx: OAuthRequestContext): Promise<boolean>;
|
|
31
|
+
private protectedResourceMetadata;
|
|
32
|
+
private authorizationServerMetadata;
|
|
33
|
+
private registrationAllowedByPolicy;
|
|
34
|
+
private handleRegister;
|
|
35
|
+
private handleAuthorize;
|
|
36
|
+
private handleToken;
|
|
37
|
+
private pruneExpiredCodes;
|
|
38
|
+
}
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { URLSearchParams } from "node:url";
|
|
3
|
+
import { issueOAuthAccessToken, timingSafeStringEqual, } from "./auth.js";
|
|
4
|
+
export const OAUTH_CODE_TTL_MS = 5 * 60 * 1000;
|
|
5
|
+
const GENERATED_SECRET_BYTES = 32;
|
|
6
|
+
const SCRYPT_N = 32768;
|
|
7
|
+
const SCRYPT_R = 8;
|
|
8
|
+
const SCRYPT_P = 1;
|
|
9
|
+
const SCRYPT_KEYLEN = 32;
|
|
10
|
+
const SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
11
|
+
export function generateSecret(bytes = GENERATED_SECRET_BYTES) {
|
|
12
|
+
return randomBytes(bytes).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
export function hashSecret(secret) {
|
|
15
|
+
const salt = randomBytes(16);
|
|
16
|
+
const hash = scryptSync(secret, salt, SCRYPT_KEYLEN, {
|
|
17
|
+
N: SCRYPT_N,
|
|
18
|
+
r: SCRYPT_R,
|
|
19
|
+
p: SCRYPT_P,
|
|
20
|
+
maxmem: SCRYPT_MAXMEM,
|
|
21
|
+
});
|
|
22
|
+
return `scrypt:N=${SCRYPT_N},r=${SCRYPT_R},p=${SCRYPT_P}:${salt.toString("base64url")}:${hash.toString("base64url")}`;
|
|
23
|
+
}
|
|
24
|
+
export function isSecretHash(value) {
|
|
25
|
+
return /^scrypt:N=\d+,r=\d+,p=\d+:[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$/.test(value);
|
|
26
|
+
}
|
|
27
|
+
export function verifySecret(secret, encodedHash) {
|
|
28
|
+
const parts = encodedHash.split(":");
|
|
29
|
+
if (parts.length !== 4 || parts[0] !== "scrypt")
|
|
30
|
+
return false;
|
|
31
|
+
const params = Object.fromEntries(parts[1].split(",").map(entry => {
|
|
32
|
+
const [key, value] = entry.split("=");
|
|
33
|
+
return [key, Number(value)];
|
|
34
|
+
}));
|
|
35
|
+
if (!params.N || !params.r || !params.p)
|
|
36
|
+
return false;
|
|
37
|
+
const salt = Buffer.from(parts[2], "base64url");
|
|
38
|
+
const expected = Buffer.from(parts[3], "base64url");
|
|
39
|
+
const actual = scryptSync(secret, salt, expected.length, {
|
|
40
|
+
N: params.N,
|
|
41
|
+
r: params.r,
|
|
42
|
+
p: params.p,
|
|
43
|
+
maxmem: SCRYPT_MAXMEM,
|
|
44
|
+
});
|
|
45
|
+
if (actual.length !== expected.length)
|
|
46
|
+
return false;
|
|
47
|
+
return timingSafeEqual(actual, expected);
|
|
48
|
+
}
|
|
49
|
+
export function redactSecret(value) {
|
|
50
|
+
return value ? "<redacted>" : null;
|
|
51
|
+
}
|
|
52
|
+
function firstHeader(value) {
|
|
53
|
+
return Array.isArray(value) ? value[0] : value;
|
|
54
|
+
}
|
|
55
|
+
function methodNotAllowed(res) {
|
|
56
|
+
res.writeHead(405, { allow: "GET, POST", "content-type": "application/json" });
|
|
57
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
58
|
+
}
|
|
59
|
+
function jsonResponse(res, status, body) {
|
|
60
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
61
|
+
res.end(JSON.stringify(body));
|
|
62
|
+
}
|
|
63
|
+
function isHttpsOrLoopbackUrl(value) {
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(value);
|
|
66
|
+
if (url.protocol === "https:")
|
|
67
|
+
return true;
|
|
68
|
+
if (url.protocol !== "http:")
|
|
69
|
+
return false;
|
|
70
|
+
return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function isLocalHost(host) {
|
|
77
|
+
const hostname = host.split(":")[0]?.toLowerCase() ?? "";
|
|
78
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
79
|
+
}
|
|
80
|
+
export function oauthBaseUrlFromRequest(req, config) {
|
|
81
|
+
if (config.issuer && config.issuer !== "auto") {
|
|
82
|
+
try {
|
|
83
|
+
return new URL(config.issuer).origin;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const configured = process.env.LLM_GATEWAY_PUBLIC_URL;
|
|
90
|
+
if (configured) {
|
|
91
|
+
try {
|
|
92
|
+
return new URL(configured).origin;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const host = firstHeader(req.headers.host) ?? "127.0.0.1:3333";
|
|
99
|
+
if (!isLocalHost(host))
|
|
100
|
+
return null;
|
|
101
|
+
return `http://${host}`;
|
|
102
|
+
}
|
|
103
|
+
function extractStringArray(value, params, key) {
|
|
104
|
+
const values = Array.isArray(value) ? value : params.getAll(key);
|
|
105
|
+
return values.filter((item) => typeof item === "string" && item.length > 0);
|
|
106
|
+
}
|
|
107
|
+
async function readRawBody(req) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const chunks = [];
|
|
110
|
+
req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
111
|
+
req.on("error", reject);
|
|
112
|
+
req.on("end", () => resolve(chunks.length ? Buffer.concat(chunks).toString("utf8") : ""));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function readOAuthBody(req) {
|
|
116
|
+
const raw = await readRawBody(req);
|
|
117
|
+
const contentType = firstHeader(req.headers["content-type"]) ?? "";
|
|
118
|
+
if (contentType.includes("application/json")) {
|
|
119
|
+
const parsed = JSON.parse(raw || "{}");
|
|
120
|
+
const params = new URLSearchParams();
|
|
121
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
122
|
+
if (typeof value === "string")
|
|
123
|
+
params.set(key, value);
|
|
124
|
+
else if (Array.isArray(value)) {
|
|
125
|
+
for (const item of value) {
|
|
126
|
+
if (typeof item === "string")
|
|
127
|
+
params.append(key, item);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { params, json: parsed };
|
|
132
|
+
}
|
|
133
|
+
return { params: new URLSearchParams(raw), json: {} };
|
|
134
|
+
}
|
|
135
|
+
function basicClientCredentials(req) {
|
|
136
|
+
const authorization = firstHeader(req.headers.authorization);
|
|
137
|
+
if (!authorization?.startsWith("Basic "))
|
|
138
|
+
return null;
|
|
139
|
+
const raw = Buffer.from(authorization.slice("Basic ".length), "base64").toString("utf8");
|
|
140
|
+
const separator = raw.indexOf(":");
|
|
141
|
+
if (separator < 0)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
clientId: decodeURIComponent(raw.slice(0, separator)),
|
|
145
|
+
clientSecret: decodeURIComponent(raw.slice(separator + 1)),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function oauthClientSecret(req, params) {
|
|
149
|
+
return params.get("client_secret") ?? basicClientCredentials(req)?.clientSecret ?? null;
|
|
150
|
+
}
|
|
151
|
+
function oauthClientId(req, params) {
|
|
152
|
+
return params.get("client_id") ?? basicClientCredentials(req)?.clientId ?? null;
|
|
153
|
+
}
|
|
154
|
+
function validPkceVerifier(verifier, challenge, method) {
|
|
155
|
+
if (!challenge)
|
|
156
|
+
return true;
|
|
157
|
+
if (!verifier)
|
|
158
|
+
return false;
|
|
159
|
+
if (method === "S256") {
|
|
160
|
+
const digest = createHash("sha256").update(verifier).digest("base64url");
|
|
161
|
+
return timingSafeStringEqual(digest, challenge);
|
|
162
|
+
}
|
|
163
|
+
if (!method || method === "plain") {
|
|
164
|
+
return timingSafeStringEqual(verifier, challenge);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function oauthErrorRedirect(redirectUri, error, state) {
|
|
169
|
+
const target = new URL(redirectUri);
|
|
170
|
+
target.searchParams.set("error", error);
|
|
171
|
+
if (state)
|
|
172
|
+
target.searchParams.set("state", state);
|
|
173
|
+
return target.toString();
|
|
174
|
+
}
|
|
175
|
+
function normalizeScopes(scope) {
|
|
176
|
+
const scopes = (scope ?? "mcp")
|
|
177
|
+
.split(/\s+/)
|
|
178
|
+
.map(item => item.trim())
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
return [...new Set(scopes.length ? scopes : ["mcp"])];
|
|
181
|
+
}
|
|
182
|
+
function scopesAllowed(requested, client) {
|
|
183
|
+
return requested.every(scope => client.scopes.has(scope));
|
|
184
|
+
}
|
|
185
|
+
function toRuntimeClient(client, allowPublicClients) {
|
|
186
|
+
return {
|
|
187
|
+
clientId: client.clientId,
|
|
188
|
+
clientSecretHash: client.clientSecretHash ?? null,
|
|
189
|
+
redirectUris: new Set(client.allowedRedirectUris),
|
|
190
|
+
scopes: new Set(client.scopes.length ? client.scopes : ["mcp"]),
|
|
191
|
+
issuedAt: Math.floor(Date.now() / 1000),
|
|
192
|
+
publicClient: allowPublicClients && !client.clientSecretHash,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export class OAuthServer {
|
|
196
|
+
opts;
|
|
197
|
+
codes = new Map();
|
|
198
|
+
clients = new Map();
|
|
199
|
+
constructor(opts) {
|
|
200
|
+
this.opts = opts;
|
|
201
|
+
for (const client of opts.config.clients) {
|
|
202
|
+
this.clients.set(client.clientId, toRuntimeClient(client, opts.config.allowPublicClients));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
resourceMetadataUrl(baseUrl) {
|
|
206
|
+
return `${baseUrl}/.well-known/oauth-protected-resource`;
|
|
207
|
+
}
|
|
208
|
+
isOAuthPath(pathname) {
|
|
209
|
+
return (pathname.startsWith("/.well-known/oauth-protected-resource") ||
|
|
210
|
+
pathname.startsWith("/.well-known/oauth-authorization-server") ||
|
|
211
|
+
pathname === "/.well-known/openid-configuration" ||
|
|
212
|
+
pathname.startsWith("/oauth/"));
|
|
213
|
+
}
|
|
214
|
+
async handle(ctx) {
|
|
215
|
+
const { req, res, url, baseUrl } = ctx;
|
|
216
|
+
if (url.pathname.startsWith("/.well-known/oauth-protected-resource")) {
|
|
217
|
+
if (req.method !== "GET") {
|
|
218
|
+
methodNotAllowed(res);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
jsonResponse(res, 200, this.protectedResourceMetadata(baseUrl));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (url.pathname.startsWith("/.well-known/oauth-authorization-server") ||
|
|
225
|
+
url.pathname === "/.well-known/openid-configuration") {
|
|
226
|
+
if (req.method !== "GET") {
|
|
227
|
+
methodNotAllowed(res);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
jsonResponse(res, 200, this.authorizationServerMetadata(baseUrl));
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (url.pathname === "/oauth/register") {
|
|
234
|
+
await this.handleRegister(req, res);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (url.pathname === "/oauth/authorize") {
|
|
238
|
+
await this.handleAuthorize(req, res);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (url.pathname === "/oauth/token") {
|
|
242
|
+
await this.handleToken(req, res);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
protectedResourceMetadata(baseUrl) {
|
|
248
|
+
return {
|
|
249
|
+
resource: `${baseUrl}${this.opts.protectedPath}`,
|
|
250
|
+
authorization_servers: [baseUrl],
|
|
251
|
+
scopes_supported: ["mcp", "workspace:admin"],
|
|
252
|
+
bearer_methods_supported: ["header"],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
authorizationServerMetadata(baseUrl) {
|
|
256
|
+
return {
|
|
257
|
+
issuer: baseUrl,
|
|
258
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
|
259
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
260
|
+
registration_endpoint: `${baseUrl}/oauth/register`,
|
|
261
|
+
response_types_supported: ["code"],
|
|
262
|
+
grant_types_supported: ["authorization_code"],
|
|
263
|
+
token_endpoint_auth_methods_supported: this.opts.config.allowPublicClients
|
|
264
|
+
? ["client_secret_post", "client_secret_basic", "none"]
|
|
265
|
+
: ["client_secret_post", "client_secret_basic"],
|
|
266
|
+
code_challenge_methods_supported: this.opts.config.allowPlainPkce
|
|
267
|
+
? ["S256", "plain"]
|
|
268
|
+
: ["S256"],
|
|
269
|
+
scopes_supported: ["mcp", "workspace:admin"],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
registrationAllowedByPolicy(req, params) {
|
|
273
|
+
const policy = this.opts.config.registrationPolicy;
|
|
274
|
+
if (policy === "open_dev") {
|
|
275
|
+
const host = firstHeader(req.headers.host) ?? "";
|
|
276
|
+
return isLocalHost(host) || process.env.LLM_GATEWAY_OAUTH_OPEN_DEV === "1";
|
|
277
|
+
}
|
|
278
|
+
if (policy === "static_clients")
|
|
279
|
+
return false;
|
|
280
|
+
const supplied = params.get("shared_secret") ?? params.get("registration_secret");
|
|
281
|
+
if (!supplied || supplied.includes("?"))
|
|
282
|
+
return false;
|
|
283
|
+
const hash = this.opts.config.sharedSecret?.enabled
|
|
284
|
+
? this.opts.config.sharedSecret.secretHash
|
|
285
|
+
: null;
|
|
286
|
+
return Boolean(hash && verifySecret(supplied, hash));
|
|
287
|
+
}
|
|
288
|
+
async handleRegister(req, res) {
|
|
289
|
+
if (req.method !== "POST") {
|
|
290
|
+
methodNotAllowed(res);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const { params, json } = await readOAuthBody(req);
|
|
294
|
+
if (new URL(req.url ?? "/", "http://localhost").searchParams.has("shared_secret")) {
|
|
295
|
+
jsonResponse(res, 400, { error: "invalid_request" });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!this.registrationAllowedByPolicy(req, params)) {
|
|
299
|
+
jsonResponse(res, 403, { error: "invalid_client" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const redirectUris = extractStringArray(json.redirect_uris, params, "redirect_uris");
|
|
303
|
+
if (redirectUris.length === 0 || redirectUris.some(uri => !isHttpsOrLoopbackUrl(uri))) {
|
|
304
|
+
jsonResponse(res, 400, { error: "invalid_redirect_uri" });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const clientId = `llm-cli-gateway-${randomUUID()}`;
|
|
308
|
+
const clientSecret = this.opts.config.allowPublicClients ? null : generateSecret();
|
|
309
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
310
|
+
this.clients.set(clientId, {
|
|
311
|
+
clientId,
|
|
312
|
+
clientSecretHash: clientSecret ? hashSecret(clientSecret) : null,
|
|
313
|
+
redirectUris: new Set(redirectUris),
|
|
314
|
+
scopes: new Set(["mcp"]),
|
|
315
|
+
issuedAt,
|
|
316
|
+
publicClient: !clientSecret,
|
|
317
|
+
});
|
|
318
|
+
jsonResponse(res, 201, {
|
|
319
|
+
client_id: clientId,
|
|
320
|
+
...(clientSecret ? { client_secret: clientSecret } : {}),
|
|
321
|
+
client_id_issued_at: issuedAt,
|
|
322
|
+
grant_types: ["authorization_code"],
|
|
323
|
+
response_types: ["code"],
|
|
324
|
+
redirect_uris: redirectUris,
|
|
325
|
+
token_endpoint_auth_method: clientSecret ? "client_secret_post" : "none",
|
|
326
|
+
scope: "mcp",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async handleAuthorize(req, res) {
|
|
330
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
331
|
+
methodNotAllowed(res);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const params = req.method === "POST"
|
|
335
|
+
? (await readOAuthBody(req)).params
|
|
336
|
+
: new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
337
|
+
if (params.has("shared_secret")) {
|
|
338
|
+
jsonResponse(res, 400, { error: "invalid_request" });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const responseType = params.get("response_type");
|
|
342
|
+
const clientId = params.get("client_id") ?? "";
|
|
343
|
+
const redirectUri = params.get("redirect_uri");
|
|
344
|
+
const state = params.get("state");
|
|
345
|
+
if (!redirectUri) {
|
|
346
|
+
jsonResponse(res, 400, { error: "invalid_request" });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const client = this.clients.get(clientId);
|
|
350
|
+
if (!client || !client.redirectUris.has(redirectUri)) {
|
|
351
|
+
jsonResponse(res, 400, { error: "invalid_request" });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const method = params.get("code_challenge_method");
|
|
355
|
+
const codeChallenge = params.get("code_challenge");
|
|
356
|
+
if (responseType !== "code" ||
|
|
357
|
+
(this.opts.config.requirePkce && !codeChallenge) ||
|
|
358
|
+
(codeChallenge &&
|
|
359
|
+
method !== "S256" &&
|
|
360
|
+
!(this.opts.config.allowPlainPkce && method === "plain"))) {
|
|
361
|
+
res.writeHead(302, {
|
|
362
|
+
location: oauthErrorRedirect(redirectUri, "invalid_request", state),
|
|
363
|
+
});
|
|
364
|
+
res.end();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const requestedScopes = normalizeScopes(params.get("scope"));
|
|
368
|
+
if (!scopesAllowed(requestedScopes, client)) {
|
|
369
|
+
res.writeHead(302, {
|
|
370
|
+
location: oauthErrorRedirect(redirectUri, "invalid_scope", state),
|
|
371
|
+
});
|
|
372
|
+
res.end();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
this.pruneExpiredCodes();
|
|
376
|
+
const code = randomUUID();
|
|
377
|
+
this.codes.set(code, {
|
|
378
|
+
clientId,
|
|
379
|
+
redirectUri,
|
|
380
|
+
scope: requestedScopes.join(" "),
|
|
381
|
+
codeChallenge,
|
|
382
|
+
codeChallengeMethod: method,
|
|
383
|
+
expiresAt: Date.now() + OAUTH_CODE_TTL_MS,
|
|
384
|
+
});
|
|
385
|
+
const target = new URL(redirectUri);
|
|
386
|
+
target.searchParams.set("code", code);
|
|
387
|
+
if (state)
|
|
388
|
+
target.searchParams.set("state", state);
|
|
389
|
+
res.writeHead(302, { location: target.toString() });
|
|
390
|
+
res.end();
|
|
391
|
+
}
|
|
392
|
+
async handleToken(req, res) {
|
|
393
|
+
if (req.method !== "POST") {
|
|
394
|
+
methodNotAllowed(res);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (new URL(req.url ?? "/", "http://localhost").searchParams.has("client_secret")) {
|
|
398
|
+
jsonResponse(res, 400, { error: "invalid_request" });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const { params } = await readOAuthBody(req);
|
|
402
|
+
const code = params.get("code") ?? "";
|
|
403
|
+
const entry = this.codes.get(code);
|
|
404
|
+
const clientId = oauthClientId(req, params);
|
|
405
|
+
const client = clientId ? this.clients.get(clientId) : undefined;
|
|
406
|
+
const clientSecret = oauthClientSecret(req, params);
|
|
407
|
+
const secretOk = client?.publicClient ||
|
|
408
|
+
Boolean(client?.clientSecretHash &&
|
|
409
|
+
clientSecret &&
|
|
410
|
+
verifySecret(clientSecret, client.clientSecretHash));
|
|
411
|
+
if (params.get("grant_type") !== "authorization_code" ||
|
|
412
|
+
!entry ||
|
|
413
|
+
entry.expiresAt < Date.now() ||
|
|
414
|
+
!client ||
|
|
415
|
+
client.clientId !== entry.clientId ||
|
|
416
|
+
!secretOk ||
|
|
417
|
+
params.get("redirect_uri") !== entry.redirectUri ||
|
|
418
|
+
!validPkceVerifier(params.get("code_verifier"), entry.codeChallenge, entry.codeChallengeMethod)) {
|
|
419
|
+
jsonResponse(res, 400, { error: "invalid_grant" });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.codes.delete(code);
|
|
423
|
+
const token = issueOAuthAccessToken({
|
|
424
|
+
clientId: client.clientId,
|
|
425
|
+
scopes: normalizeScopes(entry.scope),
|
|
426
|
+
ttlSeconds: this.opts.config.tokenTtlSeconds,
|
|
427
|
+
});
|
|
428
|
+
jsonResponse(res, 200, {
|
|
429
|
+
access_token: token.accessToken,
|
|
430
|
+
token_type: "Bearer",
|
|
431
|
+
expires_in: token.expiresIn,
|
|
432
|
+
scope: token.scope,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
pruneExpiredCodes(now = Date.now()) {
|
|
436
|
+
for (const [code, entry] of this.codes) {
|
|
437
|
+
if (entry.expiresAt < now)
|
|
438
|
+
this.codes.delete(code);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface GatewayRequestContext {
|
|
2
|
+
authKind?: "disabled" | "gateway_bearer" | "oauth";
|
|
3
|
+
authScopes: string[];
|
|
4
|
+
authClientId?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runWithRequestContext<T>(context: GatewayRequestContext, callback: () => T | Promise<T>): T | Promise<T>;
|
|
7
|
+
export declare function getRequestContext(): GatewayRequestContext | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const requestContext = new AsyncLocalStorage();
|
|
3
|
+
export function runWithRequestContext(context, callback) {
|
|
4
|
+
return requestContext.run(context, callback);
|
|
5
|
+
}
|
|
6
|
+
export function getRequestContext() {
|
|
7
|
+
return requestContext.getStore();
|
|
8
|
+
}
|
|
@@ -98,44 +98,44 @@ export declare const CLAUDE_HIGH_IMPACT_PARAMS_SCHEMA: z.ZodEffects<z.ZodObject<
|
|
|
98
98
|
effort: z.ZodOptional<z.ZodEnum<["low", "medium", "high", "xhigh", "max"]>>;
|
|
99
99
|
excludeDynamicSystemPromptSections: z.ZodOptional<z.ZodBoolean>;
|
|
100
100
|
}, "strip", z.ZodTypeAny, {
|
|
101
|
-
agent?: string | undefined;
|
|
102
101
|
agents?: Record<string, Record<string, unknown>> | undefined;
|
|
102
|
+
agent?: string | undefined;
|
|
103
103
|
forkSession?: boolean | undefined;
|
|
104
104
|
systemPrompt?: string | undefined;
|
|
105
105
|
appendSystemPrompt?: string | undefined;
|
|
106
106
|
maxBudgetUsd?: number | undefined;
|
|
107
107
|
maxTurns?: number | undefined;
|
|
108
|
-
effort?: "
|
|
108
|
+
effort?: "medium" | "low" | "high" | "xhigh" | "max" | undefined;
|
|
109
109
|
excludeDynamicSystemPromptSections?: boolean | undefined;
|
|
110
110
|
}, {
|
|
111
|
-
agent?: string | undefined;
|
|
112
111
|
agents?: Record<string, Record<string, unknown>> | undefined;
|
|
112
|
+
agent?: string | undefined;
|
|
113
113
|
forkSession?: boolean | undefined;
|
|
114
114
|
systemPrompt?: string | undefined;
|
|
115
115
|
appendSystemPrompt?: string | undefined;
|
|
116
116
|
maxBudgetUsd?: number | undefined;
|
|
117
117
|
maxTurns?: number | undefined;
|
|
118
|
-
effort?: "
|
|
118
|
+
effort?: "medium" | "low" | "high" | "xhigh" | "max" | undefined;
|
|
119
119
|
excludeDynamicSystemPromptSections?: boolean | undefined;
|
|
120
120
|
}>, {
|
|
121
|
-
agent?: string | undefined;
|
|
122
121
|
agents?: Record<string, Record<string, unknown>> | undefined;
|
|
122
|
+
agent?: string | undefined;
|
|
123
123
|
forkSession?: boolean | undefined;
|
|
124
124
|
systemPrompt?: string | undefined;
|
|
125
125
|
appendSystemPrompt?: string | undefined;
|
|
126
126
|
maxBudgetUsd?: number | undefined;
|
|
127
127
|
maxTurns?: number | undefined;
|
|
128
|
-
effort?: "
|
|
128
|
+
effort?: "medium" | "low" | "high" | "xhigh" | "max" | undefined;
|
|
129
129
|
excludeDynamicSystemPromptSections?: boolean | undefined;
|
|
130
130
|
}, {
|
|
131
|
-
agent?: string | undefined;
|
|
132
131
|
agents?: Record<string, Record<string, unknown>> | undefined;
|
|
132
|
+
agent?: string | undefined;
|
|
133
133
|
forkSession?: boolean | undefined;
|
|
134
134
|
systemPrompt?: string | undefined;
|
|
135
135
|
appendSystemPrompt?: string | undefined;
|
|
136
136
|
maxBudgetUsd?: number | undefined;
|
|
137
137
|
maxTurns?: number | undefined;
|
|
138
|
-
effort?: "
|
|
138
|
+
effort?: "medium" | "low" | "high" | "xhigh" | "max" | undefined;
|
|
139
139
|
excludeDynamicSystemPromptSections?: boolean | undefined;
|
|
140
140
|
}>;
|
|
141
141
|
export declare const CLAUDE_AGENT_DEFINITION_SCHEMA: z.ZodObject<{
|