@frontmcp/testing 1.3.0 → 1.4.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.
@@ -22,6 +22,17 @@ export interface TokenFactoryOptions {
22
22
  issuer?: string;
23
23
  /** Default audience */
24
24
  audience?: string;
25
+ /**
26
+ * When set, tokens are HS256-signed with this shared secret instead of an
27
+ * auto-generated RSA key (the default RS256 + JWKS behaviour). Use this for
28
+ * GATEWAY-mode servers (auth.mode public/local/orchestrated) that verify
29
+ * tokens against their own `JWT_SECRET` — sign with the same secret so the
30
+ * minted tokens are genuinely cryptographically valid (not just decodable).
31
+ *
32
+ * Leave unset for TRANSPARENT mode, where the server verifies against the
33
+ * provider's published JWKS (RS256).
34
+ */
35
+ hmacSecret?: string;
25
36
  }
26
37
  /**
27
38
  * Factory for creating JWT tokens for testing
@@ -48,9 +59,21 @@ export declare class TestTokenFactory {
48
59
  private publicKey;
49
60
  private jwk;
50
61
  private keyId;
62
+ /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
63
+ private readonly hmacSecret;
51
64
  constructor(options?: TokenFactoryOptions);
65
+ /** True when this factory signs HS256 with a shared secret (gateway mode). */
66
+ private get isHmac();
67
+ /** The JWS `alg` this factory signs with. */
68
+ private get alg();
69
+ /**
70
+ * The signing key: the shared HMAC secret in gateway mode, otherwise the
71
+ * generated RSA private key (lazily created via ensureKeys()).
72
+ */
73
+ private signingKey;
52
74
  /**
53
- * Initialize the key pair (called automatically on first use)
75
+ * Initialize the key pair (called automatically on first use). No-op in HS256
76
+ * mode — there is no asymmetric keypair, just the shared secret.
54
77
  */
55
78
  private ensureKeys;
56
79
  /**
@@ -78,7 +101,10 @@ export declare class TestTokenFactory {
78
101
  */
79
102
  createTokenWithInvalidSignature(options: Pick<CreateTokenOptions, 'sub'>): string;
80
103
  /**
81
- * Get the public JWKS for verifying tokens
104
+ * Get the public JWKS for verifying tokens.
105
+ *
106
+ * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
107
+ * the shared secret, not a published public key, so there is no JWKS to serve.
82
108
  */
