@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.
@@ -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. Set false (with warning) only for local development packages.
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
- /** Pinned JWT issued by the SaaS for the customer's FrontMCP server. */
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. The plugin verifies
626
- * `resource` AND `aud` claims match this on every push/pull.
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
- this.logger.warn(
737
- `[npm-source] verifyProvenance=true requested but provenance verification is not yet implemented in OSS (planned v1.2.x). Loading "${this.options.packageName}" without provenance check.`
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
- this.logger.warn(`[saas-source] poll failed: ${e.message}`);
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": "frontmcp-skilled-openapi"
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 JwksService({});
1612
+ this.jwks = jwks ?? new JwksService2({});
1531
1613
  this.providerRef = {
1532
1614
  id: "skilled-openapi-cloud-push",
1533
1615
  issuerUrl: options.expectedIssuer,