@lovable.dev/mcp-js 0.22.0 → 0.22.1-rc.1
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/dist/authorize-CumVyk7p.d.cts +27 -0
- package/dist/authorize-D6p4-9_D.d.ts +27 -0
- package/dist/base-DCe0kP79.d.cts +29 -0
- package/dist/base-DCe0kP79.d.ts +29 -0
- package/dist/cli/extract-manifest.cjs +116 -201
- package/dist/cli/extract-manifest.d.cts +1 -1
- package/dist/cli/extract-manifest.d.ts +1 -1
- package/dist/cli/extract-manifest.js +117 -156
- package/dist/content-D5LJAHyM.js +12 -0
- package/dist/content-NCYIGgOb.cjs +17 -0
- package/dist/context-BweeEcoA.cjs +56 -0
- package/dist/context-CIpPzN7P.js +51 -0
- package/dist/cors-BxwVxCm3.cjs +913 -0
- package/dist/cors-CJRL7tyh.js +830 -0
- package/dist/forwarded--h4efJy-.js +43 -0
- package/dist/forwarded-Pi8Hs5Ps.cjs +48 -0
- package/dist/fs-errors-CWNzOV75.cjs +11 -0
- package/dist/fs-errors-PA2t1TIp.js +6 -0
- package/dist/index.cjs +129 -329
- package/dist/index.d.cts +26 -26
- package/dist/index.d.ts +26 -26
- package/dist/index.js +118 -177
- package/dist/{io-D4srzRKc.d.ts → io-CfSPxeBN.d.ts} +6 -4
- package/dist/io-DPYvBqET.d.cts +15 -0
- package/dist/list-tools-B4TTPEl1.d.cts +8 -0
- package/dist/list-tools-Duj1zT0T.js +46 -0
- package/dist/list-tools-_Hh_o_mD.cjs +57 -0
- package/dist/list-tools-bh6-eqZM.d.ts +8 -0
- package/dist/logger-2OXtfiIW.js +141 -0
- package/dist/logger-BqPOa_E1.cjs +194 -0
- package/dist/mcp--iewDKrV.cjs +128 -0
- package/dist/mcp-CALHyhDi.js +123 -0
- package/dist/metadata-path-CAkNcfyY.js +12 -0
- package/dist/metadata-path-zd7NSNoa.cjs +17 -0
- package/dist/package-CLTXgGyU.cjs +9 -0
- package/dist/package-g7-GHkOW.js +4 -0
- package/dist/paths-IS65L6TA.js +7 -0
- package/dist/paths-OP318a6c.cjs +18 -0
- package/dist/protocols/mcp/index.cjs +3 -750
- package/dist/protocols/mcp/index.d.cts +6 -7
- package/dist/protocols/mcp/index.d.ts +6 -7
- package/dist/protocols/mcp/index.js +2 -12
- package/dist/protocols/oauth-metadata.cjs +48 -539
- package/dist/protocols/oauth-metadata.d.cts +5 -6
- package/dist/protocols/oauth-metadata.d.ts +5 -6
- package/dist/protocols/oauth-metadata.js +52 -10
- package/dist/protocols/rest/index.cjs +5 -843
- package/dist/protocols/rest/index.d.cts +7 -10
- package/dist/protocols/rest/index.d.ts +7 -10
- package/dist/protocols/rest/index.js +3 -16
- package/dist/rest-BcS3SYjx.cjs +122 -0
- package/dist/rest-Dv9Xq9cV.js +117 -0
- package/dist/stacks/supabase/index.cjs +76 -1318
- package/dist/stacks/supabase/index.d.cts +29 -30
- package/dist/stacks/supabase/index.d.ts +29 -30
- package/dist/stacks/supabase/index.js +73 -89
- package/dist/stacks/supabase/vite.cjs +258 -287
- package/dist/stacks/supabase/vite.d.cts +24 -24
- package/dist/stacks/supabase/vite.d.ts +24 -24
- package/dist/stacks/supabase/vite.js +252 -252
- package/dist/stacks/tanstack/index.cjs +42 -1272
- package/dist/stacks/tanstack/index.d.cts +8 -9
- package/dist/stacks/tanstack/index.d.ts +8 -9
- package/dist/stacks/tanstack/index.js +36 -41
- package/dist/stacks/tanstack/vite.cjs +223 -312
- package/dist/stacks/tanstack/vite.d.cts +80 -81
- package/dist/stacks/tanstack/vite.d.ts +80 -81
- package/dist/stacks/tanstack/vite.js +216 -282
- package/dist/types-BYdYYTjk.d.cts +321 -0
- package/dist/types-BYdYYTjk.d.ts +321 -0
- package/package.json +69 -28
- package/dist/authorize-BMeXd_Sh.d.ts +0 -25
- package/dist/base-CM-KCpp6.d.ts +0 -28
- package/dist/chunk-4EHZUUS5.js +0 -141
- package/dist/chunk-6DXGZZA4.js +0 -6
- package/dist/chunk-6KNM7RGD.js +0 -79
- package/dist/chunk-6QFZYYUV.js +0 -11
- package/dist/chunk-EOFPMSND.js +0 -6
- package/dist/chunk-H37EB22A.js +0 -125
- package/dist/chunk-MA5H6PSF.js +0 -46
- package/dist/chunk-P43FIUMZ.js +0 -114
- package/dist/chunk-RK62EMPY.js +0 -741
- package/dist/chunk-RLE4UE63.js +0 -58
- package/dist/chunk-UQK5UO6C.js +0 -26
- package/dist/chunk-XQWJN6DC.js +0 -14
- package/dist/chunk-Y3ZFPEQH.js +0 -8
- package/dist/types-qqrmJ736.d.ts +0 -321
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import { c as DEFAULT_METRICS_ENDPOINT, i as log, l as resolveMetricsConfig, n as applyLogLevelFromEnv, o as parseSafeUrl, r as describeError, s as trimTrailingSlash, t as LOG_LEVEL_ENV_VAR } from "./logger-2OXtfiIW.js";
|
|
2
|
+
import { t as version } from "./package-g7-GHkOW.js";
|
|
3
|
+
import "./metadata-path-CAkNcfyY.js";
|
|
4
|
+
import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
5
|
+
//#region src/core/http.ts
|
|
6
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
7
|
+
/** Strip the body from a built response, preserving status and headers, for HEAD. */
|
|
8
|
+
function headResponse(response) {
|
|
9
|
+
return new Response(null, {
|
|
10
|
+
status: response.status,
|
|
11
|
+
statusText: response.statusText,
|
|
12
|
+
headers: response.headers
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function methodNotAllowed(allow) {
|
|
16
|
+
return new Response(JSON.stringify({ error: "method not allowed" }), {
|
|
17
|
+
status: 405,
|
|
18
|
+
headers: {
|
|
19
|
+
...JSON_HEADERS,
|
|
20
|
+
Allow: allow
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/metrics/otlp.ts
|
|
26
|
+
const SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
27
|
+
const EVENT_NAME = "mcp.tool.invocation";
|
|
28
|
+
const SEVERITY_INFO = 9;
|
|
29
|
+
function strAttr(key, value) {
|
|
30
|
+
return {
|
|
31
|
+
key,
|
|
32
|
+
value: { stringValue: value }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function intAttr(key, value) {
|
|
36
|
+
return {
|
|
37
|
+
key,
|
|
38
|
+
value: { intValue: String(Math.round(value)) }
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function toLogRecord(rec) {
|
|
42
|
+
const attributes = [
|
|
43
|
+
strAttr("event.name", EVENT_NAME),
|
|
44
|
+
strAttr("mcp.method", rec.method),
|
|
45
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
46
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
47
|
+
];
|
|
48
|
+
if (rec.tool !== null) attributes.push(strAttr("mcp.tool", rec.tool));
|
|
49
|
+
if (rec.reqBytes !== void 0) attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
50
|
+
if (rec.resBytes !== void 0) attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
51
|
+
attributes.push(strAttr("mcp.stack", rec.stack));
|
|
52
|
+
if (rec.endUserId !== void 0) attributes.push(strAttr("mcp.end_user_id", rec.endUserId));
|
|
53
|
+
return {
|
|
54
|
+
timeUnixNano: rec.timeUnixNano,
|
|
55
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
56
|
+
severityNumber: SEVERITY_INFO,
|
|
57
|
+
severityText: "INFO",
|
|
58
|
+
body: { stringValue: EVENT_NAME },
|
|
59
|
+
attributes
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build an OTLP/HTTP JSON `ExportLogsServiceRequest` body for a batch of
|
|
64
|
+
* invocations. All records in a batch share one resource (the MCP server's
|
|
65
|
+
* identity); per-tenant attribution is derived server-side from the auth header,
|
|
66
|
+
* never from client-supplied resource attributes.
|
|
67
|
+
*/
|
|
68
|
+
function buildLogsPayload(records, server) {
|
|
69
|
+
return JSON.stringify({ resourceLogs: [{
|
|
70
|
+
resource: { attributes: [
|
|
71
|
+
strAttr("service.name", server.name),
|
|
72
|
+
strAttr("service.version", server.version),
|
|
73
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
74
|
+
strAttr("telemetry.sdk.version", version),
|
|
75
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
76
|
+
] },
|
|
77
|
+
scopeLogs: [{
|
|
78
|
+
scope: {
|
|
79
|
+
name: SCOPE_NAME,
|
|
80
|
+
version
|
|
81
|
+
},
|
|
82
|
+
logRecords: records.map(toLogRecord)
|
|
83
|
+
}]
|
|
84
|
+
}] });
|
|
85
|
+
}
|
|
86
|
+
/** Wall-clock nanoseconds since the Unix epoch as a string, without losing
|
|
87
|
+
* precision to float64 (`Date.now()` ms × 1e6 would overflow 2^53). */
|
|
88
|
+
function nowUnixNano() {
|
|
89
|
+
return `${Date.now()}000000`;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/metrics/impl/base.ts
|
|
93
|
+
/** Monotonic-ish millisecond clock for latency timing, with a `Date.now` fallback
|
|
94
|
+
* for runtimes without `performance.now`. */
|
|
95
|
+
function nowMs() {
|
|
96
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
97
|
+
}
|
|
98
|
+
const NOOP_RECORDER = { async emit() {} };
|
|
99
|
+
function createNoopRecorder() {
|
|
100
|
+
return NOOP_RECORDER;
|
|
101
|
+
}
|
|
102
|
+
/** Guarded `process.env` read — the shared fallback both stacks use for local
|
|
103
|
+
* dev. Tolerates a missing `process` global. */
|
|
104
|
+
function readProcessEnv(name) {
|
|
105
|
+
try {
|
|
106
|
+
return typeof process !== "undefined" ? process.env?.[name] || void 0 : void 0;
|
|
107
|
+
} catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Base per-request recorder. Owns everything stack-agnostic: log-level setup, the
|
|
113
|
+
* `tool.invoked` log, the single-record OTLP POST (awaited so it lands within the
|
|
114
|
+
* request — serverless workers kill detached work once the response returns), and
|
|
115
|
+
* never throwing into tool dispatch. Every invocation is reported (no sampling).
|
|
116
|
+
* Subclasses supply only the `stack` label and how to read env on that runtime.
|
|
117
|
+
*
|
|
118
|
+
* One instance is created per request by the stack handler and shared across
|
|
119
|
+
* every invocation in that request; it is never a process-wide singleton, since
|
|
120
|
+
* on Cloudflare the env binding isn't readable at module-init scope.
|
|
121
|
+
*/
|
|
122
|
+
var BaseMetricRecorder = class {
|
|
123
|
+
config;
|
|
124
|
+
server;
|
|
125
|
+
deps;
|
|
126
|
+
doFetch;
|
|
127
|
+
usesLovableEndpoint;
|
|
128
|
+
baseHeaders;
|
|
129
|
+
apiKey;
|
|
130
|
+
lazyLogLevelApplied = false;
|
|
131
|
+
constructor(config, server, deps = {}) {
|
|
132
|
+
this.config = config;
|
|
133
|
+
this.server = server;
|
|
134
|
+
this.deps = deps;
|
|
135
|
+
this.doFetch = deps.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
|
|
136
|
+
this.usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
137
|
+
const headers = {};
|
|
138
|
+
if (!this.usesLovableEndpoint) {
|
|
139
|
+
for (const [key, value] of Object.entries(config.headers)) if (key.toLowerCase() !== "content-type") headers[key] = value;
|
|
140
|
+
}
|
|
141
|
+
headers["content-type"] = "application/json";
|
|
142
|
+
this.baseHeaders = headers;
|
|
143
|
+
}
|
|
144
|
+
async emit(ev) {
|
|
145
|
+
await this.applyLazyLogLevel();
|
|
146
|
+
log.info("tool.invoked", {
|
|
147
|
+
tool: ev.tool,
|
|
148
|
+
method: ev.method,
|
|
149
|
+
outcome: ev.outcome,
|
|
150
|
+
durationMs: ev.durationMs,
|
|
151
|
+
stack: this.stack,
|
|
152
|
+
...ev.errorText !== void 0 && { errorText: ev.errorText }
|
|
153
|
+
});
|
|
154
|
+
if (!this.config.enabled) return;
|
|
155
|
+
if (!this.doFetch) {
|
|
156
|
+
log.warn("metrics.disabled_no_fetch", { endpoint: this.config.endpoint });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const headers = { ...this.baseHeaders };
|
|
160
|
+
if (this.usesLovableEndpoint) {
|
|
161
|
+
const key = await this.resolveApiKey();
|
|
162
|
+
if (!key) {
|
|
163
|
+
log.warn("metrics.disabled_no_api_key", {
|
|
164
|
+
envVar: this.config.apiKeyEnvVar,
|
|
165
|
+
endpoint: this.config.endpoint
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
headers.authorization = `Bearer ${key}`;
|
|
170
|
+
}
|
|
171
|
+
const body = buildLogsPayload([{
|
|
172
|
+
...ev,
|
|
173
|
+
timeUnixNano: nowUnixNano(),
|
|
174
|
+
stack: this.stack
|
|
175
|
+
}], this.server);
|
|
176
|
+
try {
|
|
177
|
+
const res = await this.doFetch(this.config.endpoint, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers,
|
|
180
|
+
body,
|
|
181
|
+
keepalive: true
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok) log.warn("metrics.rejected", {
|
|
184
|
+
status: res.status,
|
|
185
|
+
endpoint: this.config.endpoint
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log.warn("metrics.failed", {
|
|
189
|
+
...describeError(err),
|
|
190
|
+
endpoint: this.config.endpoint
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async resolveApiKey() {
|
|
195
|
+
if (this.apiKey) return this.apiKey;
|
|
196
|
+
this.apiKey = this.deps.getApiKey ? this.deps.getApiKey() : await this.readEnv(this.config.apiKeyEnvVar);
|
|
197
|
+
return this.apiKey;
|
|
198
|
+
}
|
|
199
|
+
async applyLazyLogLevel() {
|
|
200
|
+
if (this.lazyLogLevelApplied) return;
|
|
201
|
+
this.lazyLogLevelApplied = true;
|
|
202
|
+
applyLogLevelFromEnv(await this.readEnv(LOG_LEVEL_ENV_VAR));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/metrics/impl/cloudflare.ts
|
|
207
|
+
let cloudflareEnvPromise;
|
|
208
|
+
async function readCloudflareEnv(name) {
|
|
209
|
+
try {
|
|
210
|
+
cloudflareEnvPromise ??= import(
|
|
211
|
+
/* @vite-ignore */
|
|
212
|
+
"cloudflare:workers"
|
|
213
|
+
).then((m) => m.env).catch((err) => {
|
|
214
|
+
log.debug("metrics.cloudflare_env_import_failed", describeError(err));
|
|
215
|
+
});
|
|
216
|
+
const env = await cloudflareEnvPromise;
|
|
217
|
+
const raw = env?.[name];
|
|
218
|
+
const value = typeof raw === "string" && raw ? raw : void 0;
|
|
219
|
+
log.debug("metrics.read_cloudflare_env", {
|
|
220
|
+
name,
|
|
221
|
+
hasEnvBinding: !!env,
|
|
222
|
+
found: value !== void 0
|
|
223
|
+
});
|
|
224
|
+
return value;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
log.debug("metrics.read_cloudflare_env_error", {
|
|
227
|
+
name,
|
|
228
|
+
...describeError(err)
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** TanStack apps deploy to Cloudflare Workers: secrets live on the `env` binding
|
|
234
|
+
* (read via `cloudflare:workers`), with `process.env` as the local-dev fallback. */
|
|
235
|
+
var TanStackMetricRecorder = class extends BaseMetricRecorder {
|
|
236
|
+
stack = "tanstack";
|
|
237
|
+
async readEnv(name) {
|
|
238
|
+
return readProcessEnv(name) ?? await readCloudflareEnv(name);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/metrics/impl/supabase.ts
|
|
243
|
+
function readDenoEnv(name) {
|
|
244
|
+
try {
|
|
245
|
+
const denoEnv = globalThis.Deno?.env;
|
|
246
|
+
const value = denoEnv?.get?.(name) || void 0;
|
|
247
|
+
log.debug("metrics.read_deno_env", {
|
|
248
|
+
name,
|
|
249
|
+
hasDenoEnv: !!denoEnv,
|
|
250
|
+
found: !!value
|
|
251
|
+
});
|
|
252
|
+
return value;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
log.debug("metrics.read_deno_env_error", {
|
|
255
|
+
name,
|
|
256
|
+
...describeError(err)
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Supabase Edge Functions run on Deno: secrets are in `Deno.env`, with
|
|
262
|
+
* `process.env` as a fallback — both synchronous, resolved into the promise. */
|
|
263
|
+
var SupabaseMetricRecorder = class extends BaseMetricRecorder {
|
|
264
|
+
stack = "supabase";
|
|
265
|
+
async readEnv(name) {
|
|
266
|
+
return readDenoEnv(name) ?? readProcessEnv(name);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/metrics/recorder.ts
|
|
271
|
+
/** Vend the recorder for a stack, in-flight per request. Returns a no-op when
|
|
272
|
+
* metrics are disabled (`metrics: false`); otherwise the stack-specific class,
|
|
273
|
+
* which still logs `tool.invoked` even if the key or endpoint is unavailable. */
|
|
274
|
+
function createRecorderForRuntime(mcp, opts) {
|
|
275
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
276
|
+
const server = {
|
|
277
|
+
name: mcp.name,
|
|
278
|
+
version: mcp.version
|
|
279
|
+
};
|
|
280
|
+
if (!config.enabled) return createNoopRecorder();
|
|
281
|
+
return opts.stack === "tanstack" ? new TanStackMetricRecorder(config, server) : new SupabaseMetricRecorder(config, server);
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/core/promise.ts
|
|
285
|
+
/**
|
|
286
|
+
* Caches only the fulfilled value: until one load resolves, concurrent callers
|
|
287
|
+
* each run their own `load()`; afterwards every call returns the cached value
|
|
288
|
+
* with no I/O. A rejection caches nothing, so the next call retries.
|
|
289
|
+
*
|
|
290
|
+
* Not single-flight by design. Sharing one in-flight promise across callers
|
|
291
|
+
* breaks on Cloudflare Workers, where a promise that owns pending I/O can't be
|
|
292
|
+
* awaited from a different request's I/O context.
|
|
293
|
+
*/
|
|
294
|
+
function cachedPromise(load, label) {
|
|
295
|
+
let settled = false;
|
|
296
|
+
let value;
|
|
297
|
+
return async () => {
|
|
298
|
+
if (settled) {
|
|
299
|
+
if (label) log.debug(`${label}.cache_hit`);
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
if (label) log.debug(`${label}.load_start`);
|
|
303
|
+
try {
|
|
304
|
+
const loaded = await load();
|
|
305
|
+
settled = true;
|
|
306
|
+
value = loaded;
|
|
307
|
+
if (label) log.debug(`${label}.settled`);
|
|
308
|
+
return loaded;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (label) log.debug(`${label}.load_failed`, describeError(err));
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/auth/discovery.ts
|
|
317
|
+
var OAuthConfigurationError = class extends Error {
|
|
318
|
+
constructor(message) {
|
|
319
|
+
super(message);
|
|
320
|
+
this.name = "OAuthConfigurationError";
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const METADATA_FETCH_TIMEOUT_MS = 5e3;
|
|
324
|
+
function issuerPath(url) {
|
|
325
|
+
const path = trimTrailingSlash(url.pathname);
|
|
326
|
+
return path === "" ? void 0 : path;
|
|
327
|
+
}
|
|
328
|
+
function pathInsertedOAuthMetadataUrls(url, path) {
|
|
329
|
+
return [`${url.origin}/.well-known/oauth-authorization-server${path}`, `${url.origin}/.well-known/openid-configuration${path}`];
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Spec-legal metadata URLs for the issuer, ordered by RFC precedence: RFC 8414
|
|
333
|
+
* path insertion first, then OIDC Discovery's issuer-relative form.
|
|
334
|
+
* `fetchOAuthServerMetadata` validates the published `issuer` before accepting,
|
|
335
|
+
* so order affects latency only, never which server verifies tokens.
|
|
336
|
+
*/
|
|
337
|
+
function oauthMetadataUrlsForIssuer(issuer) {
|
|
338
|
+
const normalizedIssuer = trimTrailingSlash(issuer);
|
|
339
|
+
const url = new URL(normalizedIssuer);
|
|
340
|
+
const path = issuerPath(url);
|
|
341
|
+
if (!path) return [`${normalizedIssuer}/.well-known/oauth-authorization-server`, `${normalizedIssuer}/.well-known/openid-configuration`];
|
|
342
|
+
return [...pathInsertedOAuthMetadataUrls(url, path), `${normalizedIssuer}/.well-known/openid-configuration`];
|
|
343
|
+
}
|
|
344
|
+
async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer) {
|
|
345
|
+
const errors = [];
|
|
346
|
+
for (const url of metadataUrls) try {
|
|
347
|
+
return await fetchOAuthServerMetadata(url, expectedIssuer);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
log.debug("oauth.discovery.attempt_failed", {
|
|
350
|
+
url,
|
|
351
|
+
...describeError(err)
|
|
352
|
+
});
|
|
353
|
+
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
354
|
+
}
|
|
355
|
+
log.error("oauth.discovery.exhausted", {
|
|
356
|
+
expectedIssuer,
|
|
357
|
+
urlsTried: metadataUrls,
|
|
358
|
+
errors
|
|
359
|
+
});
|
|
360
|
+
throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
|
|
361
|
+
}
|
|
362
|
+
async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
363
|
+
log.debug("oauth.discovery.fetch", { url });
|
|
364
|
+
const response = await fetch(url, {
|
|
365
|
+
signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS),
|
|
366
|
+
redirect: "manual"
|
|
367
|
+
});
|
|
368
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
369
|
+
const json = await response.json();
|
|
370
|
+
if (typeof json.issuer !== "string") throw new Error("missing issuer");
|
|
371
|
+
parseSafeUrl("discovered issuer", json.issuer);
|
|
372
|
+
if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
|
|
373
|
+
log.warn("oauth.discovery.issuer_mismatch", {
|
|
374
|
+
url,
|
|
375
|
+
expectedIssuer,
|
|
376
|
+
published: json.issuer
|
|
377
|
+
});
|
|
378
|
+
throw new Error("issuer mismatch");
|
|
379
|
+
}
|
|
380
|
+
if (typeof json.jwks_uri !== "string") throw new Error("missing jwks_uri");
|
|
381
|
+
parseSafeUrl("discovered jwks_uri", json.jwks_uri);
|
|
382
|
+
return {
|
|
383
|
+
issuer: json.issuer,
|
|
384
|
+
jwks_uri: json.jwks_uri
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
388
|
+
try {
|
|
389
|
+
return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
log.error("oauth.discovery.config_error", {
|
|
392
|
+
issuer,
|
|
393
|
+
...describeError(err),
|
|
394
|
+
outcome: "500 oauth configuration error"
|
|
395
|
+
});
|
|
396
|
+
throw new OAuthConfigurationError(`OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Metadata and token verification share issuer/JWKS discovery, but transient failures must not poison the runtime. */
|
|
400
|
+
function createOAuthDiscoveryResolver(auth) {
|
|
401
|
+
const configuredIssuer = trimTrailingSlash(auth.issuer);
|
|
402
|
+
const oauthServerMetadata = cachedPromise(() => fetchIssuerOAuthServerMetadata(configuredIssuer), "oauth.discovery.metadata");
|
|
403
|
+
return {
|
|
404
|
+
resolveIssuer: async () => configuredIssuer,
|
|
405
|
+
resolveJwksUri: async () => {
|
|
406
|
+
if (auth.jwksUri) {
|
|
407
|
+
log.debug("oauth.jwks.resolved", {
|
|
408
|
+
jwksUri: auth.jwksUri,
|
|
409
|
+
source: "configured"
|
|
410
|
+
});
|
|
411
|
+
return auth.jwksUri;
|
|
412
|
+
}
|
|
413
|
+
const jwksUri = (await oauthServerMetadata()).jwks_uri;
|
|
414
|
+
log.debug("oauth.jwks.resolved", {
|
|
415
|
+
jwksUri,
|
|
416
|
+
source: "discovered"
|
|
417
|
+
});
|
|
418
|
+
return jwksUri;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/auth/resource.ts
|
|
424
|
+
/**
|
|
425
|
+
* Defaults the protected-resource name from the request `Host` so hosted apps stay portable
|
|
426
|
+
* across domains. A validated config always sets `acceptedAudiences` or `resource`, so this
|
|
427
|
+
* default only names the advertised metadata, never the accepted JWT audience. Deployments
|
|
428
|
+
* behind an untrusted proxy should still pin `auth.resource` so a spoofed `Host` can't shift
|
|
429
|
+
* the advertised metadata URL.
|
|
430
|
+
*/
|
|
431
|
+
function resolveProtectedResource(auth, request, options) {
|
|
432
|
+
if (auth.resource) return trimTrailingSlash(auth.resource);
|
|
433
|
+
const path = resolveResourcePath(options.resourcePath, request);
|
|
434
|
+
const resourceURL = new URL(path, request.url);
|
|
435
|
+
resourceURL.search = "";
|
|
436
|
+
resourceURL.hash = "";
|
|
437
|
+
return trimTrailingSlash(resourceURL.toString());
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* A bare absolute path can't anchor `resource` to another origin; `//host`, `\\host`,
|
|
441
|
+
* absolute URLs, and `..` segments can. WHATWG URL coerces `\` to `/` for special
|
|
442
|
+
* schemes, so a backslash would smuggle a `//`-style authority past the prefix check.
|
|
443
|
+
*/
|
|
444
|
+
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
445
|
+
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) throw new Error(`@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`);
|
|
446
|
+
}
|
|
447
|
+
function resolveResourcePath(resourcePath, request) {
|
|
448
|
+
if (resourcePath === void 0) return new URL(request.url).pathname;
|
|
449
|
+
assertResourcePathShape(resourcePath);
|
|
450
|
+
return resourcePath;
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/auth/claims.ts
|
|
454
|
+
function readString(value) {
|
|
455
|
+
return typeof value === "string" ? value : void 0;
|
|
456
|
+
}
|
|
457
|
+
function splitScopes(value) {
|
|
458
|
+
if (typeof value === "string") return value.split(/\s+/).filter(Boolean);
|
|
459
|
+
return stringClaimList(value);
|
|
460
|
+
}
|
|
461
|
+
function stringClaim(claims, name) {
|
|
462
|
+
return readString(claims[name]);
|
|
463
|
+
}
|
|
464
|
+
/** Normalizes a string-or-array claim from untrusted wire data, dropping non-string and empty entries. */
|
|
465
|
+
function stringClaimList(value) {
|
|
466
|
+
if (typeof value === "string") return value === "" ? [] : [value];
|
|
467
|
+
if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string" && entry !== "");
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/auth/verifier.ts
|
|
472
|
+
const DEFAULT_JWT_ALGORITHMS = [
|
|
473
|
+
"RS256",
|
|
474
|
+
"RS384",
|
|
475
|
+
"RS512",
|
|
476
|
+
"ES256",
|
|
477
|
+
"ES384",
|
|
478
|
+
"ES512",
|
|
479
|
+
"EdDSA"
|
|
480
|
+
];
|
|
481
|
+
const DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
482
|
+
const DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
|
|
483
|
+
const JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
484
|
+
var OAuthTokenError = class extends Error {
|
|
485
|
+
status;
|
|
486
|
+
oauthError;
|
|
487
|
+
constructor(status, oauthError, message) {
|
|
488
|
+
super(message);
|
|
489
|
+
this.status = status;
|
|
490
|
+
this.oauthError = oauthError;
|
|
491
|
+
this.name = "OAuthTokenError";
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
function resolveAcceptedAudiences(auth, resource) {
|
|
495
|
+
return auth.acceptedAudiences ?? [resource];
|
|
496
|
+
}
|
|
497
|
+
function tokenHeaderFields(token) {
|
|
498
|
+
try {
|
|
499
|
+
const header = decodeProtectedHeader(token);
|
|
500
|
+
return {
|
|
501
|
+
jwtAlg: header.alg,
|
|
502
|
+
jwtKid: header.kid,
|
|
503
|
+
tokenLength: token.length
|
|
504
|
+
};
|
|
505
|
+
} catch {
|
|
506
|
+
return { tokenLength: token.length };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
510
|
+
try {
|
|
511
|
+
const response = await fetch(jwksUri, {
|
|
512
|
+
signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS),
|
|
513
|
+
redirect: "manual"
|
|
514
|
+
});
|
|
515
|
+
if (!response.ok) throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
516
|
+
return createLocalJWKSet(await response.json());
|
|
517
|
+
} catch (err) {
|
|
518
|
+
log.error("oauth.jwks.fetch_failed", {
|
|
519
|
+
...describeError(err),
|
|
520
|
+
outcome: "500 oauth configuration error"
|
|
521
|
+
});
|
|
522
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function assertAccessTokenTyp(token, allowed) {
|
|
526
|
+
let header;
|
|
527
|
+
try {
|
|
528
|
+
header = decodeProtectedHeader(token);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
log.debug("oauth.verify.bad_header", {
|
|
531
|
+
...describeError(err),
|
|
532
|
+
outcome: "401 invalid_token"
|
|
533
|
+
});
|
|
534
|
+
throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
|
|
535
|
+
}
|
|
536
|
+
const typ = header.typ;
|
|
537
|
+
if (typeof typ !== "string" || !allowed.includes(typ)) {
|
|
538
|
+
log.debug("oauth.verify.bad_typ", {
|
|
539
|
+
jwtTyp: typ,
|
|
540
|
+
allowed,
|
|
541
|
+
outcome: "401 invalid_token"
|
|
542
|
+
});
|
|
543
|
+
throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function verifyJwtClaims(token, keySet, issuer, auth) {
|
|
547
|
+
try {
|
|
548
|
+
const { payload } = await jwtVerify(token, keySet, {
|
|
549
|
+
issuer: [issuer, `${issuer}/`],
|
|
550
|
+
algorithms: auth.algorithms ? [...auth.algorithms] : DEFAULT_JWT_ALGORITHMS,
|
|
551
|
+
requiredClaims: ["sub", "exp"],
|
|
552
|
+
clockTolerance: auth.clockToleranceSeconds ?? DEFAULT_CLOCK_TOLERANCE_SECONDS
|
|
553
|
+
});
|
|
554
|
+
return payload;
|
|
555
|
+
} catch (err) {
|
|
556
|
+
log.debug("oauth.verify.rejected", {
|
|
557
|
+
...describeError(err),
|
|
558
|
+
outcome: "401 invalid_token"
|
|
559
|
+
});
|
|
560
|
+
throw err;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function checkAcceptedAudience(claims, accepted, auth) {
|
|
564
|
+
if (claims.aud !== void 0 && !(Array.isArray(claims.aud) && claims.aud.length === 0)) {
|
|
565
|
+
if (stringClaimList(claims.aud).some((audience) => accepted.includes(audience))) return "aud";
|
|
566
|
+
log.debug("oauth.verify.bad_audience", {
|
|
567
|
+
tokenAud: claims.aud,
|
|
568
|
+
accepted,
|
|
569
|
+
outcome: "401 invalid_token"
|
|
570
|
+
});
|
|
571
|
+
} else if (auth.acceptResourceClaim !== false) {
|
|
572
|
+
const acceptedTrimmed = accepted.map(trimTrailingSlash);
|
|
573
|
+
if (stringClaimList(claims.resource).some((resource) => acceptedTrimmed.includes(trimTrailingSlash(resource)))) return "resource";
|
|
574
|
+
log.debug("oauth.verify.bad_resource", {
|
|
575
|
+
tokenResource: claims.resource,
|
|
576
|
+
accepted,
|
|
577
|
+
outcome: "401 invalid_token"
|
|
578
|
+
});
|
|
579
|
+
} else log.debug("oauth.verify.empty_audience", {
|
|
580
|
+
accepted,
|
|
581
|
+
outcome: "401 invalid_token"
|
|
582
|
+
});
|
|
583
|
+
throw new OAuthTokenError(401, "invalid_token", "token audience is not accepted");
|
|
584
|
+
}
|
|
585
|
+
function assertNonEmptySubject(claims) {
|
|
586
|
+
const sub = claims["sub"];
|
|
587
|
+
if (typeof sub !== "string" || sub.trim() === "") {
|
|
588
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
589
|
+
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/** Rejects copied app-session JWTs that were not issued to an OAuth client. */
|
|
593
|
+
function assertOAuthClientClaim(auth, clientId) {
|
|
594
|
+
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
595
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
596
|
+
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/** `token` is non-enumerable so a stray `JSON.stringify(auth)` or spread can't leak the bearer. */
|
|
600
|
+
function makeBearer(token) {
|
|
601
|
+
return Object.defineProperty({}, "token", {
|
|
602
|
+
value: token,
|
|
603
|
+
enumerable: false
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function buildMcpAuthContext(args) {
|
|
607
|
+
const { claims } = args;
|
|
608
|
+
return {
|
|
609
|
+
type: "oauth",
|
|
610
|
+
principal: {
|
|
611
|
+
claims,
|
|
612
|
+
issuer: args.issuer,
|
|
613
|
+
resource: args.resource,
|
|
614
|
+
acceptedAudiences: args.acceptedAudiences,
|
|
615
|
+
scopes: splitScopes(claims.scope),
|
|
616
|
+
sub: stringClaim(claims, "sub"),
|
|
617
|
+
email: stringClaim(claims, "email"),
|
|
618
|
+
clientId: stringClaim(claims, "client_id") ?? stringClaim(claims, "azp")
|
|
619
|
+
},
|
|
620
|
+
bearer: makeBearer(args.token)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function createOAuthTokenVerifier(auth, discovery) {
|
|
624
|
+
const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
|
|
625
|
+
return async (token, request, options) => {
|
|
626
|
+
const resource = resolveProtectedResource(auth, request, options);
|
|
627
|
+
const issuer = await discovery.resolveIssuer();
|
|
628
|
+
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
629
|
+
log.debug("oauth.verify.start", {
|
|
630
|
+
issuer,
|
|
631
|
+
acceptedAudiences,
|
|
632
|
+
resource,
|
|
633
|
+
...tokenHeaderFields(token)
|
|
634
|
+
});
|
|
635
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
636
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
637
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
638
|
+
assertAccessTokenTyp(token, allowedTyps);
|
|
639
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, auth);
|
|
640
|
+
const audienceVia = checkAcceptedAudience(claims, acceptedAudiences, auth);
|
|
641
|
+
assertNonEmptySubject(claims);
|
|
642
|
+
const context = buildMcpAuthContext({
|
|
643
|
+
token,
|
|
644
|
+
claims,
|
|
645
|
+
issuer,
|
|
646
|
+
resource,
|
|
647
|
+
acceptedAudiences
|
|
648
|
+
});
|
|
649
|
+
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
650
|
+
log.info("oauth.verify.ok", {
|
|
651
|
+
sub: context.principal.sub,
|
|
652
|
+
clientId: context.principal.clientId,
|
|
653
|
+
scopes: context.principal.scopes,
|
|
654
|
+
audienceVia
|
|
655
|
+
});
|
|
656
|
+
return context;
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/auth/authorize.ts
|
|
661
|
+
function quoteAuthenticateParam(value) {
|
|
662
|
+
return `"${value.replace(/[\u0000-\u001F\u007F]/g, "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
663
|
+
}
|
|
664
|
+
function resolveProtectedResourceMetadataUrl(auth, request, options) {
|
|
665
|
+
if (auth.protectedResourceMetadataUrl !== void 0) return auth.protectedResourceMetadataUrl;
|
|
666
|
+
const base = auth.resource ?? request.url;
|
|
667
|
+
return new URL(options.metadataPath ?? "/.well-known/oauth-protected-resource", base).toString();
|
|
668
|
+
}
|
|
669
|
+
/** OAuth clients use the challenge to find this server's protected-resource metadata. */
|
|
670
|
+
function wwwAuthenticateHeader(auth, request, options, params = {}) {
|
|
671
|
+
const values = [`realm=${quoteAuthenticateParam("mcp")}`, `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`];
|
|
672
|
+
if (auth.requiredScopes && auth.requiredScopes.length > 0) values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
|
|
673
|
+
if (params.error) values.push(`error=${quoteAuthenticateParam(params.error)}`);
|
|
674
|
+
if (params.errorDescription) values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
|
|
675
|
+
return `Bearer ${values.join(", ")}`;
|
|
676
|
+
}
|
|
677
|
+
function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
|
|
678
|
+
const headers = new Headers(JSON_HEADERS);
|
|
679
|
+
headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, options, {
|
|
680
|
+
error: oauthError,
|
|
681
|
+
errorDescription
|
|
682
|
+
}));
|
|
683
|
+
headers.set("Cache-Control", "no-store");
|
|
684
|
+
return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
|
|
685
|
+
status,
|
|
686
|
+
headers
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function oauthConfigurationErrorResponse() {
|
|
690
|
+
return new Response(JSON.stringify({ error: "oauth configuration error" }), {
|
|
691
|
+
status: 500,
|
|
692
|
+
headers: {
|
|
693
|
+
...JSON_HEADERS,
|
|
694
|
+
"Cache-Control": "no-store"
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function parseBearerToken(request) {
|
|
699
|
+
const header = request.headers.get("Authorization");
|
|
700
|
+
if (!header) return void 0;
|
|
701
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
702
|
+
if (!match) return void 0;
|
|
703
|
+
const token = match[1].trim();
|
|
704
|
+
if (token === "" || /\s/.test(token)) return void 0;
|
|
705
|
+
return token;
|
|
706
|
+
}
|
|
707
|
+
/** @internal Builds the OAuth runtime (discovery + verifier) for a definition and its route options. */
|
|
708
|
+
function getOAuthRuntime(mcp, options = {}) {
|
|
709
|
+
if (options.resourcePath !== void 0) assertResourcePathShape(options.resourcePath);
|
|
710
|
+
if (options.metadataPath !== void 0) assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
711
|
+
const stableOptions = {
|
|
712
|
+
resourcePath: options.resourcePath,
|
|
713
|
+
metadataPath: options.metadataPath
|
|
714
|
+
};
|
|
715
|
+
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
716
|
+
if (!auth) return {
|
|
717
|
+
kind: "unconfigured",
|
|
718
|
+
options: stableOptions
|
|
719
|
+
};
|
|
720
|
+
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) throw new Error(`@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`);
|
|
721
|
+
const discovery = createOAuthDiscoveryResolver(auth);
|
|
722
|
+
return {
|
|
723
|
+
kind: "configured",
|
|
724
|
+
auth,
|
|
725
|
+
discovery,
|
|
726
|
+
verify: createOAuthTokenVerifier(auth, discovery),
|
|
727
|
+
options: stableOptions
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* REST companion routes (`/.mcp/list-tools`, `/.mcp/invoke-tool/*`) are internal
|
|
732
|
+
* RPC the main MCP server calls, so their own request path must never become the
|
|
733
|
+
* advertised resource. Require `auth.resource` or a `resourcePath` so the bound
|
|
734
|
+
* `principal.resource` is the public MCP route, not the internal `/.mcp/*` path.
|
|
735
|
+
* The MCP protocol handler is exempt: its own path is the public resource.
|
|
736
|
+
*/
|
|
737
|
+
function assertRestResourceBinding(mcp, options = {}) {
|
|
738
|
+
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
739
|
+
if (auth && auth.resource === void 0 && options.resourcePath === void 0) throw new Error(`@lovable.dev/mcp-js: REST companion handlers require auth.resource or a resourcePath so principal.resource binds to the public MCP route, not the internal /.mcp/* request path`);
|
|
740
|
+
}
|
|
741
|
+
function missingRequiredScopes(auth, scopes) {
|
|
742
|
+
const requiredScopes = auth.requiredScopes ?? [];
|
|
743
|
+
if (requiredScopes.length === 0) return [];
|
|
744
|
+
const granted = new Set(scopes);
|
|
745
|
+
return requiredScopes.filter((scope) => !granted.has(scope));
|
|
746
|
+
}
|
|
747
|
+
function assertRequiredScopes(auth, context) {
|
|
748
|
+
if (missingRequiredScopes(auth, context.principal.scopes).length > 0) throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
749
|
+
}
|
|
750
|
+
function createRequestAuthorizer(mcp, options = {}) {
|
|
751
|
+
const runtime = getOAuthRuntime(mcp, options);
|
|
752
|
+
return { async authorize(request, recorder) {
|
|
753
|
+
if (runtime.kind === "unconfigured") return { ok: true };
|
|
754
|
+
const startedAt = nowMs();
|
|
755
|
+
const token = parseBearerToken(request);
|
|
756
|
+
if (!token) {
|
|
757
|
+
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
758
|
+
return {
|
|
759
|
+
ok: false,
|
|
760
|
+
response: challengeResponse(runtime.auth, request, runtime.options, 401)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const auth = await runtime.verify(token, request, runtime.options);
|
|
765
|
+
assertRequiredScopes(runtime.auth, auth);
|
|
766
|
+
return {
|
|
767
|
+
ok: true,
|
|
768
|
+
auth
|
|
769
|
+
};
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (err instanceof OAuthConfigurationError) {
|
|
772
|
+
log.error("auth.config_error", {
|
|
773
|
+
...describeError(err),
|
|
774
|
+
outcome: "500"
|
|
775
|
+
});
|
|
776
|
+
await recorder?.emit({
|
|
777
|
+
tool: null,
|
|
778
|
+
method: "authorize",
|
|
779
|
+
outcome: "auth_config_error",
|
|
780
|
+
durationMs: nowMs() - startedAt
|
|
781
|
+
});
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
response: oauthConfigurationErrorResponse()
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (err instanceof OAuthTokenError) {
|
|
788
|
+
log.info("auth.token_rejected", {
|
|
789
|
+
status: err.status,
|
|
790
|
+
oauthError: err.oauthError
|
|
791
|
+
});
|
|
792
|
+
return {
|
|
793
|
+
ok: false,
|
|
794
|
+
response: challengeResponse(runtime.auth, request, runtime.options, err.status, err.oauthError, err.message)
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
log.error("auth.unexpected_error", {
|
|
798
|
+
...describeError(err),
|
|
799
|
+
outcome: "401"
|
|
800
|
+
});
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
response: challengeResponse(runtime.auth, request, runtime.options, 401, "invalid_token", "Invalid access token")
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
} };
|
|
807
|
+
}
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/core/cors.ts
|
|
810
|
+
const EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
|
|
811
|
+
const ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
|
|
812
|
+
/** Add permissive CORS to a built response, preserving its other headers. */
|
|
813
|
+
function withCors(response) {
|
|
814
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
815
|
+
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
816
|
+
return response;
|
|
817
|
+
}
|
|
818
|
+
function corsPreflightResponse(allowMethods) {
|
|
819
|
+
return new Response(null, {
|
|
820
|
+
status: 204,
|
|
821
|
+
headers: {
|
|
822
|
+
"Access-Control-Allow-Origin": "*",
|
|
823
|
+
"Access-Control-Allow-Methods": allowMethods,
|
|
824
|
+
"Access-Control-Allow-Headers": ALLOW_HEADERS,
|
|
825
|
+
"Access-Control-Max-Age": "86400"
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
export { getOAuthRuntime as a, resolveProtectedResource as c, nowMs as d, JSON_HEADERS as f, createRequestAuthorizer as i, createRecorderForRuntime as l, methodNotAllowed as m, withCors as n, oauthConfigurationErrorResponse as o, headResponse as p, assertRestResourceBinding as r, assertResourcePathShape as s, corsPreflightResponse as t, createNoopRecorder as u };
|