@frontmcp/adapters 1.8.2 → 1.8.4
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 +287 -72
- package/esm/openapi/index.mjs +287 -72
- package/esm/package.json +6 -6
- package/esm/skills/index.mjs +94 -12
- package/index.js +288 -76
- package/openapi/index.js +288 -76
- package/openapi/openapi.adapter.d.ts.map +1 -1
- package/openapi/openapi.security.d.ts +44 -5
- package/openapi/openapi.security.d.ts.map +1 -1
- package/openapi/openapi.tool.d.ts.map +1 -1
- package/openapi/openapi.types.d.ts +19 -0
- package/openapi/openapi.types.d.ts.map +1 -1
- package/package.json +6 -6
- package/skills/bundle/bundle.schema.d.ts +2 -2
- 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/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,
|