@frontmcp/sdk 1.5.3 → 1.5.5

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 (34) hide show
  1. package/agent/agent.scope.d.ts +8 -0
  2. package/agent/agent.scope.d.ts.map +1 -1
  3. package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
  4. package/auth/flows/oauth.callback.flow.d.ts +13 -0
  5. package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
  6. package/auth/flows/oauth.provider-callback.flow.d.ts.map +1 -1
  7. package/auth/flows/oauth.register.flow.d.ts.map +1 -1
  8. package/auth/flows/oauth.token.flow.d.ts +2 -0
  9. package/auth/flows/oauth.token.flow.d.ts.map +1 -1
  10. package/auth/flows/session.verify.flow.d.ts.map +1 -1
  11. package/auth/flows/well-known.oauth-authorization-server.flow.d.ts +1 -0
  12. package/auth/flows/well-known.oauth-authorization-server.flow.d.ts.map +1 -1
  13. package/auth/flows/well-known.prm.flow.d.ts.map +1 -1
  14. package/auth/instances/instance.local-primary-auth.d.ts +2 -2
  15. package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
  16. package/common/metadata/app.metadata.d.ts +20 -4
  17. package/common/metadata/app.metadata.d.ts.map +1 -1
  18. package/common/metadata/flow.metadata.d.ts +1 -1
  19. package/common/metadata/front-mcp.metadata.d.ts +72 -8
  20. package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
  21. package/common/metadata/tool.metadata.d.ts +1 -1
  22. package/common/utils/decide-request-intent.utils.d.ts +25 -0
  23. package/common/utils/decide-request-intent.utils.d.ts.map +1 -1
  24. package/common/utils/path.utils.d.ts +2 -0
  25. package/common/utils/path.utils.d.ts.map +1 -1
  26. package/esm/index.mjs +380 -68
  27. package/front-mcp/front-mcp.providers.d.ts +32 -0
  28. package/front-mcp/front-mcp.providers.d.ts.map +1 -1
  29. package/index.js +379 -67
  30. package/package.json +10 -10
  31. package/scope/flows/http.request.flow.d.ts.map +1 -1
  32. package/server/middleware/host-validation.middleware.d.ts.map +1 -1
  33. package/tool/flows/call-tool.flow.d.ts.map +1 -1
  34. package/transport/mcp-handlers/index.d.ts +1 -1
