@frontmcp/auth 1.11.0 → 1.11.1-rc.2

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.
@@ -64,8 +64,12 @@ var CredentialProviderAlreadyRegisteredError = class extends AuthInternalError {
64
64
  }
65
65
  };
66
66
  var AuthProvidersNotConfiguredError = class extends AuthInternalError {
67
- constructor() {
68
- super("Auth providers are not configured", "AUTH_PROVIDERS_NOT_CONFIGURED");
67
+ constructor(providerName) {
68
+ const register = "Register it in @FrontMcp({ authProvidersVault: { providers: [...] } }).";
69
+ super(
70
+ providerName === void 0 ? "Auth providers are not configured: this server registers no auth providers." : `Auth provider "${providerName}" is not configured: this server registers no auth providers. ${register}`,
71
+ "AUTH_PROVIDERS_NOT_CONFIGURED"
72
+ );
69
73
  }
70
74
  };
71
75
  var OrchestratedAuthNotAvailableError = class extends AuthInternalError {
@@ -42,6 +42,40 @@ function isMissingExpError(error) {
42
42
  const { code, claim, reason } = error;
43
43
  return code === "ERR_JWT_CLAIM_VALIDATION_FAILED" && claim === "exp" && reason === "missing";
44
44
  }
45
+ var FAILURE_DEPTH = [
46
+ "jwks_unavailable",
47
+ "malformed",
48
+ "unsupported_algorithm",
49
+ "unknown_key",
50
+ "bad_signature",
51
+ "missing_claim",
52
+ "wrong_issuer",
53
+ "not_yet_valid",
54
+ "expired"
55
+ ];
56
+ function tokenVerifyFailureOf(error) {
57
+ const { code, claim, reason } = typeof error === "object" && error !== null ? error : {};
58
+ switch (code) {
59
+ case "ERR_JWT_EXPIRED":
60
+ return "expired";
61
+ case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
62
+ return "bad_signature";
63
+ case "ERR_JWKS_NO_MATCHING_KEY":
64
+ return "unknown_key";
65
+ case "ERR_JOSE_ALG_NOT_ALLOWED":
66
+ case "ERR_JOSE_NOT_SUPPORTED":
67
+ return "unsupported_algorithm";
68
+ case "ERR_JWT_CLAIM_VALIDATION_FAILED":
69
+ if (claim === "iss") return "wrong_issuer";
70
+ if (reason === "missing") return "missing_claim";
71
+ return claim === "nbf" && reason === "check_failed" ? "not_yet_valid" : "malformed";
72
+ default:
73
+ return "malformed";
74
+ }
75
+ }
76
+ function deeperFailure(current, next) {
77
+ return current && FAILURE_DEPTH.indexOf(current) >= FAILURE_DEPTH.indexOf(next) ? current : next;
78
+ }
45
79
  var WEAK_KEY_WARNING = `
46
80
  \u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
47
81
  This is considered insecure and should be updated.
@@ -115,6 +149,7 @@ var JwksService = class {
115
149
  } catch {
116
150
  }
117
151
  let missingExp = false;
152
+ let failure;
118
153
  for (const p of candidates) {
119
154
  let jwks;
120
155
  try {
@@ -122,7 +157,10 @@ var JwksService = class {
122
157
  if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
123
158
  jwks = await this.refetchForUnknownKid(p) ?? jwks;
124
159
  }
125
- if (!jwks?.keys?.length) continue;
160
+ if (!jwks?.keys?.length) {
161
+ failure = deeperFailure(failure, "jwks_unavailable");
162
+ continue;
163
+ }
126
164
  const JWKS = createLocalJWKSet(jwks);
127
165
  const issuerConstraint = this.issuerConstraintFor(p);
128
166
  const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
@@ -150,14 +188,16 @@ var JwksService = class {
150
188
  return fallbackResult;
151
189
  }
152
190
  if (fallbackResult.error === "missing_exp") missingExp = true;
191
+ if (fallbackResult.failure) failure = deeperFailure(failure, fallbackResult.failure);
153
192
  }
154
193
  }
155
194
  if (isMissingExpError(e)) missingExp = true;
195
+ failure = deeperFailure(failure, tokenVerifyFailureOf(e));
156
196
  this.logger.debug("[JwksService] Failed to verify token for provider: %s", p.id, e);
157
197
  }
158
198
  }
159
- if (missingExp) return { ok: false, error: MISSING_EXP_ERROR };
160
- return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}` };
199
+ if (missingExp) return { ok: false, error: MISSING_EXP_ERROR, failure };
200
+ return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}`, failure };
161
201
  }
162
202
  /**
163
203
  * Issuer constraint passed to `jose` for a provider: the configured issuer
@@ -199,20 +239,20 @@ var JwksService = class {
199
239
  try {
200
240
  const parts = token.split(".");
201
241
  if (parts.length !== 3) {
202
- return { ok: false, error: "invalid_token_format" };
242
+ return { ok: false, error: "invalid_token_format", failure: "malformed" };
203
243
  }
204
244
  const [headerB64, payloadB64, signatureB64] = parts;
205
245
  const header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
206
246
  const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
207
247
  const matchingKey = this.findMatchingKey(jwks, header);
208
248
  if (!matchingKey) {
209
- return { ok: false, error: "no_matching_key" };
249
+ return { ok: false, error: "no_matching_key", failure: "unknown_key" };
210
250
  }
211
251
  const signatureInput = `${headerB64}.${payloadB64}`;
212
252
  const signature = Buffer.from(signatureB64, "base64url");
213
253
  const jwtAlg = typeof header.alg === "string" ? header.alg : void 0;
214
254
  if (!jwtAlg) {
215
- return { ok: false, error: "missing_alg" };
255
+ return { ok: false, error: "missing_alg", failure: "malformed" };
216
256
  }
217
257
  const isValid = await rsaVerify(
218
258
  jwtAlg,
@@ -221,30 +261,30 @@ var JwksService = class {
221
261
  signature
222
262
  );
223
263
  if (!isValid) {
224
- return { ok: false, error: "signature_invalid" };
264
+ return { ok: false, error: "signature_invalid", failure: "bad_signature" };
225
265
  }
226
266
  if (provider.verifyIssuer !== false) {
227
267
  const payloadIssuerRaw = typeof payload.iss === "string" ? payload.iss : void 0;
228
268
  if (!payloadIssuerRaw) {
229
- return { ok: false, error: "issuer_mismatch" };
269
+ return { ok: false, error: "issuer_mismatch", failure: "wrong_issuer" };
230
270
  }
231
271
  const trustedIssuers = new Set(
232
272
  [provider.issuerUrl, ...provider.additionalIssuers ?? []].map((issuer) => normalizeIssuer(issuer))
233
273
  );
234
274
  const payloadIssuer = normalizeIssuer(payloadIssuerRaw);
235
275
  if (!trustedIssuers.has(payloadIssuer)) {
236
- return { ok: false, error: "issuer_mismatch" };
276
+ return { ok: false, error: "issuer_mismatch", failure: "wrong_issuer" };
237
277
  }
238
278
  }
239
279
  const nowSec = Math.floor(Date.now() / 1e3);
240
280
  if (payload.exp === void 0) {
241
- return { ok: false, error: "missing_exp" };
281
+ return { ok: false, error: "missing_exp", failure: "missing_claim" };
242
282
  }
243
283
  if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= nowSec) {
244
- return { ok: false, error: "token_expired" };
284
+ return { ok: false, error: "token_expired", failure: "expired" };
245
285
  }
246
286
  if (payload.nbf !== void 0 && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > nowSec)) {
247
- return { ok: false, error: "token_not_yet_valid" };
287
+ return { ok: false, error: "token_not_yet_valid", failure: "not_yet_valid" };
248
288
  }
249
289
  if (!this.warnedProviders.has(provider.id)) {
250
290
  this.warnedProviders.add(provider.id);
@@ -261,7 +301,7 @@ var JwksService = class {
261
301
  };
262
302
  } catch (err) {
263
303
  const message = err instanceof Error ? err.message : "unknown";
264
- return { ok: false, error: `weak_key_verification_failed: ${message}` };
304
+ return { ok: false, error: `weak_key_verification_failed: ${message}`, failure: "malformed" };
265
305
  }
266
306
  }
267
307
  /**
@@ -29,16 +29,20 @@ var CredentialsAccessorImpl = class {
29
29
  async requireConnect(options) {
30
30
  const { key, context } = options;
31
31
  const sub = this.resolveSub();
32
- if (sub) {
33
- const existing = await this.vault.get(sub, key);
34
- if (existing) {
35
- return { connected: true, key, credential: existing };
36
- }
32
+ if (!sub) {
33
+ return {
34
+ connected: false,
35
+ key,
36
+ resumeUrl: "",
37
+ signInRequired: true,
38
+ message: `Sign in to this server to connect credential "${key}".`
39
+ };
37
40
  }
38
- const token = signCredentialResumeToken(
39
- { sub: sub ?? "", key, context, ttlMs: this.resumeTtlMs },
40
- this.signingSecret
41
- );
41
+ const existing = await this.vault.get(sub, key);
42
+ if (existing) {
43
+ return { connected: true, key, credential: existing };
44
+ }
45
+ const token = signCredentialResumeToken({ sub, key, context, ttlMs: this.resumeTtlMs }, this.signingSecret);
42
46
  const resumeUrl = buildCredentialResumeUrl(this.basePath, token);
43
47
  this.logger.debug(`CredentialsAccessor: credential "${key}" not connected; issued resume link`);
44
48
  return {
@@ -83,6 +83,14 @@ var SessionCredentialVault = class {
83
83
  await this.storage.set(this.indexKey(sub), vaultId, this.credentialSetOpts);
84
84
  return vaultId;
85
85
  }
86
+ /** The subject's vaultId, or a new vault when it has none; unlike {@link rotateVault}, it never replaces one. */
87
+ async ensureVault(sub) {
88
+ const currentVaultId = await this.getVaultId(sub);
89
+ if (currentVaultId) return currentVaultId;
90
+ const createdVaultId = randomUUID();
91
+ await this.storage.set(this.indexKey(sub), createdVaultId, { ...this.credentialSetOpts, ifNotExists: true });
92
+ return await this.getVaultId(sub) ?? createdVaultId;
93
+ }
86
94
  // ============================================
87
95
  // Credential CRUD (committed vault)
88
96
  // ============================================
@@ -1,32 +1,36 @@
1
1
  // libs/auth/src/vault/credential-loaders/lazy-loader.ts
2
2
  import { extractCredentialExpiry } from "../credential-helpers.mjs";
3
+ function inFlightKey(providerName, context) {
4
+ return JSON.stringify([providerName, context.sessionId, context.userSub ?? null]);
5
+ }
3
6
  var LazyCredentialLoader = class {
4
7
  constructor(logger) {
5
8
  this.logger = logger;
6
9
  }
7
10
  logger;
8
- /** In-flight loading promises for deduplication */
11
+ /** In-flight loading promises for deduplication, by provider and caller */
9
12
  loading = /* @__PURE__ */ new Map();
10
13
  /**
11
14
  * Load a credential lazily.
12
- * If already loading, returns the in-flight promise (deduplication).
15
+ * If already loading for the same caller, returns the in-flight promise (deduplication).
13
16
  *
14
17
  * @param config - Provider configuration
15
18
  * @param context - Factory context
16
19
  * @returns Resolved credential or null
17
20
  */
18
21
  async load(config, context) {
19
- const existing = this.loading.get(config.name);
22
+ const key = inFlightKey(config.name, context);
23
+ const existing = this.loading.get(key);
20
24
  if (existing) {
21
25
  this.logger?.debug(`Waiting for in-flight load: ${config.name}`);
22
26
  return existing;
23
27
  }
24
28
  const promise = this.doLoad(config, context);
25
- this.loading.set(config.name, promise);
29
+ this.loading.set(key, promise);
26
30
  try {
27
31
  return await promise;
28
32
  } finally {
29
- this.loading.delete(config.name);
33
+ this.loading.delete(key);
30
34
  }
31
35
  }
32
36
  /**
@@ -100,13 +104,14 @@ var LazyCredentialLoader = class {
100
104
  }
101
105
  }
102
106
  /**
103
- * Check if a credential is currently being loaded
107
+ * Check if a credential is currently being loaded for a caller
104
108
  *
105
109
  * @param name - Provider name
110
+ * @param context - Factory context of the caller
106
111
  * @returns true if loading is in progress
107
112
  */
108
- isLoading(name) {
109
- return this.loading.has(name);
113
+ isLoading(name, context) {
114
+ return this.loading.has(inFlightKey(name, context));
110
115
  }
111
116
  /**
112
117
  * Cancel all in-flight loads (for cleanup)
@@ -67,10 +67,10 @@ export declare class CredentialProviderAlreadyRegisteredError extends AuthIntern
67
67
  constructor(name: string);
68
68
  }
69
69
  /**
70
- * Thrown when auth providers are not configured.
70
+ * Thrown when a tool reads an auth provider's credential on a server that registers no auth providers.
71
71
  */
72
72
  export declare class AuthProvidersNotConfiguredError extends AuthInternalError {
73
- constructor();
73
+ constructor(providerName?: string);
74
74
  }
75
75
  /**
76
76
  * Thrown when orchestrated auth is not available.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-internal.errors.d.ts","sourceRoot":"","sources":["../../src/errors/auth-internal.errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,iBAAiB;;CAIlE;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,iBAAiB;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK;CAInD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAG3C;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;gBAChD,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,iBAAiB;gBAC1C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,MAAM,EAAE,MAAM;CAG3B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,IAAI,EAAE,MAAM;CAQzB;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM;CAQ/B;AAED;;GAEG;AACH,qBAAa,wCAAyC,SAAQ,iBAAiB;gBACjE,IAAI,EAAE,MAAM;CAGzB;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB;;CAIrE;AAED;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,iBAAiB;;CAIvE;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB;;CAIrE;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,iBAAiB;gBAC5C,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,8BAA+B,SAAQ,iBAAiB;;CAIpE;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,iBAAiB;gBACzC,UAAU,EAAE,MAAM;CAG/B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,iBAAiB;;CAIvE;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,iBAAiB;gBAC9C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,iBAAiB;gBACtC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAoB;CAGtD"}
1
+ {"version":3,"file":"auth-internal.errors.d.ts","sourceRoot":"","sources":["../../src/errors/auth-internal.errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,iBAAiB;;CAIlE;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,iBAAiB;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK;CAInD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAG3C;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;gBAChD,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,iBAAiB;gBAC1C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,MAAM,EAAE,MAAM;CAG3B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,IAAI,EAAE,MAAM;CAQzB;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM;CAQ/B;AAED;;GAEG;AACH,qBAAa,wCAAyC,SAAQ,iBAAiB;gBACjE,IAAI,EAAE,MAAM;CAGzB;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB;gBACxD,YAAY,CAAC,EAAE,MAAM;CASlC;AAED;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,iBAAiB;;CAIvE;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB;;CAIrE;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,iBAAiB;gBAC5C,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,8BAA+B,SAAQ,iBAAiB;;CAIpE;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,iBAAiB;gBACzC,UAAU,EAAE,MAAM;CAG/B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,iBAAiB;;CAIvE;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,iBAAiB;gBAC9C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,iBAAiB;gBACtC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAoB;CAGtD"}