@frontmcp/testing 1.3.0 → 1.4.0
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/auth/token-factory.d.ts +28 -2
- package/auth/token-factory.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +190 -161
- package/esm/index.mjs +66 -16
- package/esm/package.json +6 -6
- package/esm/perf/index.mjs +40 -12
- package/fixtures/index.js +187 -158
- package/fixtures/test-fixture.d.ts.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +63 -12
- package/package.json +6 -6
- package/perf/index.js +39 -11
- package/raw-client/index.d.ts +1 -0
- package/raw-client/index.d.ts.map +1 -1
- package/raw-client/port-probe.d.ts +15 -0
- package/raw-client/port-probe.d.ts.map +1 -0
- package/server/port-registry.d.ts +4 -0
- package/server/port-registry.d.ts.map +1 -1
package/auth/token-factory.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/esm/fixtures/index.mjs
CHANGED
|
@@ -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
|
-
|
|
10
|
+
afterAll as _afterAll,
|
|
11
|
+
afterEach as _afterEach,
|
|
11
12
|
beforeAll as _beforeAll,
|
|
12
13
|
beforeEach as _beforeEach,
|
|
13
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
2207
|
-
|
|
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
|
-
|
|
2269
|
-
|
|
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
|
|
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
|
-
|
|
4747
|
+
afterAll as _afterAll,
|
|
4748
|
+
afterEach as _afterEach,
|
|
4720
4749
|
beforeAll as _beforeAll,
|
|
4721
4750
|
beforeEach as _beforeEach,
|
|
4722
|
-
|
|
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
|
-
|
|
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
|
+
"version": "1.4.0",
|
|
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,12 +87,12 @@
|
|
|
87
87
|
"./esm": null
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@frontmcp/sdk": "1.
|
|
91
|
-
"@frontmcp/ui": "1.
|
|
90
|
+
"@frontmcp/sdk": "1.4.0",
|
|
91
|
+
"@frontmcp/ui": "1.4.0",
|
|
92
92
|
"@playwright/test": "^1.40.0",
|
|
93
93
|
"jest": "^29.0.0 || ^30.0.0",
|
|
94
94
|
"@jest/globals": "^29.0.0 || ^30.0.0",
|
|
95
|
-
"@frontmcp/lazy-zod": "1.
|
|
95
|
+
"@frontmcp/lazy-zod": "1.4.0"
|
|
96
96
|
},
|
|
97
97
|
"peerDependenciesMeta": {
|
|
98
98
|
"@frontmcp/ui": {
|
|
@@ -112,8 +112,8 @@
|
|
|
112
112
|
"node": ">=24.0.0"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@frontmcp/utils": "1.
|
|
116
|
-
"@frontmcp/protocol": "1.
|
|
115
|
+
"@frontmcp/utils": "1.4.0",
|
|
116
|
+
"@frontmcp/protocol": "1.4.0",
|
|
117
117
|
"jose": "^6.0.11",
|
|
118
118
|
"tslib": "^2.3.0"
|
|
119
119
|
},
|