@frontmcp/adapters 1.8.2 → 1.8.3
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/esm/index.mjs +65 -26
- package/esm/openapi/index.mjs +65 -26
- package/esm/package.json +6 -6
- package/esm/skills/index.mjs +94 -12
- package/index.js +64 -25
- package/openapi/index.js +64 -25
- package/openapi/openapi.security.d.ts +3 -3
- package/openapi/openapi.security.d.ts.map +1 -1
- package/openapi/openapi.types.d.ts +17 -0
- package/openapi/openapi.types.d.ts.map +1 -1
- package/package.json +6 -6
- package/skills/index.js +94 -12
- package/skills/source-options.d.ts.map +1 -1
- package/skills/sources/npm.source.d.ts +9 -4
- package/skills/sources/npm.source.d.ts.map +1 -1
- package/skills/sources/saas-pull.source.d.ts +25 -2
- package/skills/sources/saas-pull.source.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -183,13 +183,13 @@ var OpenApiSpecPoller = class {
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
// libs/adapters/src/openapi/openapi.security.ts
|
|
186
|
-
import {
|
|
186
|
+
import { createSecurityContext, SecurityResolver } from "mcp-from-openapi";
|
|
187
187
|
async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
188
188
|
if (options.securityResolver) {
|
|
189
189
|
return await options.securityResolver(tool2, ctx);
|
|
190
190
|
}
|
|
191
191
|
if (options.authProviderMapper) {
|
|
192
|
-
const context =
|
|
192
|
+
const context = {};
|
|
193
193
|
const securitySchemes = /* @__PURE__ */ new Set();
|
|
194
194
|
for (const mapper of tool2.mapper) {
|
|
195
195
|
if (mapper.security?.scheme) {
|
|
@@ -242,22 +242,38 @@ async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
+
if (hasStaticAuth(options)) {
|
|
246
|
+
return createSecurityContext({ ...options.staticAuth, ...context });
|
|
247
|
+
}
|
|
245
248
|
const hasAnyAuth = context.jwt || context.apiKey || context.basic || context.oauth2Token;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
249
|
+
if (!hasAnyAuth && options.passthroughCallerToken === true) {
|
|
250
|
+
const callerToken = getCallerToken(ctx);
|
|
251
|
+
if (callerToken) {
|
|
252
|
+
context.jwt = callerToken;
|
|
250
253
|
}
|
|
251
|
-
context.jwt = authToken;
|
|
252
254
|
}
|
|
253
|
-
return context;
|
|
255
|
+
return createSecurityContext(context);
|
|
254
256
|
}
|
|
255
|
-
if (options
|
|
257
|
+
if (hasStaticAuth(options)) {
|
|
256
258
|
return createSecurityContext(options.staticAuth);
|
|
257
259
|
}
|
|
258
|
-
|
|
259
|
-
jwt: ctx
|
|
260
|
-
}
|
|
260
|
+
if (options.passthroughCallerToken === true) {
|
|
261
|
+
return createSecurityContext({ jwt: getCallerToken(ctx) });
|
|
262
|
+
}
|
|
263
|
+
return createSecurityContext({});
|
|
264
|
+
}
|
|
265
|
+
function hasStaticAuth(options) {
|
|
266
|
+
return !!options.staticAuth && Object.keys(options.staticAuth).length > 0;
|
|
267
|
+
}
|
|
268
|
+
function getCallerToken(ctx) {
|
|
269
|
+
const authToken = ctx.authInfo?.token;
|
|
270
|
+
if (authToken === void 0 || authToken === null || authToken === "") {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
if (typeof authToken !== "string") {
|
|
274
|
+
throw new Error(`authInfo.token must be a string, but got: ${typeof authToken}`);
|
|
275
|
+
}
|
|
276
|
+
return authToken;
|
|
261
277
|
}
|
|
262
278
|
function extractSecuritySchemes(tools) {
|
|
263
279
|
const schemes = /* @__PURE__ */ new Set();
|
|
@@ -293,12 +309,12 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
293
309
|
);
|
|
294
310
|
return result;
|
|
295
311
|
}
|
|
296
|
-
if (
|
|
312
|
+
if (hasStaticAuth(options)) {
|
|
297
313
|
result.securityRiskScore = "medium";
|
|
298
314
|
result.warnings.push(
|
|
299
315
|
"SECURITY INFO: Using staticAuth with hardcoded credentials. Ensure credentials are stored securely (environment variables, secrets manager)."
|
|
300
316
|
);
|
|
301
|
-
return result;
|
|
317
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
302
318
|
}
|
|
303
319
|
const schemesInInput = new Set(options.securitySchemesInInput || []);
|
|
304
320
|
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
@@ -308,13 +324,18 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
308
324
|
`INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
|
|
309
325
|
);
|
|
310
326
|
}
|
|
327
|
+
const passthroughFallback = [];
|
|
311
328
|
for (const scheme of securitySchemes) {
|
|
312
329
|
if (schemesInInput.has(scheme)) {
|
|
313
330
|
continue;
|
|
314
331
|
}
|
|
315
332
|
if (!options.authProviderMapper?.[scheme]) {
|
|
316
|
-
|
|
317
|
-
|
|
333
|
+
if (options.passthroughCallerToken === true) {
|
|
334
|
+
passthroughFallback.push(scheme);
|
|
335
|
+
} else {
|
|
336
|
+
result.valid = false;
|
|
337
|
+
result.missingMappings.push(scheme);
|
|
338
|
+
}
|
|
318
339
|
}
|
|
319
340
|
}
|
|
320
341
|
if (!result.valid) {
|
|
@@ -322,19 +343,36 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
322
343
|
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
323
344
|
);
|
|
324
345
|
}
|
|
346
|
+
withPassthroughRisk(result, securitySchemes, options);
|
|
347
|
+
if (passthroughFallback.length > 0) {
|
|
348
|
+
result.warnings.push(
|
|
349
|
+
`SECURITY WARNING: Security schemes with no authProviderMapper entry (${passthroughFallback.join(", ")}) get the MCP client's own token, because passthroughCallerToken is enabled.`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
325
352
|
return result;
|
|
326
353
|
}
|
|
327
|
-
if (securitySchemes.size > 0) {
|
|
354
|
+
if (securitySchemes.size > 0 && options.passthroughCallerToken !== true) {
|
|
355
|
+
const schemesStr = Array.from(securitySchemes).join(", ");
|
|
328
356
|
result.securityRiskScore = "medium";
|
|
329
357
|
result.warnings.push(
|
|
330
|
-
`
|
|
331
|
-
securitySchemes
|
|
332
|
-
).join(", ")}`
|
|
333
|
-
);
|
|
334
|
-
result.warnings.push(
|
|
335
|
-
"RECOMMENDATION: For multiple auth providers, use authProviderMapper or securityResolver to map each security scheme to the correct auth provider."
|
|
358
|
+
`SECURITY WARNING: No auth configuration provided, so the adapter has no credentials for the API (security schemes: ${schemesStr}). Operations that require authentication will fail. The MCP client's own token is not forwarded: configure authProviderMapper, securityResolver or staticAuth, or set passthroughCallerToken: true if the API is meant to receive the caller's MCP token.`
|
|
336
359
|
);
|
|
337
360
|
}
|
|
361
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
362
|
+
}
|
|
363
|
+
function canPassThroughCallerToken(options) {
|
|
364
|
+
return options.passthroughCallerToken === true && !options.securityResolver && !hasStaticAuth(options);
|
|
365
|
+
}
|
|
366
|
+
function withPassthroughRisk(result, securitySchemes, options) {
|
|
367
|
+
if (securitySchemes.size === 0 || !canPassThroughCallerToken(options)) {
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
const schemesStr = Array.from(securitySchemes).join(", ");
|
|
371
|
+
const when = options.authProviderMapper ? " whenever no authProviderMapper function returns a credential" : "";
|
|
372
|
+
result.securityRiskScore = "high";
|
|
373
|
+
result.warnings.push(
|
|
374
|
+
`SECURITY WARNING: passthroughCallerToken is enabled. The MCP client's own token (ctx.authInfo.token) is sent to the API${when} for security schemes: ${schemesStr}. Only use this when the API accepts tokens issued for this MCP server.`
|
|
375
|
+
);
|
|
338
376
|
return result;
|
|
339
377
|
}
|
|
340
378
|
async function resolveToolSecurity(tool2, ctx, options) {
|
|
@@ -352,9 +390,10 @@ async function resolveToolSecurity(tool2, ctx, options) {
|
|
|
352
390
|
Required security schemes: ${schemesStr}
|
|
353
391
|
Solutions:
|
|
354
392
|
1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
|
|
355
|
-
2. Add securityResolver: (tool, ctx) => ({ jwt: ctx
|
|
393
|
+
2. Add securityResolver: async (tool, ctx) => ({ jwt: await getApiToken(ctx) })
|
|
356
394
|
3. Add staticAuth: { jwt: process.env.API_TOKEN }
|
|
357
|
-
4. Set
|
|
395
|
+
4. Set passthroughCallerToken: true, only if the API accepts the MCP client's own token
|
|
396
|
+
5. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
|
|
358
397
|
);
|
|
359
398
|
}
|
|
360
399
|
return await securityResolver.resolve(tool2.mapper, securityContext);
|
|
@@ -1155,7 +1194,7 @@ Add one of the following to your adapter configuration:
|
|
|
1155
1194
|
}
|
|
1156
1195
|
|
|
1157
1196
|
2. securityResolver:
|
|
1158
|
-
securityResolver: (tool,
|
|
1197
|
+
securityResolver: async (tool, ctx) => ({ jwt: await getApiToken(ctx) })
|
|
1159
1198
|
|
|
1160
1199
|
3. staticAuth:
|
|
1161
1200
|
staticAuth: { jwt: process.env.API_TOKEN }
|
package/esm/openapi/index.mjs
CHANGED
|
@@ -183,13 +183,13 @@ var OpenApiSpecPoller = class {
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
// libs/adapters/src/openapi/openapi.security.ts
|
|
186
|
-
import {
|
|
186
|
+
import { createSecurityContext, SecurityResolver } from "mcp-from-openapi";
|
|
187
187
|
async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
188
188
|
if (options.securityResolver) {
|
|
189
189
|
return await options.securityResolver(tool2, ctx);
|
|
190
190
|
}
|
|
191
191
|
if (options.authProviderMapper) {
|
|
192
|
-
const context =
|
|
192
|
+
const context = {};
|
|
193
193
|
const securitySchemes = /* @__PURE__ */ new Set();
|
|
194
194
|
for (const mapper of tool2.mapper) {
|
|
195
195
|
if (mapper.security?.scheme) {
|
|
@@ -242,22 +242,38 @@ async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
+
if (hasStaticAuth(options)) {
|
|
246
|
+
return createSecurityContext({ ...options.staticAuth, ...context });
|
|
247
|
+
}
|
|
245
248
|
const hasAnyAuth = context.jwt || context.apiKey || context.basic || context.oauth2Token;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
249
|
+
if (!hasAnyAuth && options.passthroughCallerToken === true) {
|
|
250
|
+
const callerToken = getCallerToken(ctx);
|
|
251
|
+
if (callerToken) {
|
|
252
|
+
context.jwt = callerToken;
|
|
250
253
|
}
|
|
251
|
-
context.jwt = authToken;
|
|
252
254
|
}
|
|
253
|
-
return context;
|
|
255
|
+
return createSecurityContext(context);
|
|
254
256
|
}
|
|
255
|
-
if (options
|
|
257
|
+
if (hasStaticAuth(options)) {
|
|
256
258
|
return createSecurityContext(options.staticAuth);
|
|
257
259
|
}
|
|
258
|
-
|
|
259
|
-
jwt: ctx
|
|
260
|
-
}
|
|
260
|
+
if (options.passthroughCallerToken === true) {
|
|
261
|
+
return createSecurityContext({ jwt: getCallerToken(ctx) });
|
|
262
|
+
}
|
|
263
|
+
return createSecurityContext({});
|
|
264
|
+
}
|
|
265
|
+
function hasStaticAuth(options) {
|
|
266
|
+
return !!options.staticAuth && Object.keys(options.staticAuth).length > 0;
|
|
267
|
+
}
|
|
268
|
+
function getCallerToken(ctx) {
|
|
269
|
+
const authToken = ctx.authInfo?.token;
|
|
270
|
+
if (authToken === void 0 || authToken === null || authToken === "") {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
if (typeof authToken !== "string") {
|
|
274
|
+
throw new Error(`authInfo.token must be a string, but got: ${typeof authToken}`);
|
|
275
|
+
}
|
|
276
|
+
return authToken;
|
|
261
277
|
}
|
|
262
278
|
function extractSecuritySchemes(tools) {
|
|
263
279
|
const schemes = /* @__PURE__ */ new Set();
|
|
@@ -293,12 +309,12 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
293
309
|
);
|
|
294
310
|
return result;
|
|
295
311
|
}
|
|
296
|
-
if (
|
|
312
|
+
if (hasStaticAuth(options)) {
|
|
297
313
|
result.securityRiskScore = "medium";
|
|
298
314
|
result.warnings.push(
|
|
299
315
|
"SECURITY INFO: Using staticAuth with hardcoded credentials. Ensure credentials are stored securely (environment variables, secrets manager)."
|
|
300
316
|
);
|
|
301
|
-
return result;
|
|
317
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
302
318
|
}
|
|
303
319
|
const schemesInInput = new Set(options.securitySchemesInInput || []);
|
|
304
320
|
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
@@ -308,13 +324,18 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
308
324
|
`INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
|
|
309
325
|
);
|
|
310
326
|
}
|
|
327
|
+
const passthroughFallback = [];
|
|
311
328
|
for (const scheme of securitySchemes) {
|
|
312
329
|
if (schemesInInput.has(scheme)) {
|
|
313
330
|
continue;
|
|
314
331
|
}
|
|
315
332
|
if (!options.authProviderMapper?.[scheme]) {
|
|
316
|
-
|
|
317
|
-
|
|
333
|
+
if (options.passthroughCallerToken === true) {
|
|
334
|
+
passthroughFallback.push(scheme);
|
|
335
|
+
} else {
|
|
336
|
+
result.valid = false;
|
|
337
|
+
result.missingMappings.push(scheme);
|
|
338
|
+
}
|
|
318
339
|
}
|
|
319
340
|
}
|
|
320
341
|
if (!result.valid) {
|
|
@@ -322,19 +343,36 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
322
343
|
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
323
344
|
);
|
|
324
345
|
}
|
|
346
|
+
withPassthroughRisk(result, securitySchemes, options);
|
|
347
|
+
if (passthroughFallback.length > 0) {
|
|
348
|
+
result.warnings.push(
|
|
349
|
+
`SECURITY WARNING: Security schemes with no authProviderMapper entry (${passthroughFallback.join(", ")}) get the MCP client's own token, because passthroughCallerToken is enabled.`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
325
352
|
return result;
|
|
326
353
|
}
|
|
327
|
-
if (securitySchemes.size > 0) {
|
|
354
|
+
if (securitySchemes.size > 0 && options.passthroughCallerToken !== true) {
|
|
355
|
+
const schemesStr = Array.from(securitySchemes).join(", ");
|
|
328
356
|
result.securityRiskScore = "medium";
|
|
329
357
|
result.warnings.push(
|
|
330
|
-
`
|
|
331
|
-
securitySchemes
|
|
332
|
-
).join(", ")}`
|
|
333
|
-
);
|
|
334
|
-
result.warnings.push(
|
|
335
|
-
"RECOMMENDATION: For multiple auth providers, use authProviderMapper or securityResolver to map each security scheme to the correct auth provider."
|
|
358
|
+
`SECURITY WARNING: No auth configuration provided, so the adapter has no credentials for the API (security schemes: ${schemesStr}). Operations that require authentication will fail. The MCP client's own token is not forwarded: configure authProviderMapper, securityResolver or staticAuth, or set passthroughCallerToken: true if the API is meant to receive the caller's MCP token.`
|
|
336
359
|
);
|
|
337
360
|
}
|
|
361
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
362
|
+
}
|
|
363
|
+
function canPassThroughCallerToken(options) {
|
|
364
|
+
return options.passthroughCallerToken === true && !options.securityResolver && !hasStaticAuth(options);
|
|
365
|
+
}
|
|
366
|
+
function withPassthroughRisk(result, securitySchemes, options) {
|
|
367
|
+
if (securitySchemes.size === 0 || !canPassThroughCallerToken(options)) {
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
const schemesStr = Array.from(securitySchemes).join(", ");
|
|
371
|
+
const when = options.authProviderMapper ? " whenever no authProviderMapper function returns a credential" : "";
|
|
372
|
+
result.securityRiskScore = "high";
|
|
373
|
+
result.warnings.push(
|
|
374
|
+
`SECURITY WARNING: passthroughCallerToken is enabled. The MCP client's own token (ctx.authInfo.token) is sent to the API${when} for security schemes: ${schemesStr}. Only use this when the API accepts tokens issued for this MCP server.`
|
|
375
|
+
);
|
|
338
376
|
return result;
|
|
339
377
|
}
|
|
340
378
|
async function resolveToolSecurity(tool2, ctx, options) {
|
|
@@ -352,9 +390,10 @@ async function resolveToolSecurity(tool2, ctx, options) {
|
|
|
352
390
|
Required security schemes: ${schemesStr}
|
|
353
391
|
Solutions:
|
|
354
392
|
1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
|
|
355
|
-
2. Add securityResolver: (tool, ctx) => ({ jwt: ctx
|
|
393
|
+
2. Add securityResolver: async (tool, ctx) => ({ jwt: await getApiToken(ctx) })
|
|
356
394
|
3. Add staticAuth: { jwt: process.env.API_TOKEN }
|
|
357
|
-
4. Set
|
|
395
|
+
4. Set passthroughCallerToken: true, only if the API accepts the MCP client's own token
|
|
396
|
+
5. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
|
|
358
397
|
);
|
|
359
398
|
}
|
|
360
399
|
return await securityResolver.resolve(tool2.mapper, securityContext);
|
|
@@ -1155,7 +1194,7 @@ Add one of the following to your adapter configuration:
|
|
|
1155
1194
|
}
|
|
1156
1195
|
|
|
1157
1196
|
2. securityResolver:
|
|
1158
|
-
securityResolver: (tool,
|
|
1197
|
+
securityResolver: async (tool, ctx) => ({ jwt: await getApiToken(ctx) })
|
|
1159
1198
|
|
|
1160
1199
|
3. staticAuth:
|
|
1161
1200
|
staticAuth: { jwt: process.env.API_TOKEN }
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/adapters",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"description": "Adapters for the FrontMCP framework",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
"node": ">=24.0.0"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@frontmcp/auth": "1.8.
|
|
68
|
-
"@frontmcp/di": "1.8.
|
|
69
|
-
"@frontmcp/sdk": "1.8.
|
|
70
|
-
"@frontmcp/utils": "1.8.
|
|
67
|
+
"@frontmcp/auth": "1.8.3",
|
|
68
|
+
"@frontmcp/di": "1.8.3",
|
|
69
|
+
"@frontmcp/sdk": "1.8.3",
|
|
70
|
+
"@frontmcp/utils": "1.8.3",
|
|
71
71
|
"js-yaml": "^4.1.0",
|
|
72
72
|
"mcp-from-openapi": "2.8.0",
|
|
73
73
|
"openapi-types": "^12.1.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
76
|
+
"@frontmcp/lazy-zod": "1.8.3"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/esm/skills/index.mjs
CHANGED
|
@@ -611,7 +611,13 @@ var npmSourceSchema = z2.object({
|
|
|
611
611
|
exportName: z2.string().optional(),
|
|
612
612
|
/**
|
|
613
613
|
* Verify GitHub artifact attestation / npm provenance for the package before loading.
|
|
614
|
-
* Defaults to true.
|
|
614
|
+
* Defaults to true. Provenance verification is not implemented yet, so `true` refuses
|
|
615
|
+
* to load the package; set `false` to load it without a provenance check.
|
|
616
|
+
*
|
|
617
|
+
* With `false`, the package is imported (its code runs) before the bundle is parsed, so
|
|
618
|
+
* bundle signing (`requireSignature` + `trustedKeys`) cannot protect the import itself.
|
|
619
|
+
* Only use it for a package you trust: pin its exact version in `package.json` and
|
|
620
|
+
* install from a lockfile with integrity hashes.
|
|
615
621
|
*/
|
|
616
622
|
verifyProvenance: z2.boolean().default(true)
|
|
617
623
|
});
|
|
@@ -619,11 +625,15 @@ var saasSourceSchema = z2.object({
|
|
|
619
625
|
type: z2.literal("saas"),
|
|
620
626
|
/** SaaS pull endpoint, e.g. `https://cloud.frontmcp.dev/v1/bundles/<bundleId>`. */
|
|
621
627
|
endpoint: httpsUrl,
|
|
622
|
-
/**
|
|
628
|
+
/**
|
|
629
|
+
* Pinned JWT issued by the SaaS for the customer's FrontMCP server. Verified
|
|
630
|
+
* before every pull: signed by a key in `jwksUrl`, `iss` = `expectedIssuer`,
|
|
631
|
+
* `aud` includes `expectedAudience`, not expired. A rejected token stops the pull.
|
|
632
|
+
*/
|
|
623
633
|
authToken: z2.string().min(1),
|
|
624
634
|
/**
|
|
625
|
-
* RFC 8707 resource indicator the JWT must encode
|
|
626
|
-
*
|
|
635
|
+
* RFC 8707 resource indicator the JWT must encode: its `aud` claim must include
|
|
636
|
+
* this value (and its `resource` claim too, when it has one).
|
|
627
637
|
*/
|
|
628
638
|
expectedAudience: z2.string().min(1),
|
|
629
639
|
/** Polling interval in ms (boot pull is always immediate). Default 300000 (5 min). */
|
|
@@ -633,9 +643,9 @@ var saasSourceSchema = z2.object({
|
|
|
633
643
|
* updates synchronously. Default false in v1.2 (interval polling only).
|
|
634
644
|
*/
|
|
635
645
|
enableWebhook: z2.boolean().default(false),
|
|
636
|
-
/** JWKS URL for verifying SaaS-issued tokens. */
|
|
646
|
+
/** JWKS URL for verifying SaaS-issued tokens (fetched without credentials, redirects refused). */
|
|
637
647
|
jwksUrl: httpsUrl,
|
|
638
|
-
/** Expected issuer (`iss`) claim
|
|
648
|
+
/** Expected issuer (`iss`) claim of `authToken`. */
|
|
639
649
|
expectedIssuer: z2.string().min(1)
|
|
640
650
|
});
|
|
641
651
|
var inlineSourceSchema = z2.object({
|
|
@@ -733,8 +743,8 @@ var NpmSource = class {
|
|
|
733
743
|
}
|
|
734
744
|
async loadBundleFromPackage() {
|
|
735
745
|
if (this.options.verifyProvenance) {
|
|
736
|
-
|
|
737
|
-
`
|
|
746
|
+
throw new Error(
|
|
747
|
+
`npm source "${this.options.packageName}": verifyProvenance is true, but npm provenance verification is not implemented, so the package was not loaded. Set verifyProvenance: false to load it without a provenance check, only for a package you trust: importing it runs its code before any bundle signature is checked, so pin its exact version and install it from a lockfile with integrity hashes.`
|
|
738
748
|
);
|
|
739
749
|
}
|
|
740
750
|
const mod = await this.dynamicImport(this.options.packageName);
|
|
@@ -769,9 +779,33 @@ var NpmSource = class {
|
|
|
769
779
|
};
|
|
770
780
|
|
|
771
781
|
// libs/adapters/src/skills/sources/saas-pull.source.ts
|
|
782
|
+
import { JwksService } from "@frontmcp/auth";
|
|
772
783
|
import { dirname, ensureDir, isRedirectResponse, pathResolve, readFile, writeFile } from "@frontmcp/utils";
|
|
773
784
|
var DEFAULT_CACHE_DIR = ".frontmcp/skilled-openapi";
|
|
774
785
|
var DEFAULT_PULL_TIMEOUT_MS = 3e4;
|
|
786
|
+
var USER_AGENT = "frontmcp-skilled-openapi";
|
|
787
|
+
var SaasPullTokenRejectedError = class extends Error {
|
|
788
|
+
constructor(reason) {
|
|
789
|
+
super(`[saas-source] pull token rejected: ${reason}`);
|
|
790
|
+
this.name = "SaasPullTokenRejectedError";
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
function claimValues(claim) {
|
|
794
|
+
if (Array.isArray(claim)) return claim;
|
|
795
|
+
return claim === void 0 ? [] : [claim];
|
|
796
|
+
}
|
|
797
|
+
var PUBLIC_KEY_MEMBERS = /* @__PURE__ */ new Map([
|
|
798
|
+
["RSA", ["n", "e"]],
|
|
799
|
+
["EC", ["crv", "x", "y"]],
|
|
800
|
+
["OKP", ["crv", "x"]]
|
|
801
|
+
]);
|
|
802
|
+
function isUsableSigningKey(key) {
|
|
803
|
+
if (typeof key !== "object" || key === null) return false;
|
|
804
|
+
const jwk = key;
|
|
805
|
+
const members = typeof jwk["kty"] === "string" ? PUBLIC_KEY_MEMBERS.get(jwk["kty"]) : void 0;
|
|
806
|
+
if (!members || jwk["use"] !== void 0 && jwk["use"] !== "sig") return false;
|
|
807
|
+
return members.every((m) => typeof jwk[m] === "string" && jwk[m] !== "");
|
|
808
|
+
}
|
|
775
809
|
var SaasPullSource = class {
|
|
776
810
|
constructor(options, cacheDir, logger, deps = {}) {
|
|
777
811
|
this.options = options;
|
|
@@ -789,6 +823,7 @@ var SaasPullSource = class {
|
|
|
789
823
|
pollHandle;
|
|
790
824
|
inFlight = false;
|
|
791
825
|
stopped = false;
|
|
826
|
+
jwksService = new JwksService({});
|
|
792
827
|
async start() {
|
|
793
828
|
if (this.stopped || this.inFlight) {
|
|
794
829
|
this.schedulePoll();
|
|
@@ -801,6 +836,7 @@ var SaasPullSource = class {
|
|
|
801
836
|
bundle = await this.fetchOnce();
|
|
802
837
|
await this.persistCache(bundle);
|
|
803
838
|
} catch (e) {
|
|
839
|
+
if (e instanceof SaasPullTokenRejectedError) throw e;
|
|
804
840
|
this.logger.warn(
|
|
805
841
|
`[saas-source] initial pull failed (${e.message}); attempting cached bundle fallback`
|
|
806
842
|
);
|
|
@@ -872,7 +908,11 @@ var SaasPullSource = class {
|
|
|
872
908
|
await this.persistCache(bundle);
|
|
873
909
|
this.notify(bundle);
|
|
874
910
|
} catch (e) {
|
|
875
|
-
|
|
911
|
+
if (e instanceof SaasPullTokenRejectedError) {
|
|
912
|
+
this.logger.error(`[saas-source] poll refused: ${e.message}; keeping the current bundle`);
|
|
913
|
+
} else {
|
|
914
|
+
this.logger.warn(`[saas-source] poll failed: ${e.message}`);
|
|
915
|
+
}
|
|
876
916
|
} finally {
|
|
877
917
|
this.inFlight = false;
|
|
878
918
|
this.schedulePoll();
|
|
@@ -893,11 +933,53 @@ var SaasPullSource = class {
|
|
|
893
933
|
clearTimeout(timer);
|
|
894
934
|
}
|
|
895
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Verify the pinned pull token against the issuer's JWKS, issuer and audience.
|
|
938
|
+
* Throws {@link SaasPullTokenRejectedError} when the token is refused, and a
|
|
939
|
+
* plain Error when the JWKS can't be fetched (an outage).
|
|
940
|
+
*/
|
|
941
|
+
async verifyPullToken() {
|
|
942
|
+
const { jwksUrl, expectedIssuer, expectedAudience, authToken } = this.options;
|
|
943
|
+
const { status, body } = await this.httpGet(jwksUrl, { Accept: "application/json", "User-Agent": USER_AGENT });
|
|
944
|
+
if (status < 200 || status >= 300) {
|
|
945
|
+
throw new Error(`JWKS ${jwksUrl} responded ${status}`);
|
|
946
|
+
}
|
|
947
|
+
let jwks;
|
|
948
|
+
try {
|
|
949
|
+
jwks = JSON.parse(body);
|
|
950
|
+
} catch {
|
|
951
|
+
throw new Error(`JWKS ${jwksUrl} is not valid JSON`);
|
|
952
|
+
}
|
|
953
|
+
if (!jwks || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
|
954
|
+
throw new Error(`JWKS ${jwksUrl} has no keys`);
|
|
955
|
+
}
|
|
956
|
+
const signingKeys = jwks.keys.filter(isUsableSigningKey);
|
|
957
|
+
if (signingKeys.length === 0) {
|
|
958
|
+
throw new Error(`JWKS ${jwksUrl} has no usable signing keys`);
|
|
959
|
+
}
|
|
960
|
+
const verified = await this.jwksService.verifyTransparentToken(authToken, [
|
|
961
|
+
{ id: "skilled-openapi-saas", issuerUrl: expectedIssuer, jwks: { keys: signingKeys } }
|
|
962
|
+
]);
|
|
963
|
+
if (!verified.ok || !verified.payload) {
|
|
964
|
+
throw new SaasPullTokenRejectedError(
|
|
965
|
+
`not a valid token from issuer "${expectedIssuer}" signed by a key in ${jwksUrl} (${verified.error ?? "verification failed"})`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const payload = verified.payload;
|
|
969
|
+
if (!claimValues(payload["aud"]).includes(expectedAudience)) {
|
|
970
|
+
throw new SaasPullTokenRejectedError(`its "aud" claim does not include "${expectedAudience}"`);
|
|
971
|
+
}
|
|
972
|
+
const resource = claimValues(payload["resource"]);
|
|
973
|
+
if (resource.length > 0 && !resource.includes(expectedAudience)) {
|
|
974
|
+
throw new SaasPullTokenRejectedError(`its "resource" claim does not include "${expectedAudience}"`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
896
977
|
async fetchOnce() {
|
|
978
|
+
await this.verifyPullToken();
|
|
897
979
|
const { status, body } = await this.httpGet(this.options.endpoint, {
|
|
898
980
|
Authorization: `Bearer ${this.options.authToken}`,
|
|
899
981
|
Accept: "application/json, application/yaml",
|
|
900
|
-
"User-Agent":
|
|
982
|
+
"User-Agent": USER_AGENT
|
|
901
983
|
});
|
|
902
984
|
if (status < 200 || status >= 300) {
|
|
903
985
|
throw new Error(`pull responded ${status}`);
|
|
@@ -1521,13 +1603,13 @@ function verifyBundleSignature(bundle, trustedKeys, telemetry) {
|
|
|
1521
1603
|
}
|
|
1522
1604
|
|
|
1523
1605
|
// libs/adapters/src/skills/security/jwt-verifier.ts
|
|
1524
|
-
import { JwksService } from "@frontmcp/auth";
|
|
1606
|
+
import { JwksService as JwksService2 } from "@frontmcp/auth";
|
|
1525
1607
|
var DEFAULT_REQUIRED_ROLES = ["frontmcp:cloud:push"];
|
|
1526
1608
|
var BundlePushJwtVerifier = class {
|
|
1527
1609
|
constructor(options, logger, jwks) {
|
|
1528
1610
|
this.options = options;
|
|
1529
1611
|
this.logger = logger;
|
|
1530
|
-
this.jwks = jwks ?? new
|
|
1612
|
+
this.jwks = jwks ?? new JwksService2({});
|
|
1531
1613
|
this.providerRef = {
|
|
1532
1614
|
id: "skilled-openapi-cloud-push",
|
|
1533
1615
|
issuerUrl: options.expectedIssuer,
|