83
109
  getPublicJwks(): Promise<{
84
110
  keys: JWK[];
@@ -1 +1 @@
1
- {"version":3,"file":"token-factory.d.ts","sourceRoot":"","sources":["../../src/auth/token-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAwD,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAMtF,MAAM,WAAW,kBAAkB;IACjC,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe;IACf,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,mBAAmB;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAS;gBAEV,OAAO,GAAE,mBAAwB;IAM7C;;OAEG;YACW,UAAU;IAkBxB;;OAEG;IACG,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BnE;;OAEG;IACG,gBAAgB,CAAC,GAAG,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1D;;OAEG;IACG,eAAe,CAAC,GAAG,SAAa,EAAE,MAAM,WAAoB,GAAG,OAAO,CAAC,MAAM,CAAC;IAYpF;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;IAW7C;;OAEG;IACG,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBnF;;OAEG;IACH,+BAA+B,CAAC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,GAAG,MAAM;IAqBjF;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC;IAU/C;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;OAEG;IACH,WAAW,IAAI,MAAM;CAGtB"}
1
+ {"version":3,"file":"token-factory.d.ts","sourceRoot":"","sources":["../../src/auth/token-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAuC,KAAK,GAAG,EAAmB,MAAM,MAAM,CAAC;AAMtF,MAAM,WAAW,kBAAkB;IACjC,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe;IACf,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,mBAAmB;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAS;IACtB,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;gBAEnC,OAAO,GAAE,mBAAwB;IAO7C,8EAA8E;IAC9E,OAAO,KAAK,MAAM,GAEjB;IAED,6CAA6C;IAC7C,OAAO,KAAK,GAAG,GAEd;IAED;;;OAGG;IACH,OAAO,CAAC,UAAU;IAQlB;;;OAGG;YACW,UAAU;IAmBxB;;OAEG;IACG,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAsBnE;;OAEG;IACG,gBAAgB,CAAC,GAAG,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1D;;OAEG;IACG,eAAe,CAAC,GAAG,SAAa,EAAE,MAAM,WAAoB,GAAG,OAAO,CAAC,MAAM,CAAC;IAYpF;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;IAW7C;;OAEG;IACG,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBnF;;OAEG;IACH,+BAA+B,CAAC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,GAAG,MAAM;IAuBjF;;;;;OAKG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC;IAa/C;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;OAEG;IACH,WAAW,IAAI,MAAM;CAGtB"}
@@ -7,14 +7,198 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
 
8
8
  // libs/testing/src/fixtures/test-fixture.ts
9
9
  import {
10
- describe as _describe,
10
+ afterAll as _afterAll,
11
+ afterEach as _afterEach,
11
12
  beforeAll as _beforeAll,
12
13
  beforeEach as _beforeEach,
13
- afterEach as _afterEach,
14
- afterAll as _afterAll,
14
+ describe as _describe,
15
15
  it as _it
16
16
  } from "@jest/globals";
17
17
 
18
+ // libs/testing/src/auth/token-factory.ts
19
+ import { exportJWK, generateKeyPair, SignJWT } from "jose";
20
+ var TestTokenFactory = class {
21
+ issuer;
22
+ audience;
23
+ privateKey = null;
24
+ publicKey = null;
25
+ jwk = null;
26
+ keyId;
27
+ /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
28
+ hmacSecret;
29
+ constructor(options = {}) {
30
+ this.issuer = options.issuer ?? "https://test.frontmcp.local";
31
+ this.audience = options.audience ?? "frontmcp-test";
32
+ this.keyId = `test-key-${Date.now()}`;
33
+ this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
34
+ }
35
+ /** True when this factory signs HS256 with a shared secret (gateway mode). */
36
+ get isHmac() {
37
+ return this.hmacSecret !== null;
38
+ }
39
+ /** The JWS `alg` this factory signs with. */
40
+ get alg() {
41
+ return this.isHmac ? "HS256" : "RS256";
42
+ }
43
+ /**
44
+ * The signing key: the shared HMAC secret in gateway mode, otherwise the
45
+ * generated RSA private key (lazily created via ensureKeys()).
46
+ */
47
+ signingKey() {
48
+ if (this.hmacSecret) return this.hmacSecret;
49
+ if (!this.privateKey) {
50
+ throw new Error("Private key not initialized");
51
+ }
52
+ return this.privateKey;
53
+ }
54
+ /**
55
+ * Initialize the key pair (called automatically on first use). No-op in HS256
56
+ * mode — there is no asymmetric keypair, just the shared secret.
57
+ */
58
+ async ensureKeys() {
59
+ if (this.isHmac) return;
60
+ if (this.privateKey && this.publicKey) return;
61
+ const { publicKey, privateKey } = await generateKeyPair("RS256", {
62
+ extractable: true
63
+ });
64
+ this.privateKey = privateKey;
65
+ this.publicKey = publicKey;
66
+ this.jwk = await exportJWK(publicKey);
67
+ this.jwk.kid = this.keyId;
68
+ this.jwk.use = "sig";
69
+ this.jwk.alg = "RS256";
70
+ }
71
+ /**
72
+ * Create a JWT token with the specified claims
73
+ */
74
+ async createTestToken(options) {
75
+ await this.ensureKeys();
76
+ const now = Math.floor(Date.now() / 1e3);
77
+ const exp = options.exp ?? 3600;
78
+ const payload = {
79
+ iss: options.iss ?? this.issuer,
80
+ sub: options.sub,
81
+ aud: options.aud ?? this.audience,
82
+ iat: now,
83
+ exp: now + exp,
84
+ scope: options.scopes?.join(" "),
85
+ ...options.claims
86
+ };
87
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
88
+ const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
89
+ return token;
90
+ }
91
+ /**
92
+ * Create an admin token with full access
93
+ */
94
+ async createAdminToken(sub = "admin-001") {
95
+ return this.createTestToken({
96
+ sub,
97
+ scopes: ["admin:*", "read", "write", "delete"],
98
+ claims: {
99
+ email: "admin@test.local",
100
+ name: "Test Admin",
101
+ role: "admin"
102
+ }
103
+ });
104
+ }
105
+ /**
106
+ * Create a regular user token
107
+ */
108
+ async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
109
+ return this.createTestToken({
110
+ sub,
111
+ scopes,
112
+ claims: {
113
+ email: "user@test.local",
114
+ name: "Test User",
115
+ role: "user"
116
+ }
117
+ });
118
+ }
119
+ /**
120
+ * Create an anonymous user token
121
+ */
122
+ async createAnonymousToken() {
123
+ return this.createTestToken({
124
+ sub: `anon:${Date.now()}`,
125
+ scopes: ["anonymous"],
126
+ claims: {
127
+ name: "Anonymous",
128
+ role: "anonymous"
129
+ }
130
+ });
131
+ }
132
+ /**
133
+ * Create an expired token (for testing token expiration)
134
+ */
135
+ async createExpiredToken(options) {
136
+ await this.ensureKeys();
137
+ const now = Math.floor(Date.now() / 1e3);
138
+ const payload = {
139
+ iss: this.issuer,
140
+ sub: options.sub,
141
+ aud: this.audience,
142
+ iat: now - 7200,
143
+ // 2 hours ago
144
+ exp: now - 3600
145
+ // Expired 1 hour ago
146
+ };
147
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
148
+ const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
149
+ return token;
150
+ }
151
+ /**
152
+ * Create a token with an invalid signature (for testing signature validation)
153
+ */
154
+ createTokenWithInvalidSignature(options) {
155
+ const now = Math.floor(Date.now() / 1e3);
156
+ const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
157
+ const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
158
+ const payload = Buffer.from(
159
+ JSON.stringify({
160
+ iss: this.issuer,
161
+ sub: options.sub,
162
+ aud: this.audience,
163
+ iat: now,
164
+ exp: now + 3600
165
+ })
166
+ ).toString("base64url");
167
+ const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
168
+ return `${header}.${payload}.${signature}`;
169
+ }
170
+ /**
171
+ * Get the public JWKS for verifying tokens.
172
+ *
173
+ * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
174
+ * the shared secret, not a published public key, so there is no JWKS to serve.
175
+ */
176
+ async getPublicJwks() {
177
+ if (this.isHmac) {
178
+ throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
179
+ }
180
+ await this.ensureKeys();
181
+ if (!this.jwk) {
182
+ throw new Error("JWK not initialized");
183
+ }
184
+ return {
185
+ keys: [this.jwk]
186
+ };
187
+ }
188
+ /**
189
+ * Get the issuer URL
190
+ */
191
+ getIssuer() {
192
+ return this.issuer;
193
+ }
194
+ /**
195
+ * Get the audience
196
+ */
197
+ getAudience() {
198
+ return this.audience;
199
+ }
200
+ };
201
+
18
202
  // libs/testing/src/platform/platform-client-info.ts
19
203
  var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
20
204
  function getPlatformClientInfo(platform) {
@@ -2085,163 +2269,6 @@ var ProgressCollector = class {
2085
2269
  }
2086
2270
  };
2087
2271
 
2088
- // libs/testing/src/auth/token-factory.ts
2089
- import { SignJWT, generateKeyPair, exportJWK } from "jose";
2090
- var TestTokenFactory = class {
2091
- issuer;
2092
- audience;
2093
- privateKey = null;
2094
- publicKey = null;
2095
- jwk = null;
2096
- keyId;
2097
- constructor(options = {}) {
2098
- this.issuer = options.issuer ?? "https://test.frontmcp.local";
2099
- this.audience = options.audience ?? "frontmcp-test";
2100
- this.keyId = `test-key-${Date.now()}`;
2101
- }
2102
- /**
2103
- * Initialize the key pair (called automatically on first use)
2104
- */
2105
- async ensureKeys() {
2106
- if (this.privateKey && this.publicKey) return;
2107
- const { publicKey, privateKey } = await generateKeyPair("RS256", {
2108
- extractable: true
2109
- });
2110
- this.privateKey = privateKey;
2111
- this.publicKey = publicKey;
2112
- this.jwk = await exportJWK(publicKey);
2113
- this.jwk.kid = this.keyId;
2114
- this.jwk.use = "sig";
2115
- this.jwk.alg = "RS256";
2116
- }
2117
- /**
2118
- * Create a JWT token with the specified claims
2119
- */
2120
- async createTestToken(options) {
2121
- await this.ensureKeys();
2122
- const now = Math.floor(Date.now() / 1e3);
2123
- const exp = options.exp ?? 3600;
2124
- const payload = {
2125
- iss: options.iss ?? this.issuer,
2126
- sub: options.sub,
2127
- aud: options.aud ?? this.audience,
2128
- iat: now,
2129
- exp: now + exp,
2130
- scope: options.scopes?.join(" "),
2131
- ...options.claims
2132
- };
2133
- if (!this.privateKey) {
2134
- throw new Error("Private key not initialized");
2135
- }
2136
- const token = await new SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
2137
- return token;
2138
- }
2139
- /**
2140
- * Create an admin token with full access
2141
- */
2142
- async createAdminToken(sub = "admin-001") {
2143
- return this.createTestToken({
2144
- sub,
2145
- scopes: ["admin:*", "read", "write", "delete"],
2146
- claims: {
2147
- email: "admin@test.local",
2148
- name: "Test Admin",
2149
- role: "admin"
2150
- }
2151
- });
2152
- }
2153
- /**
2154
- * Create a regular user token
2155
- */
2156
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
2157
- return this.createTestToken({
2158
- sub,
2159
- scopes,
2160
- claims: {
2161
- email: "user@test.local",
2162
- name: "Test User",
2163
- role: "user"
2164
- }
2165
- });
2166
- }
2167
- /**
2168
- * Create an anonymous user token
2169
- */
2170
- async createAnonymousToken() {
2171
- return this.createTestToken({
2172
- sub: `anon:${Date.now()}`,
2173
- scopes: ["anonymous"],
2174
- claims: {
2175
- name: "Anonymous",
2176
- role: "anonymous"
2177
- }
2178
- });
2179
- }
2180
- /**
2181
- * Create an expired token (for testing token expiration)
2182
- */
2183
- async createExpiredToken(options) {
2184
- await this.ensureKeys();
2185
- const now = Math.floor(Date.now() / 1e3);
2186
- const payload = {
2187
- iss: this.issuer,
2188
- sub: options.sub,
2189
- aud: this.audience,
2190
- iat: now - 7200,
2191
- // 2 hours ago
2192
- exp: now - 3600
2193
- // Expired 1 hour ago
2194
- };
2195
- if (!this.privateKey) {
2196
- throw new Error("Private key not initialized");
2197
- }
2198
- const token = await new SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
2199
- return token;
2200
- }
2201
- /**
2202
- * Create a token with an invalid signature (for testing signature validation)
2203
- */
2204
- createTokenWithInvalidSignature(options) {
2205
- const now = Math.floor(Date.now() / 1e3);
2206
- const header = Buffer.from(JSON.stringify({ alg: "RS256", kid: this.keyId })).toString("base64url");
2207
- const payload = Buffer.from(
2208
- JSON.stringify({
2209
- iss: this.issuer,
2210
- sub: options.sub,
2211
- aud: this.audience,
2212
- iat: now,
2213
- exp: now + 3600
2214
- })
2215
- ).toString("base64url");
2216
- const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
2217
- return `${header}.${payload}.${signature}`;
2218
- }
2219
- /**
2220
- * Get the public JWKS for verifying tokens
2221
- */
2222
- async getPublicJwks() {
2223
- await this.ensureKeys();
2224
- if (!this.jwk) {
2225
- throw new Error("JWK not initialized");
2226
- }
2227
- return {
2228
- keys: [this.jwk]
2229
- };
2230
- }
2231
- /**
2232
- * Get the issuer URL
2233
- */
2234
- getIssuer() {
2235
- return this.issuer;
2236
- }
2237
- /**
2238
- * Get the audience
2239
- */
2240
- getAudience() {
2241
- return this.audience;
2242
- }
2243
- };
2244
-
2245
2272
  // libs/testing/src/server/test-server.ts
2246
2273
  import { spawn } from "child_process";
2247
2274
  import { sha256Hex } from "@frontmcp/utils";
@@ -2281,6 +2308,7 @@ var E2E_PORT_RANGES = {
2281
2308
  "demo-e2e-orchestrated": { start: 50100, size: 10 },
2282
2309
  "demo-e2e-transparent": { start: 50110, size: 10 },
2283
2310
  "demo-e2e-cimd": { start: 50120, size: 10 },
2311
+ "demo-e2e-remote-proxy": { start: 50130, size: 10 },
2284
2312
  // Feature E2E tests (50200-50299)
2285
2313
  "demo-e2e-skills": { start: 50200, size: 10 },
2286
2314
  "demo-e2e-remote": { start: 50210, size: 10 },
@@ -2955,7 +2983,8 @@ var tokenFactory = null;
2955
2983
  var serverStartedByUs = false;
2956
2984
  async function initializeSharedResources() {
2957
2985
  if (!tokenFactory) {
2958
- tokenFactory = new TestTokenFactory();
2986
+ const sharedSecret = currentConfig.env?.["JWT_SECRET"];
2987
+ tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret } : {});
2959
2988
  }
2960
2989
  if (!serverInstance) {
2961
2990
  if (currentConfig.baseUrl) {
package/esm/index.mjs CHANGED
@@ -2159,7 +2159,7 @@ var ProgressCollector = class {
2159
2159
  };
2160
2160
 
2161
2161
  // libs/testing/src/auth/token-factory.ts
2162
- import { SignJWT, generateKeyPair, exportJWK } from "jose";
2162
+ import { exportJWK, generateKeyPair, SignJWT } from "jose";
2163
2163
  var TestTokenFactory = class {
2164
2164
  issuer;
2165
2165
  audience;
@@ -2167,15 +2167,39 @@ var TestTokenFactory = class {
2167
2167
  publicKey = null;
2168
2168
  jwk = null;
2169
2169
  keyId;
2170
+ /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
2171
+ hmacSecret;
2170
2172
  constructor(options = {}) {
2171
2173
  this.issuer = options.issuer ?? "https://test.frontmcp.local";
2172
2174
  this.audience = options.audience ?? "frontmcp-test";
2173
2175
  this.keyId = `test-key-${Date.now()}`;
2176
+ this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
2177
+ }
2178
+ /** True when this factory signs HS256 with a shared secret (gateway mode). */
2179
+ get isHmac() {
2180
+ return this.hmacSecret !== null;
2181
+ }
2182
+ /** The JWS `alg` this factory signs with. */
2183
+ get alg() {
2184
+ return this.isHmac ? "HS256" : "RS256";
2185
+ }
2186
+ /**
2187
+ * The signing key: the shared HMAC secret in gateway mode, otherwise the
2188
+ * generated RSA private key (lazily created via ensureKeys()).
2189
+ */
2190
+ signingKey() {
2191
+ if (this.hmacSecret) return this.hmacSecret;
2192
+ if (!this.privateKey) {
2193
+ throw new Error("Private key not initialized");
2194
+ }
2195
+ return this.privateKey;
2174
2196
  }
2175
2197
  /**
2176
- * Initialize the key pair (called automatically on first use)
2198
+ * Initialize the key pair (called automatically on first use). No-op in HS256
2199
+ * mode — there is no asymmetric keypair, just the shared secret.
2177
2200
  */
2178
2201
  async ensureKeys() {
2202
+ if (this.isHmac) return;
2179
2203
  if (this.privateKey && this.publicKey) return;
2180
2204
  const { publicKey, privateKey } = await generateKeyPair("RS256", {
2181
2205
  extractable: true
@@ -2203,10 +2227,8 @@ var TestTokenFactory = class {
2203
2227
  scope: options.scopes?.join(" "),
2204
2228
  ...options.claims
2205
2229
  };
2206
- if (!this.privateKey) {
2207
- throw new Error("Private key not initialized");
2208
- }
2209
- const token = await new SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
2230
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
2231
+ const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
2210
2232
  return token;
2211
2233
  }
2212
2234
  /**
@@ -2265,10 +2287,8 @@ var TestTokenFactory = class {
2265
2287
  exp: now - 3600
2266
2288
  // Expired 1 hour ago
2267
2289
  };
2268
- if (!this.privateKey) {
2269
- throw new Error("Private key not initialized");
2270
- }
2271
- const token = await new SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
2290
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
2291
+ const token = await new SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
2272
2292
  return token;
2273
2293
  }
2274
2294
  /**
@@ -2276,7 +2296,8 @@ var TestTokenFactory = class {
2276
2296
  */
2277
2297
  createTokenWithInvalidSignature(options) {
2278
2298
  const now = Math.floor(Date.now() / 1e3);
2279
- const header = Buffer.from(JSON.stringify({ alg: "RS256", kid: this.keyId })).toString("base64url");
2299
+ const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
2300
+ const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
2280
2301
  const payload = Buffer.from(
2281
2302
  JSON.stringify({
2282
2303
  iss: this.issuer,
@@ -2290,9 +2311,15 @@ var TestTokenFactory = class {
2290
2311
  return `${header}.${payload}.${signature}`;
2291
2312
  }
2292
2313
  /**
2293
- * Get the public JWKS for verifying tokens
2314
+ * Get the public JWKS for verifying tokens.
2315
+ *
2316
+ * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
2317
+ * the shared secret, not a published public key, so there is no JWKS to serve.
2294
2318
  */
2295
2319
  async getPublicJwks() {
2320
+ if (this.isHmac) {
2321
+ throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
2322
+ }
2296
2323
  await this.ensureKeys();
2297
2324
  if (!this.jwk) {
2298
2325
  throw new Error("JWK not initialized");
@@ -3888,6 +3915,7 @@ var E2E_PORT_RANGES = {
3888
3915
  "demo-e2e-orchestrated": { start: 50100, size: 10 },
3889
3916
  "demo-e2e-transparent": { start: 50110, size: 10 },
3890
3917
  "demo-e2e-cimd": { start: 50120, size: 10 },
3918
+ "demo-e2e-remote-proxy": { start: 50130, size: 10 },
3891
3919
  // Feature E2E tests (50200-50299)
3892
3920
  "demo-e2e-skills": { start: 50200, size: 10 },
3893
3921
  "demo-e2e-remote": { start: 50210, size: 10 },
@@ -4716,11 +4744,11 @@ function hasMimeType(result, mimeType) {
4716
4744
 
4717
4745
  // libs/testing/src/fixtures/test-fixture.ts
4718
4746
  import {
4719
- describe as _describe,
4747
+ afterAll as _afterAll,
4748
+ afterEach as _afterEach,
4720
4749
  beforeAll as _beforeAll,
4721
4750
  beforeEach as _beforeEach,
4722
- afterEach as _afterEach,
4723
- afterAll as _afterAll,
4751
+ describe as _describe,
4724
4752
  it as _it
4725
4753
  } from "@jest/globals";
4726
4754
  var describe2 = _describe;
@@ -4735,7 +4763,8 @@ var tokenFactory = null;
4735
4763
  var serverStartedByUs = false;
4736
4764
  async function initializeSharedResources() {
4737
4765
  if (!tokenFactory) {
4738
- tokenFactory = new TestTokenFactory();
4766
+ const sharedSecret = currentConfig.env?.["JWT_SECRET"];
4767
+ tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret } : {});
4739
4768
  }
4740
4769
  if (!serverInstance) {
4741
4770
  if (currentConfig.baseUrl) {
@@ -7981,6 +8010,26 @@ function createReportGenerator() {
7981
8010
  // libs/testing/src/raw-client/index.ts
7982
8011
  import { Client } from "@frontmcp/protocol";
7983
8012
  import { StdioClientTransport } from "@frontmcp/protocol";
8013
+
8014
+ // libs/testing/src/raw-client/port-probe.ts
8015
+ import net from "node:net";
8016
+ function isTcpPortListening(port, host = "127.0.0.1", timeoutMs = 1500) {
8017
+ return new Promise((resolve) => {
8018
+ const socket = net.connect({ port, host });
8019
+ socket.unref();
8020
+ socket.setTimeout(timeoutMs);
8021
+ let settled = false;
8022
+ const finish = (listening) => {
8023
+ if (settled) return;
8024
+ settled = true;
8025
+ socket.destroy();
8026
+ resolve(listening);
8027
+ };
8028
+ socket.once("connect", () => finish(true));
8029
+ socket.once("error", () => finish(false));
8030
+ socket.once("timeout", () => finish(false));
8031
+ });
8032
+ }
7984
8033
  export {
7985
8034
  AssertionError,
7986
8035
  AuthHeaders,
@@ -8063,6 +8112,7 @@ export {
8063
8112
  isGcAvailable,
8064
8113
  isOpenAIPlatform,
8065
8114
  isSuccessful,
8115
+ isTcpPortListening,
8066
8116
  isUiPlatform,
8067
8117
  mcpMatchers,
8068
8118
  mockResponse,
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/testing",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "E2E testing framework for FrontMCP servers - MCP client, auth mocks, Playwright integration",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -87,24 +87,24 @@
87
87
  "./esm": null
88
88
  },
89
89
  "peerDependencies": {
90
- "@frontmcp/sdk": "1.3.0",
91
- "@frontmcp/ui": "1.3.0",
92
- "@playwright/test": "^1.40.0",
93
- "jest": "^29.0.0 || ^30.0.0",
90
+ "@frontmcp/lazy-zod": "1.4.1",
91
+ "@frontmcp/sdk": "1.4.1",
92
+ "@frontmcp/ui": "1.4.1",
94
93
  "@jest/globals": "^29.0.0 || ^30.0.0",
95
- "@frontmcp/lazy-zod": "1.3.0"
94
+ "@playwright/test": "^1.40.0",
95
+ "jest": "^29.0.0 || ^30.0.0"
96
96
  },
97
97
  "peerDependenciesMeta": {
98
98
  "@frontmcp/ui": {
99
99
  "optional": true
100
100
  },
101
- "@playwright/test": {
101
+ "@jest/globals": {
102
102
  "optional": true
103
103
  },
104
- "jest": {
104
+ "@playwright/test": {
105
105
  "optional": true
106
106
  },
107
- "@jest/globals": {
107
+ "jest": {
108
108
  "optional": true
109
109
  }
110
110
  },
@@ -112,8 +112,8 @@
112
112
  "node": ">=24.0.0"
113
113
  },
114
114
  "dependencies": {
115
- "@frontmcp/utils": "1.3.0",
116
- "@frontmcp/protocol": "1.3.0",
115
+ "@frontmcp/protocol": "1.4.1",
116
+ "@frontmcp/utils": "1.4.1",
117
117
  "jose": "^6.0.11",
118
118
  "tslib": "^2.3.0"
119
119
  },