package/index.js CHANGED
@@ -20862,18 +20862,28 @@ var init_call_tool_flow = __esm({
20862
20862
  const consentCtx = this.input.ctx;
20863
20863
  if (!consentCtx?.internalCall) {
20864
20864
  const consentedToolIds = getConsentedToolIds(authInfo);
20865
- if (consentedToolIds && tool && !isToolConsented(consentedToolIds, tool.name, tool.fullName)) {
20866
- this.logger.info(
20867
- `checkToolAuthorization: tool "${tool.fullName || tool.name}" not in consented set \u2014 rejecting`
20868
- );
20869
- throw new ToolNotConsentedError(tool.fullName || tool.name);
20865
+ if (consentedToolIds && tool) {
20866
+ const bareNameCount = this.scope.tools.getTools(true).filter((t) => t.name === tool.name).length;
20867
+ const consented = bareNameCount > 1 ? isToolConsented(consentedToolIds, tool.fullName) : isToolConsented(consentedToolIds, tool.name, tool.fullName);
20868
+ if (!consented) {
20869
+ this.logger.info(
20870
+ `checkToolAuthorization: tool "${tool.fullName || tool.name}" not in consented set \u2014 rejecting`
20871
+ );
20872
+ throw new ToolNotConsentedError(tool.fullName || tool.name);
20873
+ }
20870
20874
  }
20871
20875
  }
20872
20876
  const claimAuthorizedApps = getAuthorizedAppIds(authInfo);
20873
20877
  const legacyAuthorization = authInfo?.extra?.["authorization"];
20874
20878
  if (!claimAuthorizedApps && !legacyAuthorization) {
20875
- this.logger.verbose("checkToolAuthorization:skip (no auth context)");
20876
- return;
20879
+ const incrementalEnabled = !consentCtx?.internalCall && !!authInfo?.token && this.scope.auth?.options?.incrementalAuth?.enabled === true;
20880
+ if (!incrementalEnabled) {
20881
+ this.logger.verbose("checkToolAuthorization:skip (no auth context)");
20882
+ return;
20883
+ }
20884
+ this.logger.warn(
20885
+ "checkToolAuthorization: incrementalAuth enabled but the verified token carries no authorized_apps claim \u2014 failing closed"
20886
+ );
20877
20887
  }
20878
20888
  const appId = tool?.owner?.id;
20879
20889
  if (!appId) {
@@ -34171,7 +34181,7 @@ var init_oauth_authorize_flow = __esm({
34171
34181
  if (!result.success) {
34172
34182
  const errors = this.formatZodErrors(result.error);
34173
34183
  this.logger.warn(`Authorization request validation failed: ${errors.join(", ")}`);
34174
- this.respondWithError(errors, rawRedirectUri, rawState);
34184
+ this.respondWithError(errors, void 0, rawState);
34175
34185
  return;
34176
34186
  }
34177
34187
  this.state.set("validatedRequest", result.data);
@@ -34226,6 +34236,22 @@ var init_oauth_authorize_flow = __esm({
34226
34236
  this.respondWithError(["redirect_uri is not registered for this client"], void 0, rawState);
34227
34237
  return;
34228
34238
  }
34239
+ if (!registered) {
34240
+ const authOptions = this.scope.auth?.options;
34241
+ const requireRegistered = authOptions?.requireRegisteredClients === true;
34242
+ const redirectAllowlisted = registry?.hasRedirectAllowlist?.() === true;
34243
+ if (requireRegistered && !redirectAllowlisted) {
34244
+ this.logger.warn(
34245
+ `OAuth authorize: rejecting unregistered client_id "${client_id}" (requireRegisteredClients)`
34246
+ );
34247
+ this.respondWithError(
34248
+ ["Unknown client_id: register the client (DCR / pre-registered) or use a CIMD client-id URL"],
34249
+ void 0,
34250
+ rawState
34251
+ );
34252
+ return;
34253
+ }
34254
+ }
34229
34255
  }
34230
34256
  }
34231
34257
  async checkIfAuthorized() {
@@ -34946,6 +34972,11 @@ var init_oauth_callback_flow = __esm({
34946
34972
  return;
34947
34973
  }
34948
34974
  }
34975
+ if (this.isCrossOriginSubmission()) {
34976
+ this.logger.warn("Cross-origin login submission rejected (CSRF)");
34977
+ this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Cross-origin request blocked"), 400));
34978
+ return;
34979
+ }
34949
34980
  let userSub;
34950
34981
  if (email) {
34951
34982
  userSub = this.generateUserSub(email);
@@ -35402,6 +35433,33 @@ var init_oauth_callback_flow = __esm({
35402
35433
  }
35403
35434
  return void 0;
35404
35435
  }
35436
+ /**
35437
+ * Same-origin (CSRF) check for a state-changing login/consent submission.
35438
+ *
35439
+ * Returns true when the request carries an `Origin` or `Referer` header whose
35440
+ * host does NOT match the request's own host — the signature of a cross-site
35441
+ * login-CSRF/fixation. `Origin`/`Referer` are set by the browser and cannot
35442
+ * be forged by a malicious page, so this reliably distinguishes a legitimate
35443
+ * same-origin submission (the built-in form is served by THIS server) from a
35444
+ * cross-site one. Returns false (do NOT block) when neither header is present
35445
+ * or the request's own host cannot be determined — defense-in-depth, not a
35446
+ * hard gate, so header-stripped GET navigations and existing flows keep working.
35447
+ */
35448
+ isCrossOriginSubmission() {
35449
+ const headers = this.rawInput?.request?.headers ?? {};
35450
+ const first = (v) => Array.isArray(v) ? v[0] : typeof v === "string" ? v : void 0;
35451
+ const source = first(headers["origin"]) ?? first(headers["referer"]);
35452
+ if (!source) return false;
35453
+ let sourceHost;
35454
+ try {
35455
+ sourceHost = new URL(source).host.toLowerCase();
35456
+ } catch {
35457
+ return true;
35458
+ }
35459
+ const selfHosts = [first(headers["host"]), first(headers["x-forwarded-host"])].filter((h2) => !!h2).map((h2) => h2.toLowerCase());
35460
+ if (selfHosts.length === 0) return false;
35461
+ return !selfHosts.includes(sourceHost);
35462
+ }
35405
35463
  collectLoginFields(request) {
35406
35464
  const fields = {};
35407
35465
  const absorb = (source) => {
@@ -36161,19 +36219,21 @@ var init_oauth_provider_callback_flow = __esm({
36161
36219
  );
36162
36220
  return;
36163
36221
  }
36164
- const stateValidation = this.getStateValidation();
36165
- if (stateValidation === "strict") {
36166
- const expectedState = session.currentProviderState;
36167
- if (!expectedState || expectedState !== providerState) {
36168
- this.logger.warn("State mismatch for provider callback");
36169
- this.respond(
36170
- httpRespond.html(
36171
- this.renderErrorPage("invalid_request", "Invalid state parameter. Please restart authentication."),
36172
- 400
36173
- )
36222
+ const expectedState = session.currentProviderState;
36223
+ if (!expectedState || expectedState !== providerState) {
36224
+ if (this.getStateValidation() === "format") {
36225
+ this.logger.warn(
36226
+ 'federatedAuth.stateValidation:"format" no longer skips provider-state nonce verification (security hardening) \u2014 the callback state must match the value issued for this session'
36174
36227
  );
36175
- return;
36176
36228
  }
36229
+ this.logger.warn("State mismatch for provider callback");
36230
+ this.respond(
36231
+ httpRespond.html(
36232
+ this.renderErrorPage("invalid_request", "Invalid state parameter. Please restart authentication."),
36233
+ 400
36234
+ )
36235
+ );
36236
+ return;
36177
36237
  }
36178
36238
  this.state.set("federatedSession", session);
36179
36239
  }
@@ -36260,11 +36320,19 @@ var init_oauth_provider_callback_flow = __esm({
36260
36320
  this.state.set("providerUserInfo", userInfo);
36261
36321
  } catch (err) {
36262
36322
  const errMsg = err instanceof Error ? err.message : "Unknown error";
36263
- this.logger.warn(`Failed to get user info from provider ${providerId}: ${errMsg}`);
36323
+ this.logger.warn(`Failed to resolve user identity from provider ${providerId}: ${errMsg}`);
36324
+ this.respond(
36325
+ httpRespond.html(
36326
+ this.renderErrorPage("access_denied", "Could not determine your identity from the provider"),
36327
+ 400
36328
+ )
36329
+ );
36330
+ return;
36264
36331
  }
36265
36332
  }
36266
36333
  this.logger.info(`Successfully exchanged code for tokens with provider: ${providerId}`);
36267
36334
  } catch (err) {
36335
+ if (err instanceof FlowControl) throw err;
36268
36336
  const errMsg = err instanceof Error ? err.message : "Unknown error";
36269
36337
  this.logger.error(`Provider token exchange error: ${errMsg}`);
36270
36338
  this.respond(
@@ -36642,6 +36710,24 @@ function extractBearer(headers) {
36642
36710
  const match = /^Bearer\s+(.+)$/i.exec(raw.trim());
36643
36711
  return match ? match[1].trim() : void 0;
36644
36712
  }
36713
+ function isLoopbackRedirectUri(uri) {
36714
+ let url;
36715
+ try {
36716
+ url = new URL(uri);
36717
+ } catch {
36718
+ return false;
36719
+ }
36720
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
36721
+ if (url.username || url.password) return false;
36722
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
36723
+ if (host === "localhost" || host === "::1") return true;
36724
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
36725
+ if (m) {
36726
+ const octets = m.slice(1).map((n) => Number(n));
36727
+ return octets.every((o) => o >= 0 && o <= 255) && octets[0] === 127;
36728
+ }
36729
+ return false;
36730
+ }
36645
36731
  var import_lazy_zod68, import_utils67, inputSchema20, outputSchema19, registrationRequestSchema, stateSchema19, plan19, name19, Stage19, OauthRegisterFlow;
36646
36732
  var init_oauth_register_flow = __esm({
36647
36733
  "libs/sdk/src/auth/flows/oauth.register.flow.ts"() {
@@ -36770,13 +36856,13 @@ var init_oauth_register_flow = __esm({
36770
36856
  return;
36771
36857
  }
36772
36858
  } else {
36773
- const bad = redirect_uris.find((u) => !/^https?:\/\/(localhost|\d+\.\d+\.\d+\.\d+|127\.0\.0\.1)/.test(u));
36859
+ const bad = redirect_uris.find((u) => !isLoopbackRedirectUri(u));
36774
36860
  if (bad) {
36775
36861
  this.respond(
36776
36862
  httpRespond.json(
36777
36863
  {
36778
36864
  error: "invalid_redirect_uri",
36779
- error_description: `Registration allows only localhost-style redirect_uris; got ${bad}`
36865
+ error_description: `Registration allows only loopback redirect_uris (localhost, 127.0.0.0/8, ::1) without an allowlist; got ${bad}`
36780
36866
  },
36781
36867
  { status: 400 }
36782
36868
  )
@@ -36817,7 +36903,20 @@ var init_oauth_register_flow = __esm({
36817
36903
  created_at: now,
36818
36904
  dev: true
36819
36905
  };
36820
- this.localAuth.dcrClientRegistry.register(this.registered);
36906
+ const accepted = this.localAuth.dcrClientRegistry.register(this.registered);
36907
+ if (!accepted) {
36908
+ this.scopeLogger.warn("OAuth register: dynamic client capacity reached \u2014 rejecting registration");
36909
+ this.respond(
36910
+ httpRespond.json(
36911
+ {
36912
+ error: "temporarily_unavailable",
36913
+ error_description: "Client registration capacity reached; please try again later."
36914
+ },
36915
+ { status: 503 }
36916
+ )
36917
+ );
36918
+ return;
36919
+ }
36821
36920
  }
36822
36921
  async respondRegistration() {
36823
36922
  const c = this.registered;
@@ -36881,6 +36980,23 @@ var init_oauth_register_flow = __esm({
36881
36980
  });
36882
36981
 
36883
36982
  // libs/sdk/src/auth/flows/oauth.token.flow.ts
36983
+ function parseBasicAuthorization(header) {
36984
+ if (typeof header !== "string") return void 0;
36985
+ const m = /^Basic\s+(.+)$/i.exec(header.trim());
36986
+ if (!m) return void 0;
36987
+ let decoded;
36988
+ try {
36989
+ const b64 = m[1].trim();
36990
+ if (typeof Buffer !== "undefined") decoded = Buffer.from(b64, "base64").toString("utf8");
36991
+ else if (typeof atob === "function") decoded = atob(b64);
36992
+ else return void 0;
36993
+ } catch {
36994
+ return void 0;
36995
+ }
36996
+ const idx = decoded.indexOf(":");
36997
+ if (idx < 0) return void 0;
36998
+ return { clientId: decoded.slice(0, idx), clientSecret: decoded.slice(idx + 1) };
36999
+ }
36884
37000
  function coerceTokenRequestBody(request) {
36885
37001
  const body = request.body;
36886
37002
  if (body && typeof body === "object" && !Array.isArray(body) && Object.keys(body).length > 0) {
@@ -36927,6 +37043,8 @@ var init_oauth_token_flow = __esm({
36927
37043
  redirect_uri: import_lazy_zod69.z.string().url(),
36928
37044
  /** Public client identifier */
36929
37045
  client_id: import_lazy_zod69.z.string().min(1),
37046
+ /** Client secret (confidential clients; also accepted via HTTP Basic). */
37047
+ client_secret: import_lazy_zod69.z.string().optional(),
36930
37048
  /** PKCE verifier bound to the code */
36931
37049
  code_verifier: import_lazy_zod69.z.string().regex(pkceVerifierRegex, "code_verifier must be 43\u2013128 chars of A\u2013Z, a\u2013z, 0\u20139, '-', '.', '_' or '~'")
36932
37050
  });
@@ -36935,7 +37053,9 @@ var init_oauth_token_flow = __esm({
36935
37053
  /** The refresh token */
36936
37054
  refresh_token: import_lazy_zod69.z.string().min(1, "refresh_token is required"),
36937
37055
  /** Public client identifier */
36938
- client_id: import_lazy_zod69.z.string().min(1)
37056
+ client_id: import_lazy_zod69.z.string().min(1),
37057
+ /** Client secret (confidential clients; also accepted via HTTP Basic). */
37058
+ client_secret: import_lazy_zod69.z.string().optional()
36939
37059
  });
36940
37060
  anonymousGrant = import_lazy_zod69.z.object({
36941
37061
  grant_type: import_lazy_zod69.z.literal("anonymous"),
@@ -36982,6 +37102,12 @@ var init_oauth_token_flow = __esm({
36982
37102
  const isDefaultAuthProvider = !metadata.auth;
36983
37103
  const isOrchestrated = !isDefaultAuthProvider;
36984
37104
  const rawBody = coerceTokenRequestBody(request);
37105
+ const basic = parseBasicAuthorization(request.headers?.["authorization"]);
37106
+ if (basic && rawBody && typeof rawBody === "object") {
37107
+ const b = rawBody;
37108
+ if (b["client_id"] === void 0) b["client_id"] = basic.clientId;
37109
+ if (b["client_secret"] === void 0) b["client_secret"] = basic.clientSecret;
37110
+ }
36985
37111
  const parsed = tokenRequestSchema.safeParse(rawBody);
36986
37112
  if (parsed.success) {
36987
37113
  this.state.set({
@@ -37032,7 +37158,13 @@ var init_oauth_token_flow = __esm({
37032
37158
  return;
37033
37159
  }
37034
37160
  const localAuth = this.scope.auth;
37035
- const result = await localAuth.exchangeCode(body.code, body.client_id, body.redirect_uri, body.code_verifier);
37161
+ const result = await localAuth.exchangeCode(
37162
+ body.code,
37163
+ body.client_id,
37164
+ body.redirect_uri,
37165
+ body.code_verifier,
37166
+ body.client_secret
37167
+ );
37036
37168
  if ("error" in result) {
37037
37169
  this.logger.warn(`Code exchange failed: ${result.error}`);
37038
37170
  this.respond(
@@ -37079,7 +37211,7 @@ var init_oauth_token_flow = __esm({
37079
37211
  return;
37080
37212
  }
37081
37213
  const localAuth = this.scope.auth;
37082
- const result = await localAuth.refreshAccessToken(body.refresh_token, body.client_id);
37214
+ const result = await localAuth.refreshAccessToken(body.refresh_token, body.client_id, body.client_secret);
37083
37215
  if ("error" in result) {
37084
37216
  this.logger.warn(`Refresh token failed: ${result.error}`);
37085
37217
  this.respond(
@@ -37302,7 +37434,7 @@ var init_oauth_userinfo_flow = __esm({
37302
37434
  });
37303
37435
 
37304
37436
  // libs/sdk/src/auth/flows/session.verify.flow.ts
37305
- var import_reflect_metadata17, import_auth26, import_lazy_zod71, import_utils69, inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, SessionVerifyFlow;
37437
+ var import_reflect_metadata17, import_auth26, import_lazy_zod71, import_utils69, inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, warnedUnpinnedAudience, SessionVerifyFlow;
37306
37438
  var init_session_verify_flow = __esm({
37307
37439
  "libs/sdk/src/auth/flows/session.verify.flow.ts"() {
37308
37440
  "use strict";
@@ -37351,6 +37483,7 @@ var init_session_verify_flow = __esm({
37351
37483
  };
37352
37484
  name22 = "session:verify";
37353
37485
  Stage22 = StageHookOf(name22);
37486
+ warnedUnpinnedAudience = false;
37354
37487
  SessionVerifyFlow = class extends FlowBase {
37355
37488
  logger = this.scopeLogger.child("SessionVerifyFlow");
37356
37489
  maskSub(sub) {
@@ -37368,20 +37501,20 @@ var init_session_verify_flow = __esm({
37368
37501
  const machineId = (0, import_utils69.getMachineId)();
37369
37502
  if (sessionIdHeader) {
37370
37503
  const existingPayload = decryptPublicSession(sessionIdHeader);
37371
- const user2 = existingPayload ? { sub: `anon:${existingPayload.iat * 1e3}`, iss: issuer, name: "Anonymous", scope: scopes.join(" ") } : { sub: `anon:${(0, import_utils69.randomUUID)()}`, iss: issuer, name: "Anonymous", scope: scopes.join(" ") };
37372
- const finalPayload = existingPayload && existingPayload.nodeId === machineId ? existingPayload : void 0;
37373
- this.respond({
37374
- kind: "authorized",
37375
- authorization: {
37376
- token: "",
37377
- user: user2,
37378
- session: {
37379
- id: sessionIdHeader,
37380
- payload: finalPayload
37504
+ if (existingPayload && existingPayload.nodeId === machineId) {
37505
+ const anonId = existingPayload.uuid ?? `${existingPayload.iat * 1e3}`;
37506
+ const user2 = { sub: `anon:${anonId}`, iss: issuer, name: "Anonymous", scope: scopes.join(" ") };
37507
+ this.respond({
37508
+ kind: "authorized",
37509
+ authorization: {
37510
+ token: "",
37511
+ user: user2,
37512
+ session: { id: sessionIdHeader, payload: existingPayload }
37381
37513
  }
37382
- }
37383
- });
37384
- return;
37514
+ });
37515
+ return;
37516
+ }
37517
+ this.logger.verbose("createAnonymousSession: ignoring unrecognized client session id; minting a fresh one");
37385
37518
  }
37386
37519
  const now = Date.now();
37387
37520
  const user = {
@@ -37519,6 +37652,8 @@ var init_session_verify_flow = __esm({
37519
37652
  {
37520
37653
  id: primary.providerConfig?.id ?? "default",
37521
37654
  issuerUrl: issuer,
37655
+ additionalIssuers: primary.providerConfig?.additionalIssuers,
37656
+ verifyIssuer: primary.providerConfig?.verifyIssuer,
37522
37657
  jwks: primary.providerConfig?.jwks,
37523
37658
  jwksUri: primary.providerConfig?.jwksUri
37524
37659
  }
@@ -37530,6 +37665,37 @@ var init_session_verify_flow = __esm({
37530
37665
  const result = await verify;
37531
37666
  if (result.ok) {
37532
37667
  this.state.set({ jwtPayload: result.payload });
37668
+ if ((0, import_auth10.isTransparentMode)(authOptions)) {
37669
+ const configuredAudience = authOptions.expectedAudience;
37670
+ if (!configuredAudience && !isPublicUrlPinned() && !warnedUnpinnedAudience) {
37671
+ warnedUnpinnedAudience = true;
37672
+ this.logger.warn(
37673
+ "transparent audience validation is deriving the expected audience from the request Host: set auth.expectedAudience or FRONTMCP_PUBLIC_URL to bind tokens to a fixed resource. X-Forwarded-Host is ignored unless FRONTMCP_TRUST_PROXY is enabled."
37674
+ );
37675
+ }
37676
+ const expectedAudiences = configuredAudience ? Array.isArray(configuredAudience) ? configuredAudience : [configuredAudience] : (0, import_auth26.deriveExpectedAudience)(this.state.required.baseUrl);
37677
+ const audResult = (0, import_auth26.validateAudience)(result.payload?.["aud"], {
37678
+ expectedAudiences,
37679
+ // By default accept tokens with no `aud` claim: many IdPs omit it, and
37680
+ // rejecting them here would break existing transparent deployments.
37681
+ // Tokens that DO carry an `aud` for another service are still rejected,
37682
+ // which blocks the cross-service replay in GHSA-hvvp-67p3-j379.
37683
+ // Operators whose IdP always sets `aud` can flip `requireAudience:true`
37684
+ // to reject audience-less tokens outright.
37685
+ allowNoAudience: authOptions.requireAudience !== true
37686
+ });
37687
+ if (!audResult.valid) {
37688
+ this.logger.warn("verifyIfJwt: audience validation failed", { error: audResult.error });
37689
+ this.respond({
37690
+ kind: "unauthorized",
37691
+ prmMetadataHeader: (0, import_auth26.buildInvalidTokenHeader)(
37692
+ this.state.required.prmUrl,
37693
+ audResult.error ?? "Token audience is not valid for this resource"
37694
+ )
37695
+ });
37696
+ return;
37697
+ }
37698
+ }
37533
37699
  const requiredScopes = authOptions?.["requiredScopes"];
37534
37700
  if (requiredScopes && requiredScopes.length > 0) {
37535
37701
  const scopeClaim = result.payload?.["scope"];
@@ -37749,6 +37915,10 @@ var init_well_known_oauth_authorization_server_flow = __esm({
37749
37915
  kind: import_lazy_zod73.z.literal("json"),
37750
37916
  status: import_lazy_zod73.z.literal(200),
37751
37917
  contentType: import_lazy_zod73.z.literal("application/json; charset=utf-8"),
37918
+ // Response headers (e.g. `Cache-Control: no-store`). This flow uses a custom
37919
+ // output schema rather than `HttpJsonSchema`, so `headers` must be declared
37920
+ // explicitly for the renderer to emit it.
37921
+ headers: import_lazy_zod73.z.record(import_lazy_zod73.z.string(), import_lazy_zod73.z.string()).optional(),
37752
37922
  body: import_lazy_zod73.z.object({
37753
37923
  issuer: import_lazy_zod73.z.string().min(1),
37754
37924
  authorization_endpoint: import_lazy_zod73.z.string().min(1),
@@ -37830,6 +38000,10 @@ var init_well_known_oauth_authorization_server_flow = __esm({
37830
38000
  kind: "json",
37831
38001
  contentType: "application/json; charset=utf-8",
37832
38002
  status: 200,
38003
+ // Never cache: the advertised endpoints are derived from the request
38004
+ // origin, so a shared cache could serve a poisoned authorization/token
38005
+ // endpoint to another client (OAuth mix-up).
38006
+ headers: { "cache-control": "no-store" },
37833
38007
  body: {
37834
38008
  issuer: baseIssuer,
37835
38009
  authorization_endpoint: `${oauthBaseUrl}/oauth/authorize`,
@@ -37933,6 +38107,10 @@ var init_well_known_prm_flow = __esm({
37933
38107
  kind: "json",
37934
38108
  contentType: "application/json; charset=utf-8",
37935
38109
  status: 200,
38110
+ // Never cache: the body embeds the request-derived origin, so a shared
38111
+ // cache keyed only on (Host, path) could otherwise serve a poisoned
38112
+ // `authorization_servers` value to another client (OAuth mix-up).
38113
+ headers: { "cache-control": "no-store" },
37936
38114
  body: {
37937
38115
  resource,
37938
38116
  authorization_servers: [baseUrl],
@@ -37946,6 +38124,7 @@ var init_well_known_prm_flow = __esm({
37946
38124
  kind: "json",
37947
38125
  status: 200,
37948
38126
  contentType: "application/json; charset=utf-8",
38127
+ headers: { "cache-control": "no-store" },
37949
38128
  body: {
37950
38129
  resource,
37951
38130
  authorization_servers: [baseUrl],
@@ -38506,7 +38685,11 @@ var init_instance_local_primary_auth = __esm({
38506
38685
  /**
38507
38686
  * Exchange an authorization code for tokens
38508
38687
  */
38509
- async exchangeCode(code, clientId, redirectUri, codeVerifier) {
38688
+ async exchangeCode(code, clientId, redirectUri, codeVerifier, clientSecret) {
38689
+ if (this.dcrClientRegistryImpl.verifyClientSecret(clientId, clientSecret) === "invalid") {
38690
+ this.logger.warn("Client authentication failed at token endpoint (authorization_code)");
38691
+ return { error: "invalid_client", error_description: "Client authentication failed" };
38692
+ }
38510
38693
  const codeRecord = await this.authorizationStore.getAuthorizationCode(code);
38511
38694
  if (!codeRecord) {
38512
38695
  this.logger.warn(`Authorization code not found or expired: ${code.substring(0, 8)}...`);
@@ -38517,6 +38700,13 @@ var init_instance_local_primary_auth = __esm({
38517
38700
  }
38518
38701
  if (codeRecord.used) {
38519
38702
  this.logger.warn(`Authorization code already used: ${code.substring(0, 8)}...`);
38703
+ if (codeRecord.issuedRefreshToken) {
38704
+ try {
38705
+ await this.authorizationStore.revokeRefreshToken(codeRecord.issuedRefreshToken);
38706
+ } catch (err) {
38707
+ this.logger.warn(`Failed to revoke refresh token on authorization-code replay: ${err}`);
38708
+ }
38709
+ }
38520
38710
  await this.authorizationStore.deleteAuthorizationCode(code);
38521
38711
  return {
38522
38712
  error: "invalid_grant",
@@ -38578,9 +38768,19 @@ var init_instance_local_primary_auth = __esm({
38578
38768
  clientId,
38579
38769
  userSub: user.sub,
38580
38770
  scopes: codeRecord.scopes,
38581
- resource: codeRecord.resource
38771
+ resource: codeRecord.resource,
38772
+ userEmail: user.email,
38773
+ userName: user.name,
38774
+ consentEnabled: codeRecord.consentEnabled,
38775
+ selectedToolIds: codeRecord.selectedToolIds,
38776
+ authorizedAppIds: codeRecord.authorizedAppIds,
38777
+ customClaims: codeRecord.customClaims,
38778
+ federatedLoginUsed: codeRecord.federatedLoginUsed,
38779
+ selectedProviderIds: codeRecord.selectedProviderIds,
38780
+ skippedProviderIds: codeRecord.skippedProviderIds
38582
38781
  });
38583
38782
  await this.authorizationStore.storeRefreshToken(refreshTokenRecord);
38783
+ await this.authorizationStore.markCodeUsed(code, refreshTokenRecord.token);
38584
38784
  this.logger.info(`Tokens issued for user: ${user.sub}`);
38585
38785
  return {
38586
38786
  access_token: accessToken,
@@ -38593,7 +38793,11 @@ var init_instance_local_primary_auth = __esm({
38593
38793
  /**
38594
38794
  * Refresh an access token using a refresh token
38595
38795
  */
38596
- async refreshAccessToken(refreshToken, clientId) {
38796
+ async refreshAccessToken(refreshToken, clientId, clientSecret) {
38797
+ if (this.dcrClientRegistryImpl.verifyClientSecret(clientId, clientSecret) === "invalid") {
38798
+ this.logger.warn("Client authentication failed at token endpoint (refresh_token)");
38799
+ return { error: "invalid_client", error_description: "Client authentication failed" };
38800
+ }
38597
38801
  const tokenRecord = await this.authorizationStore.getRefreshToken(refreshToken);
38598
38802
  if (!tokenRecord) {
38599
38803
  this.logger.warn("Refresh token not found or expired");
@@ -38609,13 +38813,37 @@ var init_instance_local_primary_auth = __esm({
38609
38813
  error_description: "Client ID does not match"
38610
38814
  };
38611
38815
  }
38612
- const user = { sub: tokenRecord.userSub };
38613
- const accessToken = await this.signAccessToken(user, tokenRecord.scopes, tokenRecord.resource);
38816
+ const user = {
38817
+ sub: tokenRecord.userSub,
38818
+ email: tokenRecord.userEmail,
38819
+ name: tokenRecord.userName
38820
+ };
38821
+ const hasAuthorizedApps = Array.isArray(tokenRecord.authorizedAppIds);
38822
+ const hasCustomClaims = !!tokenRecord.customClaims && Object.keys(tokenRecord.customClaims).length > 0;
38823
+ const consentMetadata = tokenRecord.consentEnabled || tokenRecord.federatedLoginUsed || hasCustomClaims || hasAuthorizedApps ? {
38824
+ selectedToolIds: tokenRecord.selectedToolIds,
38825
+ selectedProviderIds: tokenRecord.selectedProviderIds,
38826
+ skippedProviderIds: tokenRecord.skippedProviderIds,
38827
+ consentEnabled: tokenRecord.consentEnabled,
38828
+ federatedLoginUsed: tokenRecord.federatedLoginUsed,
38829
+ authorizedAppIds: tokenRecord.authorizedAppIds,
38830
+ customClaims: tokenRecord.customClaims
38831
+ } : void 0;
38832
+ const accessToken = await this.signAccessToken(user, tokenRecord.scopes, tokenRecord.resource, consentMetadata);
38614
38833
  const newRefreshRecord = this.authorizationStore.createRefreshTokenRecord({
38615
38834
  clientId,
38616
38835
  userSub: tokenRecord.userSub,
38617
38836
  scopes: tokenRecord.scopes,
38618
- resource: tokenRecord.resource
38837
+ resource: tokenRecord.resource,
38838
+ userEmail: tokenRecord.userEmail,
38839
+ userName: tokenRecord.userName,
38840
+ consentEnabled: tokenRecord.consentEnabled,
38841
+ selectedToolIds: tokenRecord.selectedToolIds,
38842
+ authorizedAppIds: tokenRecord.authorizedAppIds,
38843
+ customClaims: tokenRecord.customClaims,
38844
+ federatedLoginUsed: tokenRecord.federatedLoginUsed,
38845
+ selectedProviderIds: tokenRecord.selectedProviderIds,
38846
+ skippedProviderIds: tokenRecord.skippedProviderIds
38619
38847
  });
38620
38848
  await this.authorizationStore.rotateRefreshToken(refreshToken, newRefreshRecord);
38621
38849
  this.logger.info(`Tokens refreshed for user: ${user.sub}`);
@@ -38915,13 +39143,22 @@ var init_instance_local_primary_auth = __esm({
38915
39143
  try {
38916
39144
  const [, payloadB64] = idToken.split(".");
38917
39145
  const payload = JSON.parse(new TextDecoder().decode((0, import_utils71.base64urlDecode)(payloadB64)));
38918
- return {
38919
- sub: payload["sub"],
38920
- email: payload["email"],
38921
- name: payload["name"],
38922
- picture: payload["picture"],
38923
- claims: payload
38924
- };
39146
+ const exp = typeof payload["exp"] === "number" ? payload["exp"] : void 0;
39147
+ if (exp !== void 0 && exp * 1e3 < Date.now()) {
39148
+ this.logger.warn(`ID token for ${providerId} is expired; falling back to userinfo`);
39149
+ } else {
39150
+ const sub = typeof payload["sub"] === "string" && payload["sub"].trim() ? payload["sub"] : void 0;
39151
+ if (sub) {
39152
+ return {
39153
+ sub,
39154
+ email: payload["email"],
39155
+ name: payload["name"],
39156
+ picture: payload["picture"],
39157
+ claims: payload
39158
+ };
39159
+ }
39160
+ this.logger.warn(`ID token for ${providerId} has no usable sub; falling back to userinfo`);
39161
+ }
38925
39162
  } catch (err) {
38926
39163
  this.logger.warn(`Failed to parse ID token for ${providerId}: ${err}`);
38927
39164
  }
@@ -38936,21 +39173,25 @@ var init_instance_local_primary_auth = __esm({
38936
39173
  });
38937
39174
  if (response.ok) {
38938
39175
  const userInfo = await response.json();
38939
- return {
38940
- sub: userInfo["sub"],
38941
- email: userInfo["email"],
38942
- name: userInfo["name"],
38943
- picture: userInfo["picture"],
38944
- claims: userInfo
38945
- };
39176
+ const sub = typeof userInfo["sub"] === "string" && userInfo["sub"].trim() ? userInfo["sub"] : void 0;
39177
+ if (sub) {
39178
+ return {
39179
+ sub,
39180
+ email: userInfo["email"],
39181
+ name: userInfo["name"],
39182
+ picture: userInfo["picture"],
39183
+ claims: userInfo
39184
+ };
39185
+ }
39186
+ this.logger.warn(`userinfo for ${providerId} returned no usable sub`);
38946
39187
  }
38947
39188
  } catch (err) {
38948
39189
  this.logger.warn(`Failed to get userinfo from ${providerId}: ${err}`);
38949
39190
  }
38950
39191
  }
38951
- return {
38952
- sub: `${providerId}:unknown`
38953
- };
39192
+ throw new Error(
39193
+ `Unable to determine a stable user identity from provider "${providerId}" (no id_token sub and no usable userinfo endpoint)`
39194
+ );
38954
39195
  }
38955
39196
  /**
38956
39197
  * Refresh tokens from upstream provider
@@ -49169,6 +49410,16 @@ function decideIntent(req, cfg) {
49169
49410
  debug: { key, channel: channel2, flags }
49170
49411
  };
49171
49412
  }
49413
+ function authorizeSessionTermination(verifyResult, requestedSessionId) {
49414
+ if (!verifyResult || verifyResult.kind !== "authorized" || !verifyResult.authorization) {
49415
+ return { kind: "unauthorized" };
49416
+ }
49417
+ const ownedSessionId = verifyResult.authorization.session?.id;
49418
+ if (!ownedSessionId || !requestedSessionId || requestedSessionId !== ownedSessionId) {
49419
+ return { kind: "forbidden" };
49420
+ }
49421
+ return { kind: "ok", sessionId: ownedSessionId };
49422
+ }
49172
49423
  var import_lazy_zod99, intentSchema, decisionSchema, CH_OTHER, CH_GET_SSE, CH_POST_INIT_JSON, CH_POST_INIT_SSE, CH_POST_JSON, CH_POST_SSE, CH_POST_MESSAGE, CH_GET_SSE_PATH, CH_MASK, B_HAS_SESSION, B_STATELESS_EN, B_REQ_SESSION, B_LEGACY_HINT, B_SSE_LISTENER_EN, B_STREAMABLE_EN, B_STATEFUL_EN, B_LEGACY_EN, h, wantsSSE, wantsJSON, isInitialize, RULES;
49173
49424
  var init_decide_request_intent_utils = __esm({
49174
49425
  "libs/sdk/src/common/utils/decide-request-intent.utils.ts"() {
@@ -49431,13 +49682,29 @@ function readPublicUrlPin() {
49431
49682
  }
49432
49683
  return void 0;
49433
49684
  }
49685
+ function isPublicUrlPinned() {
49686
+ return readPublicUrlPin() !== void 0;
49687
+ }
49688
+ function isProxyTrusted() {
49689
+ try {
49690
+ if (typeof process !== "undefined" && process.env) {
49691
+ const v = process.env["FRONTMCP_TRUST_PROXY"];
49692
+ return typeof v === "string" && /^(1|true|yes|on)$/i.test(v.trim());
49693
+ }
49694
+ } catch {
49695
+ }
49696
+ return false;
49697
+ }
49434
49698
  function getRequestBaseUrl(req, entryPath) {
49435
49699
  const pin = readPublicUrlPin();
49436
49700
  if (pin) {
49437
49701
  return `${pin}${entryPath ?? ""}`;
49438
49702
  }
49439
- const proto = req.headers["x-forwarded-proto"] || req.protocol || "http";
49440
- const host = req.headers["x-forwarded-host"] || req.headers["host"];
49703
+ const trustProxy = isProxyTrusted();
49704
+ const fwdProto = trustProxy ? req.headers["x-forwarded-proto"] : void 0;
49705
+ const fwdHost = trustProxy ? req.headers["x-forwarded-host"] : void 0;
49706
+ const proto = fwdProto || req.protocol || "http";
49707
+ const host = fwdHost || req.headers["host"];
49441
49708
  return `${proto}://${host}${entryPath ?? ""}`;
49442
49709
  }
49443
49710
  function computeIssuer(req, entryPath, scopeBase) {
@@ -54196,6 +54463,10 @@ var init_web_standard_mcp = __esm({
54196
54463
  });
54197
54464
 
54198
54465
  // libs/sdk/src/scope/flows/http.request.flow.ts
54466
+ function parseUserScopes(user) {
54467
+ const s = user?.scope;
54468
+ return typeof s === "string" ? s.split(/\s+/).filter(Boolean) : [];
54469
+ }
54199
54470
  var import_auth38, import_lazy_zod105, import_utils94, plan39, httpRequestStateSchema, name40, Stage40, HttpRequestFlow;
54200
54471
  var init_http_request_flow = __esm({
54201
54472
  "libs/sdk/src/scope/flows/http.request.flow.ts"() {
@@ -54340,7 +54611,10 @@ var init_http_request_flow = __esm({
54340
54611
  ctx.updateAuthInfo({
54341
54612
  token,
54342
54613
  clientId: user.sub,
54343
- scopes: [],
54614
+ // Populate scopes from the verified token's `scope` claim instead of
54615
+ // hardcoding `[]`, so any consumer that authorizes on
54616
+ // `authInfo.scopes` sees the real grant (same accessor as the web path).
54617
+ scopes: parseUserScopes(user),
54344
54618
  // JWT exp is in seconds, SDK uses milliseconds throughout (e.g., Date.now())
54345
54619
  expiresAt: user.exp ? user.exp * 1e3 : void 0,
54346
54620
  extra: {
@@ -54459,6 +54733,27 @@ var init_http_request_flow = __esm({
54459
54733
  this.next();
54460
54734
  return;
54461
54735
  }
54736
+ const deleteVerify = this.state.required.verifyResult;
54737
+ const requestedSessionId = request[ServerRequestTokens.sessionId] ?? request.headers["mcp-session-id"] ?? request.query?.["sessionId"];
54738
+ const deleteAuthz = authorizeSessionTermination(deleteVerify, requestedSessionId);
54739
+ if (deleteAuthz.kind === "unauthorized") {
54740
+ this.logger.warn(`[${this.requestId}] DELETE session denied: request is not authenticated`);
54741
+ this.respond(
54742
+ httpRespond.unauthorized({
54743
+ headers: {
54744
+ "WWW-Authenticate": deleteVerify.prmMetadataHeader ?? "Bearer"
54745
+ }
54746
+ })
54747
+ );
54748
+ return;
54749
+ }
54750
+ if (deleteAuthz.kind === "forbidden") {
54751
+ this.logger.warn(`[${this.requestId}] DELETE session denied: caller does not own the requested session id`);
54752
+ this.respond(httpRespond.notFound("Session not found"));
54753
+ return;
54754
+ }
54755
+ request[ServerRequestTokens.auth] = deleteVerify.authorization;
54756
+ request[ServerRequestTokens.sessionId] = deleteAuthz.sessionId;
54462
54757
  this.state.set("intent", decision.intent);
54463
54758
  return;
54464
54759
  }
@@ -54571,7 +54866,9 @@ var init_http_request_flow = __esm({
54571
54866
  const authInfo = authorization ? {
54572
54867
  token: authorization.token,
54573
54868
  clientId: authorization.user?.sub,
54574
- scopes: [],
54869
+ // Same scope parsing as the Node path so scope-aware handlers behave
54870
+ // identically on the web transport.
54871
+ scopes: parseUserScopes(authorization.user),
54575
54872
  expiresAt: authorization.user?.exp ? authorization.user.exp * 1e3 : void 0,
54576
54873
  extra: {
54577
54874
  user: authorization.user,
@@ -57530,6 +57827,17 @@ function createHostValidationMiddleware(options) {
57530
57827
  });
57531
57828
  return;
57532
57829
  }
57830
+ const forwardedHost = req.headers?.["x-forwarded-host"];
57831
+ if (forwardedHost) {
57832
+ const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
57833
+ if (candidates.some((h2) => !allowedHostsSet.has(h2))) {
57834
+ res.status(403).json({
57835
+ error: "Forbidden",
57836
+ message: "Invalid X-Forwarded-Host header"
57837
+ });
57838
+ return;
57839
+ }
57840
+ }
57533
57841
  }
57534
57842
  if (allowedOriginsSet) {
57535
57843
  const origin = req.headers?.["origin"];
@@ -60958,6 +61266,7 @@ __export(index_exports, {
60958
61266
  authorizationRequiredMetaSchema: () => authorizationRequiredMetaSchema,
60959
61267
  authorizationRequiredParamsSchema: () => authorizationRequiredParamsSchema,
60960
61268
  authorizationSchema: () => authorizationSchema,
61269
+ authorizeSessionTermination: () => authorizeSessionTermination,
60961
61270
  buildChannelInstructions: () => buildChannelInstructions,
60962
61271
  buildEsmPromptRecord: () => buildEsmPromptRecord,
60963
61272
  buildEsmResourceRecord: () => buildEsmResourceRecord,
@@ -61131,6 +61440,7 @@ __export(index_exports, {
61131
61440
  isPackageSpecifier: () => isPackageSpecifier,
61132
61441
  isPublicError: () => isPublicError,
61133
61442
  isPublicMode: () => import_auth10.isPublicMode,
61443
+ isPublicUrlPinned: () => isPublicUrlPinned,
61134
61444
  isPubsubConfigured: () => isPubsubConfigured,
61135
61445
  isRedisProvider: () => isRedisProvider,
61136
61446
  isRemoteMode: () => import_auth10.isRemoteMode,
@@ -62144,6 +62454,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
62144
62454
  authorizationRequiredMetaSchema,
62145
62455
  authorizationRequiredParamsSchema,
62146
62456
  authorizationSchema,
62457
+ authorizeSessionTermination,
62147
62458
  buildChannelInstructions,
62148
62459
  buildEsmPromptRecord,
62149
62460
  buildEsmResourceRecord,
@@ -62316,6 +62627,7 @@ var ChannelListHook = FlowHooksOf("channels:list");
62316
62627
  isPackageSpecifier,
62317
62628
  isPublicError,
62318
62629
  isPublicMode,
62630
+ isPublicUrlPinned,
62319
62631
  isPubsubConfigured,
62320
62632
  isRedisProvider,
62321
62633
  isRemoteMode,