@frontmcp/auth 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.
Files changed (50) hide show
  1. package/authorities/authorities.engine.d.ts +25 -2
  2. package/authorities/authorities.engine.d.ts.map +1 -1
  3. package/authorities/authorities.validation.d.ts +27 -0
  4. package/authorities/authorities.validation.d.ts.map +1 -0
  5. package/authorities/index.d.ts +1 -0
  6. package/authorities/index.d.ts.map +1 -1
  7. package/authorization/dcr-client.registry.d.ts +13 -0
  8. package/authorization/dcr-client.registry.d.ts.map +1 -1
  9. package/esm/index.mjs +530 -91
  10. package/esm/package.json +5 -5
  11. package/index.d.ts +4 -4
  12. package/index.d.ts.map +1 -1
  13. package/index.js +615 -166
  14. package/jwks/jwks.service.d.ts +4 -1
  15. package/jwks/jwks.service.d.ts.map +1 -1
  16. package/options/app-auth.schema.d.ts +4 -0
  17. package/options/app-auth.schema.d.ts.map +1 -1
  18. package/options/interfaces.d.ts +48 -7
  19. package/options/interfaces.d.ts.map +1 -1
  20. package/options/orchestrated.schema.d.ts +8 -0
  21. package/options/orchestrated.schema.d.ts.map +1 -1
  22. package/options/schema.d.ts +4 -0
  23. package/options/schema.d.ts.map +1 -1
  24. package/options/shared.schemas.d.ts +2 -0
  25. package/options/shared.schemas.d.ts.map +1 -1
  26. package/package.json +5 -5
  27. package/session/authorization.store.d.ts +19 -3
  28. package/session/authorization.store.d.ts.map +1 -1
  29. package/session/federated-auth.session.d.ts +16 -0
  30. package/session/federated-auth.session.d.ts.map +1 -1
  31. package/session/index.d.ts +4 -0
  32. package/session/index.d.ts.map +1 -1
  33. package/session/pending-login.d.ts +23 -0
  34. package/session/pending-login.d.ts.map +1 -0
  35. package/session/signin-binding.d.ts +39 -0
  36. package/session/signin-binding.d.ts.map +1 -0
  37. package/ui/base-layout.d.ts +26 -0
  38. package/ui/base-layout.d.ts.map +1 -1
  39. package/ui/index.d.ts +2 -2
  40. package/ui/index.d.ts.map +1 -1
  41. package/ui/local-login.helper.d.ts +9 -2
  42. package/ui/local-login.helper.d.ts.map +1 -1
  43. package/ui/templates.d.ts +5 -4
  44. package/ui/templates.d.ts.map +1 -1
  45. package/utils/index.d.ts +2 -0
  46. package/utils/index.d.ts.map +1 -1
  47. package/utils/redirect-uri.utils.d.ts +16 -0
  48. package/utils/redirect-uri.utils.d.ts.map +1 -0
  49. package/utils/scope-grant.utils.d.ts +21 -0
  50. package/utils/scope-grant.utils.d.ts.map +1 -0
package/esm/index.mjs CHANGED
@@ -619,6 +619,12 @@ var TRANSPARENT_JWT_ALGS = [
619
619
  ];
620
620
  var MAX_JWKS_FETCH_BYTES = 1048576;
621
621
  var MAX_JWKS_FETCH_HOPS = 3;
622
+ var MISSING_EXP_ERROR = 'missing required "exp" claim';
623
+ function isMissingExpError(error) {
624
+ if (typeof error !== "object" || error === null) return false;
625
+ const { code, claim, reason } = error;
626
+ return code === "ERR_JWT_CLAIM_VALIDATION_FAILED" && claim === "exp" && reason === "missing";
627
+ }
622
628
  var WEAK_KEY_WARNING = `
623
629
  \u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
624
630
  This is considered insecure and should be updated.
@@ -688,6 +694,7 @@ var JwksService = class {
688
694
  kid = typeof header?.kid === "string" ? header.kid : void 0;
689
695
  } catch {
690
696
  }
697
+ let missingExp = false;
691
698
  for (const p of candidates) {
692
699
  let jwks;
693
700
  try {
@@ -699,7 +706,9 @@ var JwksService = class {
699
706
  // Pin to asymmetric algorithms so a symmetric key in the provider JWKS
700
707
  // can't be used to accept an HMAC-signed token, and `none` is refused.
701
708
  algorithms: [...TRANSPARENT_JWT_ALGS],
702
- ...issuerConstraint ? { issuer: issuerConstraint } : {}
709
+ ...issuerConstraint ? { issuer: issuerConstraint } : {},
710
+ // A bearer token without `exp` would be valid forever (#272).
711
+ requiredClaims: ["exp"]
703
712
  });
704
713
  return {
705
714
  ok: true,
@@ -717,11 +726,14 @@ var JwksService = class {
717
726
  if (fallbackResult.ok) {
718
727
  return fallbackResult;
719
728
  }
729
+ if (fallbackResult.error === "missing_exp") missingExp = true;
720
730
  }
721
731
  }
732
+ if (isMissingExpError(e)) missingExp = true;
722
733
  this.logger.debug("[JwksService] Failed to verify token for provider: %s", p.id, e);
723
734
  }
724
735
  }
736
+ if (missingExp) return { ok: false, error: MISSING_EXP_ERROR };
725
737
  return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}` };
726
738
  }
727
739
  /**
@@ -802,7 +814,10 @@ var JwksService = class {
802
814
  }
803
815
  }
804
816
  const nowSec = Math.floor(Date.now() / 1e3);
805
- if (payload.exp !== void 0 && (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp < nowSec)) {
817
+ if (payload.exp === void 0) {
818
+ return { ok: false, error: "missing_exp" };
819
+ }
820
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= nowSec) {
806
821
  return { ok: false, error: "token_expired" };
807
822
  }
808
823
  if (payload.nbf !== void 0 && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > nowSec)) {
@@ -851,7 +866,10 @@ var JwksService = class {
851
866
  * 1) inline jwks (if provided) → cache & return
852
867
  * 2) cached & fresh (TTL) → return
853
868
  * 3) explicit jwksUri → fetch, cache, return
854
- * 4) discover jwks_uri via AS → fetch AS metadata, then jwks_uri, cache, return
869
+ * 4) discover jwks_uri → the issuer's OAuth authorization server
870
+ * metadata (RFC 8414), else its OpenID configuration (OpenID Connect
871
+ * Discovery 1.0 §4, where Google, Okta, Auth0 and Entra publish it),
872
+ * then fetch jwks_uri, cache, return
855
873
  */
856
874
  async getJwksForProvider(ref) {
857
875
  if (ref.jwks?.keys?.length) {
@@ -867,11 +885,13 @@ var JwksService = class {
867
885
  if (fromUri?.keys?.length) return fromUri;
868
886
  }
869
887
  const issuer = trimSlash(ref.issuerUrl);
870
- const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/oauth-authorization-server`);
871
- const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
872
- if (uri) {
873
- const fromMeta = await this.tryFetchJwks(ref.id, uri);
874
- if (fromMeta?.keys?.length) return fromMeta;
888
+ for (const document of ["oauth-authorization-server", "openid-configuration"]) {
889
+ const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/${document}`);
890
+ const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
891
+ if (uri) {
892
+ const fromMeta = await this.tryFetchJwks(ref.id, uri);
893
+ if (fromMeta?.keys?.length) return fromMeta;
894
+ }
875
895
  }
876
896
  const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
877
897
  if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
@@ -1058,6 +1078,26 @@ var CDN = {
1058
1078
  stylesheet: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
1059
1079
  }
1060
1080
  };
1081
+ var BUILT_IN_AUTH_PAGE_CSP = [
1082
+ "default-src 'self'",
1083
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
1084
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
1085
+ "font-src 'self' https://fonts.gstatic.com",
1086
+ "img-src 'self' data: https:",
1087
+ "connect-src 'self'",
1088
+ "object-src 'none'",
1089
+ "base-uri 'none'",
1090
+ "frame-ancestors 'none'"
1091
+ ].join("; ");
1092
+ function builtInAuthPageHeaders(options = {}) {
1093
+ return {
1094
+ ...options.csp === false ? {} : { "Content-Security-Policy": BUILT_IN_AUTH_PAGE_CSP },
1095
+ "X-Frame-Options": "DENY",
1096
+ "X-Content-Type-Options": "nosniff",
1097
+ "Referrer-Policy": "same-origin",
1098
+ "Cache-Control": "no-store"
1099
+ };
1100
+ }
1061
1101
  var DEFAULT_THEME = {
1062
1102
  colors: {
1063
1103
  primary: "#3b82f6",
@@ -1295,7 +1335,7 @@ function buildIncrementalAuthPage(params) {
1295
1335
  </div>
1296
1336
  </div>
1297
1337
 
1298
- <form method="GET" action="${escapeHtml3(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
1338
+ <form method="POST" action="${escapeHtml3(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
1299
1339
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(sessionHint)}">
1300
1340
  <button type="button" onclick="window.close()"
1301
1341
  class="flex-1 px-4 py-2.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 font-medium rounded-lg transition-colors">
@@ -1341,7 +1381,7 @@ function buildFederatedLoginPage(params) {
1341
1381
  ${escapeHtml3(clientName)} uses multiple authentication providers. Select which ones you want to authorize.
1342
1382
  </p>
1343
1383
 
1344
- <form method="GET" action="${escapeHtml3(callbackPath)}" id="federated-form">
1384
+ <form method="POST" action="${escapeHtml3(callbackPath)}" id="federated-form">
1345
1385
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1346
1386
  <input type="hidden" name="federated" value="true">
1347
1387
 
@@ -1486,7 +1526,7 @@ function buildToolConsentPage(params) {
1486
1526
  ${errorBanner}
1487
1527
  ${userInfo}
1488
1528
 
1489
- <form method="GET" action="${escapeHtml3(callbackPath)}" id="consent-form">
1529
+ <form method="POST" action="${escapeHtml3(callbackPath)}" id="consent-form">
1490
1530
  <input type="hidden" name="csrf" value="${escapeHtml3(csrfToken)}">
1491
1531
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1492
1532
  <input type="hidden" name="consent_submitted" value="1">
@@ -1584,7 +1624,7 @@ function buildLoginPage(params) {
1584
1624
 
1585
1625
  ${scopesHtml}
1586
1626
 
1587
- <form method="GET" action="${escapeHtml3(callbackPath)}">
1627
+ <form method="POST" action="${escapeHtml3(callbackPath)}">
1588
1628
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1589
1629
  ${fieldsHtml}
1590
1630
 
@@ -1681,7 +1721,17 @@ function toLoginExtraFields(fields, values) {
1681
1721
  value: values?.[name]
1682
1722
  }));
1683
1723
  }
1684
- function renderLocalLoginPage(login, ctx, values) {
1724
+ function withoutSecretFieldValues(values, fields) {
1725
+ if (!values) return void 0;
1726
+ const kept = {};
1727
+ for (const [name, value] of Object.entries(values)) {
1728
+ if (fields?.[name]?.type === "password") continue;
1729
+ kept[name] = value;
1730
+ }
1731
+ return kept;
1732
+ }
1733
+ function renderLocalLoginPage(login, ctx, submitted) {
1734
+ const values = withoutSecretFieldValues(submitted, login?.fields);
1685
1735
  if (login?.render) {
1686
1736
  return login.render({ ...ctx, values });
1687
1737
  }
@@ -1788,7 +1838,8 @@ function buildPendingRecord(params) {
1788
1838
  priorAuthorizedAppIds: params.priorAuthorizedAppIds,
1789
1839
  federatedLogin: params.federatedLogin,
1790
1840
  consent: params.consent,
1791
- authUiCsrf: params.authUiCsrf
1841
+ authUiCsrf: params.authUiCsrf,
1842
+ signinBinding: params.signinBinding
1792
1843
  };
1793
1844
  }
1794
1845
  function buildRefreshTokenRecord(params) {
@@ -2018,6 +2069,63 @@ var RedisAuthorizationStore = class {
2018
2069
  }
2019
2070
  };
2020
2071
 
2072
+ // libs/auth/src/session/pending-login.ts
2073
+ import {
2074
+ base64urlDecode,
2075
+ base64urlEncode,
2076
+ decryptAesGcm,
2077
+ encryptAesGcm,
2078
+ hkdfSha256,
2079
+ randomBytes as randomBytes2
2080
+ } from "@frontmcp/utils";
2081
+ var SALT = new TextEncoder().encode("frontmcp-pending-login");
2082
+ function keyFor(secret, pendingAuthId) {
2083
+ return hkdfSha256(secret, SALT, new TextEncoder().encode(`pending-login:${pendingAuthId}`), 32);
2084
+ }
2085
+ function sealPendingLogin(secret, pendingAuthId, state) {
2086
+ const iv = randomBytes2(12);
2087
+ const { ciphertext, tag } = encryptAesGcm(
2088
+ keyFor(secret, pendingAuthId),
2089
+ new TextEncoder().encode(JSON.stringify(state)),
2090
+ iv
2091
+ );
2092
+ return [iv, tag, ciphertext].map((part) => base64urlEncode(part)).join(".");
2093
+ }
2094
+ function openPendingLogin(secret, pendingAuthId, sealed) {
2095
+ try {
2096
+ const [iv, tag, ciphertext] = sealed.split(".").map((part) => base64urlDecode(part));
2097
+ if (!iv || !tag || !ciphertext) return void 0;
2098
+ const plaintext = decryptAesGcm(keyFor(secret, pendingAuthId), ciphertext, iv, tag);
2099
+ const state = JSON.parse(new TextDecoder().decode(plaintext));
2100
+ return typeof state?.sub === "string" && state.sub.length > 0 ? state : void 0;
2101
+ } catch {
2102
+ return void 0;
2103
+ }
2104
+ }
2105
+
2106
+ // libs/auth/src/session/signin-binding.ts
2107
+ import { base64urlEncode as base64urlEncode2, getCookie, randomBytes as randomBytes3, sha256Hex, timingSafeEqual } from "@frontmcp/utils";
2108
+ var SIGNIN_BINDING_COOKIE_PREFIX = "frontmcp_signin_";
2109
+ var SIGNIN_BINDING_SECURE_COOKIE_PREFIX = `__Host-${SIGNIN_BINDING_COOKIE_PREFIX}`;
2110
+ var SIGNIN_BINDING_MAX_AGE_SECONDS = 30 * 60;
2111
+ function signinBindingCookieName(pendingAuthId, options = {}) {
2112
+ const prefix = options.secure ? SIGNIN_BINDING_SECURE_COOKIE_PREFIX : SIGNIN_BINDING_COOKIE_PREFIX;
2113
+ return `${prefix}${sha256Hex(`signin:${pendingAuthId}`).slice(0, 16)}`;
2114
+ }
2115
+ function createSigninBinding(pendingAuthId, options = {}) {
2116
+ const value = base64urlEncode2(randomBytes3(32));
2117
+ return { cookieName: signinBindingCookieName(pendingAuthId, options), value, hash: sha256Hex(value) };
2118
+ }
2119
+ function signinBindingMatches(cookieHeader, pendingAuthId, expectedHash, options = {}) {
2120
+ if (!expectedHash) return false;
2121
+ const value = getCookie(cookieHeader, signinBindingCookieName(pendingAuthId, options));
2122
+ if (!value) return false;
2123
+ const encoder = new TextEncoder();
2124
+ const actual = encoder.encode(sha256Hex(value));
2125
+ const expected = encoder.encode(expectedHash);
2126
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
2127
+ }
2128
+
2021
2129
  // libs/auth/src/session/storage-authorization.store.ts
2022
2130
  import { TypedStorage } from "@frontmcp/utils";
2023
2131
  var StorageAuthorizationStore = class {
@@ -2410,12 +2518,12 @@ import { getEnv, randomUUID as randomUUID2 } from "@frontmcp/utils";
2410
2518
  // libs/auth/src/session/vault-encryption.ts
2411
2519
  import { z as z3 } from "@frontmcp/lazy-zod";
2412
2520
  import {
2413
- base64urlDecode,
2414
- base64urlEncode,
2415
- decryptAesGcm,
2416
- encryptAesGcm,
2417
- hkdfSha256,
2418
- randomBytes as randomBytes2
2521
+ base64urlDecode as base64urlDecode2,
2522
+ base64urlEncode as base64urlEncode3,
2523
+ decryptAesGcm as decryptAesGcm2,
2524
+ encryptAesGcm as encryptAesGcm2,
2525
+ hkdfSha256 as hkdfSha2562,
2526
+ randomBytes as randomBytes4
2419
2527
  } from "@frontmcp/utils";
2420
2528
  var encryptedDataSchema = z3.object({
2421
2529
  /** Version for future algorithm changes */
@@ -2456,7 +2564,7 @@ var VaultEncryption = class {
2456
2564
  ];
2457
2565
  const ikm = new TextEncoder().encode(ikmParts.join(""));
2458
2566
  const infoBytes = new TextEncoder().encode(this.hkdfInfo);
2459
- const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
2567
+ const key = await hkdfSha2562(ikm, new Uint8Array(0), infoBytes, 32);
2460
2568
  return key;
2461
2569
  }
2462
2570
  /**
@@ -2483,7 +2591,7 @@ var VaultEncryption = class {
2483
2591
  ];
2484
2592
  const ikm = new TextEncoder().encode(ikmParts.join(""));
2485
2593
  const infoBytes = new TextEncoder().encode(this.hkdfInfo);
2486
- const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
2594
+ const key = await hkdfSha2562(ikm, new Uint8Array(0), infoBytes, 32);
2487
2595
  return key;
2488
2596
  }
2489
2597
  /**
@@ -2497,14 +2605,14 @@ var VaultEncryption = class {
2497
2605
  if (key.length !== 32) {
2498
2606
  throw new Error("Encryption key must be 32 bytes");
2499
2607
  }
2500
- const iv = randomBytes2(12);
2501
- const { ciphertext, tag } = await encryptAesGcm(key, new TextEncoder().encode(plaintext), iv);
2608
+ const iv = randomBytes4(12);
2609
+ const { ciphertext, tag } = await encryptAesGcm2(key, new TextEncoder().encode(plaintext), iv);
2502
2610
  return {
2503
2611
  v: 1,
2504
2612
  alg: "aes-256-gcm",
2505
- iv: base64urlEncode(iv),
2506
- ct: base64urlEncode(ciphertext),
2507
- tag: base64urlEncode(tag)
2613
+ iv: base64urlEncode3(iv),
2614
+ ct: base64urlEncode3(ciphertext),
2615
+ tag: base64urlEncode3(tag)
2508
2616
  };
2509
2617
  }
2510
2618
  /**
@@ -2524,11 +2632,11 @@ var VaultEncryption = class {
2524
2632
  throw new Error("Invalid encrypted data format");
2525
2633
  }
2526
2634
  const { iv, ct, tag } = parsed.data;
2527
- const ivBuffer = base64urlDecode(iv);
2528
- const ciphertext = base64urlDecode(ct);
2529
- const tagBuffer = base64urlDecode(tag);
2635
+ const ivBuffer = base64urlDecode2(iv);
2636
+ const ciphertext = base64urlDecode2(ct);
2637
+ const tagBuffer = base64urlDecode2(tag);
2530
2638
  try {
2531
- const plaintext = await decryptAesGcm(key, ciphertext, ivBuffer, tagBuffer);
2639
+ const plaintext = await decryptAesGcm2(key, ciphertext, ivBuffer, tagBuffer);
2532
2640
  return new TextDecoder().decode(plaintext);
2533
2641
  } catch (_error) {
2534
2642
  throw new Error("Decryption failed: invalid key or corrupted data");
@@ -3124,7 +3232,7 @@ function errMsg2(err) {
3124
3232
  var SECURE_STORE_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:SECURE_STORE_ACCESSOR");
3125
3233
 
3126
3234
  // libs/auth/src/session/secure-store-accessor.impl.ts
3127
- import { sha256Hex } from "@frontmcp/utils";
3235
+ import { sha256Hex as sha256Hex2 } from "@frontmcp/utils";
3128
3236
  var GLOBAL_NAMESPACE = "global";
3129
3237
  var SecureStoreAccessorImpl = class {
3130
3238
  backend;
@@ -3153,12 +3261,12 @@ var SecureStoreAccessorImpl = class {
3153
3261
  return GLOBAL_NAMESPACE;
3154
3262
  case "session": {
3155
3263
  const sid = this.resolveSessionId();
3156
- return sid ? `s:${sha256Hex(sid).slice(0, 32)}` : void 0;
3264
+ return sid ? `s:${sha256Hex2(sid).slice(0, 32)}` : void 0;
3157
3265
  }
3158
3266
  case "user":
3159
3267
  default: {
3160
3268
  const sub = this.resolveSub();
3161
- return sub ? `u:${sha256Hex(sub).slice(0, 32)}` : void 0;
3269
+ return sub ? `u:${sha256Hex2(sub).slice(0, 32)}` : void 0;
3162
3270
  }
3163
3271
  }
3164
3272
  }
@@ -3262,7 +3370,7 @@ async function createSecureStore(options) {
3262
3370
  }
3263
3371
 
3264
3372
  // libs/auth/src/session/token.vault.ts
3265
- import { encryptAesGcm as encryptAesGcm2, decryptAesGcm as decryptAesGcm2, randomBytes as randomBytes3, base64urlEncode as base64urlEncode2, base64urlDecode as base64urlDecode2 } from "@frontmcp/utils";
3373
+ import { encryptAesGcm as encryptAesGcm3, decryptAesGcm as decryptAesGcm3, randomBytes as randomBytes5, base64urlEncode as base64urlEncode4, base64urlDecode as base64urlDecode3 } from "@frontmcp/utils";
3266
3374
  var TokenVault = class {
3267
3375
  /** Active key used for new encryptions */
3268
3376
  active;
@@ -3291,14 +3399,14 @@ var TokenVault = class {
3291
3399
  this.keys.set(k.kid, k.key);
3292
3400
  }
3293
3401
  async encrypt(plaintext, opts) {
3294
- const iv = randomBytes3(12);
3295
- const { ciphertext, tag } = await encryptAesGcm2(this.active.key, new TextEncoder().encode(plaintext), iv);
3402
+ const iv = randomBytes5(12);
3403
+ const { ciphertext, tag } = await encryptAesGcm3(this.active.key, new TextEncoder().encode(plaintext), iv);
3296
3404
  return {
3297
3405
  alg: "A256GCM",
3298
3406
  kid: this.active.kid,
3299
- iv: base64urlEncode2(iv),
3300
- tag: base64urlEncode2(tag),
3301
- data: base64urlEncode2(ciphertext),
3407
+ iv: base64urlEncode4(iv),
3408
+ tag: base64urlEncode4(tag),
3409
+ data: base64urlEncode4(ciphertext),
3302
3410
  exp: opts?.exp,
3303
3411
  meta: opts?.meta
3304
3412
  };
@@ -3312,21 +3420,21 @@ var TokenVault = class {
3312
3420
  }
3313
3421
  const key = this.keys.get(blob.kid);
3314
3422
  if (!key) throw new Error(`vault_unknown_kid:${blob.kid}`);
3315
- const iv = base64urlDecode2(blob.iv);
3316
- const tag = base64urlDecode2(blob.tag);
3317
- const data = base64urlDecode2(blob.data);
3318
- const plaintext = await decryptAesGcm2(key, data, iv, tag);
3423
+ const iv = base64urlDecode3(blob.iv);
3424
+ const tag = base64urlDecode3(blob.tag);
3425
+ const data = base64urlDecode3(blob.data);
3426
+ const plaintext = await decryptAesGcm3(key, data, iv, tag);
3319
3427
  return new TextDecoder().decode(plaintext);
3320
3428
  }
3321
3429
  };
3322
3430
 
3323
3431
  // libs/auth/src/session/index.ts
3324
3432
  import {
3325
- hkdfSha256 as hkdfSha2564,
3433
+ hkdfSha256 as hkdfSha2565,
3326
3434
  encryptValue as encryptValue2,
3327
3435
  decryptValue as decryptValue2,
3328
- encryptAesGcm as encryptAesGcm5,
3329
- decryptAesGcm as decryptAesGcm5
3436
+ encryptAesGcm as encryptAesGcm6,
3437
+ decryptAesGcm as decryptAesGcm6
3330
3438
  } from "@frontmcp/utils";
3331
3439
 
3332
3440
  // libs/auth/src/session/transport-session.types.ts
@@ -3433,7 +3541,7 @@ import {
3433
3541
  } from "@frontmcp/utils";
3434
3542
 
3435
3543
  // libs/auth/src/errors/auth-internal.error.ts
3436
- import { randomBytes as randomBytes4, bytesToHex as bytesToHex2 } from "@frontmcp/utils";
3544
+ import { randomBytes as randomBytes6, bytesToHex as bytesToHex2 } from "@frontmcp/utils";
3437
3545
  var AuthInternalError = class extends Error {
3438
3546
  /**
3439
3547
  * Unique error ID for tracking in logs.
@@ -3455,7 +3563,7 @@ var AuthInternalError = class extends Error {
3455
3563
  super(message);
3456
3564
  this.name = this.constructor.name;
3457
3565
  this.code = code;
3458
- this.errorId = `err_${bytesToHex2(randomBytes4(8))}`;
3566
+ this.errorId = `err_${bytesToHex2(randomBytes6(8))}`;
3459
3567
  Error.captureStackTrace(this, this.constructor);
3460
3568
  }
3461
3569
  /**
@@ -4761,7 +4869,7 @@ var VercelKvSessionStore = class {
4761
4869
  };
4762
4870
 
4763
4871
  // libs/auth/src/session/orchestrated-token.store.ts
4764
- import { encryptAesGcm as encryptAesGcm3, decryptAesGcm as decryptAesGcm3, randomBytes as randomBytes5, hkdfSha256 as hkdfSha2562 } from "@frontmcp/utils";
4872
+ import { encryptAesGcm as encryptAesGcm4, decryptAesGcm as decryptAesGcm4, randomBytes as randomBytes7, hkdfSha256 as hkdfSha2563 } from "@frontmcp/utils";
4765
4873
  var InMemoryOrchestratedTokenStore = class {
4766
4874
  /** Token storage: Map<compositeKey, ProviderTokenRecord> */
4767
4875
  tokens = /* @__PURE__ */ new Map();
@@ -4803,7 +4911,7 @@ var InMemoryOrchestratedTokenStore = class {
4803
4911
  }
4804
4912
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
4805
4913
  const salt = new TextEncoder().encode("frontmcp-token-store");
4806
- const derivedKey = hkdfSha2562(this.encryptionKey, salt, info, 32);
4914
+ const derivedKey = hkdfSha2563(this.encryptionKey, salt, info, 32);
4807
4915
  this.derivedKeys.set(compositeKey, derivedKey);
4808
4916
  return derivedKey;
4809
4917
  }
@@ -4813,8 +4921,8 @@ var InMemoryOrchestratedTokenStore = class {
4813
4921
  async encryptRecord(compositeKey, record) {
4814
4922
  const key = await this.deriveKeyForRecord(compositeKey);
4815
4923
  const plaintext = JSON.stringify(record);
4816
- const iv = randomBytes5(12);
4817
- const { ciphertext, tag } = encryptAesGcm3(key, new TextEncoder().encode(plaintext), iv);
4924
+ const iv = randomBytes7(12);
4925
+ const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
4818
4926
  return JSON.stringify({
4819
4927
  iv: Buffer.from(iv).toString("base64url"),
4820
4928
  tag: Buffer.from(tag).toString("base64url"),
@@ -4830,7 +4938,7 @@ var InMemoryOrchestratedTokenStore = class {
4830
4938
  const ivBytes = Buffer.from(iv, "base64url");
4831
4939
  const tagBytes = Buffer.from(tag, "base64url");
4832
4940
  const ciphertextBytes = Buffer.from(data, "base64url");
4833
- const plaintext = decryptAesGcm3(key, ciphertextBytes, ivBytes, tagBytes);
4941
+ const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
4834
4942
  return JSON.parse(new TextDecoder().decode(plaintext));
4835
4943
  }
4836
4944
  /**
@@ -5032,7 +5140,7 @@ var InMemoryOrchestratedTokenStore = class {
5032
5140
  };
5033
5141
 
5034
5142
  // libs/auth/src/session/orchestrated-token.crypto.ts
5035
- import { decryptAesGcm as decryptAesGcm4, encryptAesGcm as encryptAesGcm4, hkdfSha256 as hkdfSha2563, randomBytes as randomBytes6 } from "@frontmcp/utils";
5143
+ import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
5036
5144
  var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
5037
5145
  function deriveKeyForRecord(masterKey, compositeKey, cache) {
5038
5146
  const cached = cache.get(compositeKey);
@@ -5040,14 +5148,14 @@ function deriveKeyForRecord(masterKey, compositeKey, cache) {
5040
5148
  return cached;
5041
5149
  }
5042
5150
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5043
- const derived = hkdfSha2563(masterKey, HKDF_SALT, info, 32);
5151
+ const derived = hkdfSha2564(masterKey, HKDF_SALT, info, 32);
5044
5152
  cache.set(compositeKey, derived);
5045
5153
  return derived;
5046
5154
  }
5047
5155
  function encryptRecord(key, record) {
5048
5156
  const plaintext = JSON.stringify(record);
5049
- const iv = randomBytes6(12);
5050
- const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
5157
+ const iv = randomBytes8(12);
5158
+ const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
5051
5159
  return JSON.stringify({
5052
5160
  iv: Buffer.from(iv).toString("base64url"),
5053
5161
  tag: Buffer.from(tag).toString("base64url"),
@@ -5059,7 +5167,7 @@ function decryptRecord(key, encrypted) {
5059
5167
  const ivBytes = Buffer.from(iv, "base64url");
5060
5168
  const tagBytes = Buffer.from(tag, "base64url");
5061
5169
  const ciphertextBytes = Buffer.from(data, "base64url");
5062
- const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
5170
+ const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
5063
5171
  return JSON.parse(new TextDecoder().decode(plaintext));
5064
5172
  }
5065
5173
 
@@ -5325,6 +5433,7 @@ var InMemoryFederatedAuthSessionStore = class {
5325
5433
  providerQueue: [...params.providerIds],
5326
5434
  completedProviders: /* @__PURE__ */ new Map(),
5327
5435
  skippedProviders: [],
5436
+ signinBinding: params.signinBinding,
5328
5437
  createdAt: now,
5329
5438
  expiresAt: now + this.sessionTtlMs
5330
5439
  };
@@ -5357,6 +5466,7 @@ function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
5357
5466
  providerQueue: [...params.providerIds],
5358
5467
  completedProviders: /* @__PURE__ */ new Map(),
5359
5468
  skippedProviders: [],
5469
+ signinBinding: params.signinBinding,
5360
5470
  createdAt: now,
5361
5471
  expiresAt: now + ttlMs
5362
5472
  };
@@ -5911,7 +6021,7 @@ function createEncryptedVault(redis, config = {}) {
5911
6021
  }
5912
6022
 
5913
6023
  // libs/auth/src/session/token.refresh.ts
5914
- import { base64urlDecode as base64urlDecode3 } from "@frontmcp/utils";
6024
+ import { base64urlDecode as base64urlDecode4 } from "@frontmcp/utils";
5915
6025
  function toEpochSeconds(exp) {
5916
6026
  if (exp == null) return void 0;
5917
6027
  if (exp instanceof Date) return Math.floor(exp.getTime() / 1e3);
@@ -5933,7 +6043,7 @@ function tryJwtExp(token) {
5933
6043
  const parts = token.split(".");
5934
6044
  if (parts.length < 2) return void 0;
5935
6045
  try {
5936
- const json = JSON.parse(new TextDecoder().decode(base64urlDecode3(parts[1])));
6046
+ const json = JSON.parse(new TextDecoder().decode(base64urlDecode4(parts[1])));
5937
6047
  const e = json?.exp;
5938
6048
  return typeof e === "number" ? toEpochSeconds(e) : void 0;
5939
6049
  } catch {
@@ -5996,7 +6106,7 @@ var progressiveAuthStateSchema = z6.object({
5996
6106
  });
5997
6107
 
5998
6108
  // libs/auth/src/authorization/authorization.class.ts
5999
- import { base64urlDecode as base64urlDecode4, getMachineId as getMachineId2, randomUUID as randomUUID11 } from "@frontmcp/utils";
6109
+ import { base64urlDecode as base64urlDecode5, getMachineId as getMachineId2, randomUUID as randomUUID11 } from "@frontmcp/utils";
6000
6110
  var AuthorizationBase = class _AuthorizationBase {
6001
6111
  id;
6002
6112
  isAnonymous;
@@ -6192,7 +6302,7 @@ var AuthorizationBase = class _AuthorizationBase {
6192
6302
  */
6193
6303
  static isJwtHeader(segment) {
6194
6304
  try {
6195
- const bytes = base64urlDecode4(segment);
6305
+ const bytes = base64urlDecode5(segment);
6196
6306
  const text = new TextDecoder().decode(bytes);
6197
6307
  const parsed = JSON.parse(text);
6198
6308
  return typeof parsed === "object" && parsed !== null && typeof parsed.alg === "string";
@@ -6364,7 +6474,7 @@ var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
6364
6474
  };
6365
6475
 
6366
6476
  // libs/auth/src/authorization/transparent.authorization.ts
6367
- import { sha256Hex as sha256Hex2 } from "@frontmcp/utils";
6477
+ import { sha256Hex as sha256Hex3 } from "@frontmcp/utils";
6368
6478
  var TransparentAuthorization = class _TransparentAuthorization extends AuthorizationBase {
6369
6479
  mode = "transparent";
6370
6480
  /**
@@ -6464,7 +6574,7 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
6464
6574
  static generateAuthorizationId(token) {
6465
6575
  const parts = token.split(".");
6466
6576
  const signature = parts[2] || token;
6467
- return sha256Hex2(signature).substring(0, 16);
6577
+ return sha256Hex3(signature).substring(0, 16);
6468
6578
  }
6469
6579
  /**
6470
6580
  * Get the issuer from the token claims
@@ -6490,11 +6600,11 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
6490
6600
  };
6491
6601
 
6492
6602
  // libs/auth/src/utils/authorization-id.utils.ts
6493
- import { sha256Hex as sha256Hex3 } from "@frontmcp/utils";
6603
+ import { sha256Hex as sha256Hex4 } from "@frontmcp/utils";
6494
6604
  function deriveAuthorizationId(token) {
6495
6605
  const parts = token.split(".");
6496
6606
  const signature = parts[2] || token;
6497
- return sha256Hex3(signature).substring(0, 16);
6607
+ return sha256Hex4(signature).substring(0, 16);
6498
6608
  }
6499
6609
 
6500
6610
  // libs/auth/src/authorization/orchestrated.authorization.ts
@@ -6863,7 +6973,7 @@ var OrchestratedAuthAccessorAdapter = class {
6863
6973
  };
6864
6974
 
6865
6975
  // libs/auth/src/authorization/dcr-client.registry.ts
6866
- import { sha256 as sha2562, timingSafeEqual } from "@frontmcp/utils";
6976
+ import { sha256 as sha2562, timingSafeEqual as timingSafeEqual2 } from "@frontmcp/utils";
6867
6977
  var DEFAULT_MAX_DYNAMIC_CLIENTS = 1e3;
6868
6978
  function globMatch(pattern, input) {
6869
6979
  let p = 0;
@@ -6989,7 +7099,7 @@ var DcrClientRegistry = class {
6989
7099
  if (!isConfidential) return "ok";
6990
7100
  if (!client.client_secret) return "invalid";
6991
7101
  if (!presentedSecret) return "invalid";
6992
- return timingSafeEqual(sha2562(presentedSecret), sha2562(client.client_secret)) ? "ok" : "invalid";
7102
+ return timingSafeEqual2(sha2562(presentedSecret), sha2562(client.client_secret)) ? "ok" : "invalid";
6993
7103
  }
6994
7104
  /** Whether a client id is known to the registry. */
6995
7105
  has(clientId) {
@@ -7077,7 +7187,7 @@ var DcrClientRegistry = class {
7077
7187
  if (typeof presented !== "string" || presented.length === 0) {
7078
7188
  return false;
7079
7189
  }
7080
- return timingSafeEqual(sha2562(presented), sha2562(expected));
7190
+ return timingSafeEqual2(sha2562(presented), sha2562(expected));
7081
7191
  }
7082
7192
  };
7083
7193
 
@@ -7894,7 +8004,16 @@ var upstreamProviderSchema = z10.object({
7894
8004
  /** User info endpoint (optional, used to enrich the session identity). */
7895
8005
  userInfoEndpoint: z10.string().url().optional(),
7896
8006
  /** JWKS URI for upstream id_token validation (optional). */
7897
- jwksUri: z10.string().url().optional()
8007
+ jwksUri: z10.string().url().optional(),
8008
+ /**
8009
+ * The provider's issuer identifier (its OIDC `iss`). When set, an RFC 9207
8010
+ * `iss` on the provider's callback and the `iss` of its `id_token` must name
8011
+ * it (or one of {@link additionalIssuers}). Without it the provider's
8012
+ * `id_token` is not used for identity; `userInfoEndpoint` is asked instead.
8013
+ */
8014
+ issuer: z10.string().url().optional(),
8015
+ /** Other issuer values this provider legitimately uses. */
8016
+ additionalIssuers: z10.array(z10.string().trim().min(1, "additionalIssuers entries must be non-empty")).optional()
7898
8017
  }).refine((p) => !!(p.authorizationEndpoint ?? p.authorizeUrl), {
7899
8018
  message: "authorizationEndpoint (or authorizeUrl) is required",
7900
8019
  path: ["authorizationEndpoint"]
@@ -8303,17 +8422,21 @@ var sharedAuthFields = {
8303
8422
  * Require the OAuth client to be KNOWN (registered via DCR / pre-registered,
8304
8423
  * or a CIMD client-id URL) before an authorization request is accepted.
8305
8424
  *
8306
- * @default false (backward compatible — unknown client ids are accepted).
8425
+ * @default true
8307
8426
  *
8308
- * SECURITY: with the default, an unregistered client id has no trusted
8309
- * `redirect_uris` to validate against, so its (attacker-chosen) redirect_uri
8310
- * is accepted and a real authorization code can be delivered to an attacker
8311
- * (auth-code interception → account takeover in real-IdP modes). Set this to
8312
- * `true` (recommended for production) to enforce OAuth 2.1 exact redirect-uri
8313
- * matching for every client. Clients on a configured
8314
- * `dcr.allowedRedirectUris` allowlist still pass.
8315
- */
8316
- requireRegisteredClients: z14.boolean().default(false),
8427
+ * SECURITY: an unregistered client id has no trusted `redirect_uris` to
8428
+ * validate against, so accepting it lets anyone start a sign-in whose
8429
+ * authorization code is delivered to a redirect_uri of their choosing
8430
+ * (auth-code interception). `false` is for local development only; a
8431
+ * `dcr.allowedRedirectUris` allowlist still constrains such clients.
8432
+ */
8433
+ requireRegisteredClients: z14.boolean().default(true),
8434
+ /**
8435
+ * Scopes the server may grant (exact values or `*` globs). Requested scopes
8436
+ * outside this list are dropped from the grant. Unset → the standard OpenID
8437
+ * scopes only (see `DEFAULT_ALLOWED_SCOPES`).
8438
+ */
8439
+ allowedScopes: z14.array(z14.string().trim().min(1, "allowedScopes entries must be non-empty")).optional(),
8317
8440
  cimd: cimdConfigSchema.optional(),
8318
8441
  // #469 — custom auth UI as a slot→file map + extras name→handler map (per-app
8319
8442
  // under splitByApp). Both optional; omitting them serves the built-in pages.
@@ -8924,6 +9047,50 @@ var AudienceValidator = class _AudienceValidator {
8924
9047
  }
8925
9048
  };
8926
9049
 
9050
+ // libs/auth/src/utils/scope-grant.utils.ts
9051
+ var DEFAULT_ALLOWED_SCOPES = Object.freeze([
9052
+ "openid",
9053
+ "profile",
9054
+ "email",
9055
+ "offline_access"
9056
+ ]);
9057
+ function grantScopes(requested, allowed) {
9058
+ const patterns = allowed ?? DEFAULT_ALLOWED_SCOPES;
9059
+ const granted = [];
9060
+ for (const scope of requested) {
9061
+ if (!scope || granted.includes(scope)) continue;
9062
+ if (patterns.some((pattern) => globMatch(pattern, scope))) granted.push(scope);
9063
+ }
9064
+ return granted;
9065
+ }
9066
+ function advertisedScopes(allowed) {
9067
+ const scopes = [];
9068
+ for (const scope of allowed ?? DEFAULT_ALLOWED_SCOPES) {
9069
+ if (scope && !scope.includes("*") && !scopes.includes(scope)) scopes.push(scope);
9070
+ }
9071
+ return scopes;
9072
+ }
9073
+
9074
+ // libs/auth/src/utils/redirect-uri.utils.ts
9075
+ function isLoopbackRedirectUri(uri) {
9076
+ let url;
9077
+ try {
9078
+ url = new URL(uri);
9079
+ } catch {
9080
+ return false;
9081
+ }
9082
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
9083
+ if (url.username || url.password) return false;
9084
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
9085
+ if (host === "localhost" || host === "::1") return true;
9086
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
9087
+ if (m) {
9088
+ const octets = m.slice(1).map((n) => Number(n));
9089
+ return octets.every((o) => o >= 0 && o <= 255) && octets[0] === 127;
9090
+ }
9091
+ return false;
9092
+ }
9093
+
8927
9094
  // libs/auth/src/vault/auth-providers.types.ts
8928
9095
  import { z as z19 } from "@frontmcp/lazy-zod";
8929
9096
  var credentialScopeSchema = z19.enum(["global", "user", "session"]);
@@ -10489,6 +10656,211 @@ async function evaluateRebac(policy, ctx) {
10489
10656
  return { granted: true, evaluatedPolicies: ["relationships"] };
10490
10657
  }
10491
10658
 
10659
+ // libs/auth/src/authorities/authorities.validation.ts
10660
+ var ABAC_OPERATORS = /* @__PURE__ */ new Set([
10661
+ "eq",
10662
+ "neq",
10663
+ "in",
10664
+ "notIn",
10665
+ "gt",
10666
+ "gte",
10667
+ "lt",
10668
+ "lte",
10669
+ "contains",
10670
+ "startsWith",
10671
+ "endsWith",
10672
+ "exists",
10673
+ "matches"
10674
+ ]);
10675
+ var CHECK_FIELDS = [
10676
+ "roles",
10677
+ "permissions",
10678
+ "attributes",
10679
+ "relationships",
10680
+ "custom",
10681
+ "guards",
10682
+ "allOf",
10683
+ "anyOf",
10684
+ "not"
10685
+ ];
10686
+ var RULE_FIELDS = /* @__PURE__ */ new Set([...CHECK_FIELDS, "operator"]);
10687
+ function isPlainObject(value) {
10688
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10689
+ }
10690
+ function isNonEmptyString(value) {
10691
+ return typeof value === "string" && value.length > 0;
10692
+ }
10693
+ function unknownFields(value, known, path) {
10694
+ const prefix = path ? `${path} has` : "has";
10695
+ return Object.keys(value).filter((key) => !known.has(key)).map((key) => `${prefix} an unknown field "${key}"`);
10696
+ }
10697
+ function checkStringList(value, path) {
10698
+ if (!Array.isArray(value)) return [`${path} must be a list of names`];
10699
+ if (value.length === 0) return [`${path} is empty`];
10700
+ return value.every(isNonEmptyString) ? [] : [`${path} must contain only non-empty names`];
10701
+ }
10702
+ var ALL_OR_ANY = /* @__PURE__ */ new Set(["all", "any"]);
10703
+ function checkAllOrAny(value, path) {
10704
+ if (!isPlainObject(value)) return [`${path} must be an object with "all" or "any"`];
10705
+ const problems = unknownFields(value, ALL_OR_ANY, path);
10706
+ if (value["all"] === void 0 && value["any"] === void 0) {
10707
+ problems.push(`${path} needs "all" or "any"`);
10708
+ }
10709
+ if (value["all"] !== void 0) problems.push(...checkStringList(value["all"], `${path}.all`));
10710
+ if (value["any"] !== void 0) problems.push(...checkStringList(value["any"], `${path}.any`));
10711
+ return problems;
10712
+ }
10713
+ var ATTRIBUTE_FIELDS = /* @__PURE__ */ new Set(["match", "conditions"]);
10714
+ var CONDITION_FIELDS = /* @__PURE__ */ new Set(["path", "op", "value"]);
10715
+ function isValueReference(value) {
10716
+ return isPlainObject(value) && ("fromInput" in value || "fromClaims" in value);
10717
+ }
10718
+ function checkValueReference(value, path) {
10719
+ const ref = value["fromInput"] ?? value["fromClaims"];
10720
+ return Object.keys(value).length === 1 && isNonEmptyString(ref) ? [] : [`${path} must be { fromInput: "<name>" } or { fromClaims: "<path>" }`];
10721
+ }
10722
+ var OPERATOR_VALUES = {
10723
+ in: { accepts: Array.isArray, needs: "a list" },
10724
+ notIn: { accepts: Array.isArray, needs: "a list" },
10725
+ gt: { accepts: (value) => typeof value === "number", needs: "a number" },
10726
+ gte: { accepts: (value) => typeof value === "number", needs: "a number" },
10727
+ lt: { accepts: (value) => typeof value === "number", needs: "a number" },
10728
+ lte: { accepts: (value) => typeof value === "number", needs: "a number" },
10729
+ startsWith: { accepts: (value) => typeof value === "string", needs: "a string" },
10730
+ endsWith: { accepts: (value) => typeof value === "string", needs: "a string" },
10731
+ matches: { accepts: (value) => typeof value === "string", needs: "a string" }
10732
+ };
10733
+ function checkConditionValue(op, condition, path) {
10734
+ const value = condition["value"];
10735
+ if (value === void 0) return [`${path} is missing`];
10736
+ if (op === "exists") {
10737
+ return typeof value === "boolean" ? [] : [`${path} must be true or false for "exists"`];
10738
+ }
10739
+ if (isValueReference(value)) return checkValueReference(value, path);
10740
+ const expected = OPERATOR_VALUES[op];
10741
+ if (expected && !expected.accepts(value)) return [`${path} must be ${expected.needs} for "${op}"`];
10742
+ if (Array.isArray(value) && value.length === 0 && (op === "in" || op === "notIn")) return [`${path} is empty`];
10743
+ return [];
10744
+ }
10745
+ function checkAttributes(value, path) {
10746
+ if (!isPlainObject(value)) return [`${path} must be an object with "match" or "conditions"`];
10747
+ const problems = unknownFields(value, ATTRIBUTE_FIELDS, path);
10748
+ const { match, conditions } = value;
10749
+ if (match === void 0 && conditions === void 0) {
10750
+ problems.push(`${path} needs "match" or "conditions"`);
10751
+ }
10752
+ if (match !== void 0) {
10753
+ if (!isPlainObject(match)) problems.push(`${path}.match must be an object`);
10754
+ else if (Object.keys(match).length === 0) problems.push(`${path}.match is empty`);
10755
+ else {
10756
+ for (const [attribute, expected] of Object.entries(match)) {
10757
+ const at = `${path}.match[${JSON.stringify(attribute)}]`;
10758
+ if (expected === void 0) problems.push(`${at} has no value`);
10759
+ else if (isValueReference(expected)) problems.push(...checkValueReference(expected, at));
10760
+ }
10761
+ }
10762
+ }
10763
+ if (conditions !== void 0) {
10764
+ if (!Array.isArray(conditions)) problems.push(`${path}.conditions must be a list`);
10765
+ else if (conditions.length === 0) problems.push(`${path}.conditions is empty`);
10766
+ else {
10767
+ conditions.forEach((condition, index) => {
10768
+ const at = `${path}.conditions[${index}]`;
10769
+ if (!isPlainObject(condition)) {
10770
+ problems.push(`${at} must be an object with "path" and "op"`);
10771
+ return;
10772
+ }
10773
+ problems.push(...unknownFields(condition, CONDITION_FIELDS, at));
10774
+ if (!isNonEmptyString(condition["path"])) problems.push(`${at}.path must be a non-empty string`);
10775
+ const op = condition["op"];
10776
+ if (!ABAC_OPERATORS.has(op)) {
10777
+ problems.push(`${at}.op "${String(condition["op"])}" is not an operator`);
10778
+ } else {
10779
+ problems.push(...checkConditionValue(op, condition, `${at}.value`));
10780
+ }
10781
+ });
10782
+ }
10783
+ }
10784
+ return problems;
10785
+ }
10786
+ var RELATIONSHIP_FIELDS = /* @__PURE__ */ new Set(["type", "resource", "resourceId"]);
10787
+ function checkResourceId(value, path) {
10788
+ if (isNonEmptyString(value)) return [];
10789
+ if (isPlainObject(value) && Object.keys(value).length === 1) {
10790
+ const ref = value["fromInput"] ?? value["fromClaims"];
10791
+ if (isNonEmptyString(ref)) return [];
10792
+ }
10793
+ return [`${path} must be a string, { fromInput } or { fromClaims }`];
10794
+ }
10795
+ function checkRelationship(value, path) {
10796
+ if (!isPlainObject(value)) return [`${path} must be an object with "type", "resource" and "resourceId"`];
10797
+ const problems = unknownFields(value, RELATIONSHIP_FIELDS, path);
10798
+ if (!isNonEmptyString(value["type"])) problems.push(`${path}.type must be a non-empty string`);
10799
+ if (!isNonEmptyString(value["resource"])) problems.push(`${path}.resource must be a non-empty string`);
10800
+ problems.push(...checkResourceId(value["resourceId"], `${path}.resourceId`));
10801
+ return problems;
10802
+ }
10803
+ function checkRelationships(value, path) {
10804
+ if (!Array.isArray(value)) return checkRelationship(value, path);
10805
+ if (value.length === 0) return [`${path} is empty`];
10806
+ return value.flatMap((relationship, index) => checkRelationship(relationship, `${path}[${index}]`));
10807
+ }
10808
+ function checkRuleList(value, path) {
10809
+ if (!Array.isArray(value)) return [`${path} must be a list of rules`];
10810
+ if (value.length === 0) return [`${path} is empty`];
10811
+ return value.flatMap((rule, index) => checkRule(rule, `${path}[${index}]`));
10812
+ }
10813
+ function checkRule(rule, path) {
10814
+ if (!isPlainObject(rule)) {
10815
+ return [`${path || "the rule"} must be a rule object${typeof rule === "string" ? ", not a profile name" : ""}`];
10816
+ }
10817
+ const problems = unknownFields(rule, RULE_FIELDS, path);
10818
+ if (!CHECK_FIELDS.some((field) => rule[field] !== void 0)) {
10819
+ problems.push(`${path ? `${path} ` : ""}checks nothing`);
10820
+ }
10821
+ if (rule["operator"] !== void 0 && rule["operator"] !== "AND" && rule["operator"] !== "OR") {
10822
+ problems.push(`${path}.operator must be "AND" or "OR"`);
10823
+ }
10824
+ if (rule["roles"] !== void 0) problems.push(...checkAllOrAny(rule["roles"], `${path}.roles`));
10825
+ if (rule["permissions"] !== void 0) problems.push(...checkAllOrAny(rule["permissions"], `${path}.permissions`));
10826
+ if (rule["attributes"] !== void 0) problems.push(...checkAttributes(rule["attributes"], `${path}.attributes`));
10827
+ if (rule["relationships"] !== void 0) {
10828
+ problems.push(...checkRelationships(rule["relationships"], `${path}.relationships`));
10829
+ }
10830
+ if (rule["custom"] !== void 0) {
10831
+ if (!isPlainObject(rule["custom"])) problems.push(`${path}.custom must be an object`);
10832
+ else if (Object.keys(rule["custom"]).length === 0) problems.push(`${path}.custom is empty`);
10833
+ }
10834
+ if (rule["guards"] !== void 0) {
10835
+ const guards = rule["guards"];
10836
+ if (!Array.isArray(guards)) problems.push(`${path}.guards must be a list of functions`);
10837
+ else if (guards.length === 0) problems.push(`${path}.guards is empty`);
10838
+ else if (!guards.every((guard) => typeof guard === "function")) {
10839
+ problems.push(`${path}.guards must contain only functions`);
10840
+ }
10841
+ }
10842
+ if (rule["allOf"] !== void 0) problems.push(...checkRuleList(rule["allOf"], `${path}.allOf`));
10843
+ if (rule["anyOf"] !== void 0) problems.push(...checkRuleList(rule["anyOf"], `${path}.anyOf`));
10844
+ if (rule["not"] !== void 0) problems.push(...checkRule(rule["not"], `${path}.not`));
10845
+ return problems;
10846
+ }
10847
+ function findAuthoritiesRuleProblems(authorities) {
10848
+ if (typeof authorities === "string") {
10849
+ return authorities.length > 0 ? [] : ["the profile name is empty"];
10850
+ }
10851
+ if (Array.isArray(authorities)) {
10852
+ if (authorities.length === 0) return ["the list of profiles is empty"];
10853
+ return authorities.every(isNonEmptyString) ? [] : ["a list of profiles must contain only non-empty profile names"];
10854
+ }
10855
+ if (!isPlainObject(authorities)) {
10856
+ return ["must be a profile name, a list of profile names, or a rule object"];
10857
+ }
10858
+ return checkRule(authorities, "");
10859
+ }
10860
+ function findAuthoritiesProfileProblems(name, policy) {
10861
+ return checkRule(policy, "").map((problem) => `profile "${name}": ${problem.replace(/^\./, "")}`);
10862
+ }
10863
+
10492
10864
  // libs/auth/src/authorities/authorities.engine.ts
10493
10865
  function mergeResult(base, ...others) {
10494
10866
  const evaluatedPolicies = [...base.evaluatedPolicies];
@@ -10497,8 +10869,12 @@ function mergeResult(base, ...others) {
10497
10869
  }
10498
10870
  return { ...base, evaluatedPolicies };
10499
10871
  }
10500
- function granted(policies) {
10501
- return { granted: true, evaluatedPolicies: policies };
10872
+ function invalidRule(problems, policies = []) {
10873
+ return {
10874
+ granted: false,
10875
+ deniedBy: `invalid authorities rule: ${problems.join("; ")}`,
10876
+ evaluatedPolicies: policies
10877
+ };
10502
10878
  }
10503
10879
  var AuthoritiesEngine = class {
10504
10880
  constructor(profiles, evaluators) {
@@ -10507,10 +10883,36 @@ var AuthoritiesEngine = class {
10507
10883
  }
10508
10884
  profiles;
10509
10885
  evaluators;
10886
+ /** Problems found per rule object, so a rule is validated once however often it is evaluated. */
10887
+ ruleProblems = /* @__PURE__ */ new WeakMap();
10888
+ /**
10889
+ * Problems with an `authorities` value declared on an entry (see {@link findAuthoritiesRuleProblems}),
10890
+ * and each profile name it uses that no registered profile has. The server reports them when it
10891
+ * starts; {@link evaluate} denies such a value (an unknown profile as `profile '…' is not registered`).
10892
+ */
10893
+ findRuleProblems(authorities) {
10894
+ const problems = findAuthoritiesRuleProblems(authorities);
10895
+ if (problems.length > 0) return problems;
10896
+ const names = typeof authorities === "string" ? [authorities] : Array.isArray(authorities) ? authorities : [];
10897
+ return names.filter((name) => typeof name === "string" && !this.profiles.has(name)).map((name) => `names an unknown profile "${name}"`);
10898
+ }
10899
+ /**
10900
+ * Problems with every registered profile's rule (see {@link findAuthoritiesRuleProblems}).
10901
+ * The server reports them when it starts.
10902
+ */
10903
+ findProfileProblems() {
10904
+ return Object.entries(this.profiles.getAll()).flatMap(
10905
+ ([name, policy]) => findAuthoritiesProfileProblems(name, policy)
10906
+ );
10907
+ }
10510
10908
  /**
10511
10909
  * Evaluate an AuthoritiesMetadata value (string, string[], or policy object).
10910
+ *
10911
+ * A value that is malformed or checks nothing (see {@link findAuthoritiesRuleProblems}) is denied.
10512
10912
  */
10513
10913
  async evaluate(authorities, ctx) {
10914
+ const problems = this.problemsOf(authorities, findAuthoritiesRuleProblems);
10915
+ if (problems.length > 0) return invalidRule(problems);
10514
10916
  if (typeof authorities === "string") {
10515
10917
  return this.evaluateProfile(authorities, ctx);
10516
10918
  }
@@ -10531,6 +10933,8 @@ var AuthoritiesEngine = class {
10531
10933
  evaluatedPolicies: [`profile:${name}`]
10532
10934
  };
10533
10935
  }
10936
+ const problems = this.problemsOf(policy, (rule) => findAuthoritiesProfileProblems(name, rule));
10937
+ if (problems.length > 0) return invalidRule(problems, [`profile:${name}`]);
10534
10938
  const result = await this.evaluatePolicy(policy, ctx);
10535
10939
  if (!result.granted) {
10536
10940
  return {
@@ -10541,6 +10945,16 @@ var AuthoritiesEngine = class {
10541
10945
  }
10542
10946
  return mergeResult(result, { granted: true, evaluatedPolicies: [`profile:${name}`] });
10543
10947
  }
10948
+ /** Validate a rule, reusing the result for a rule object seen before. */
10949
+ problemsOf(rule, find) {
10950
+ if (typeof rule !== "object" || rule === null) return find(rule);
10951
+ let problems = this.ruleProblems.get(rule);
10952
+ if (!problems) {
10953
+ problems = find(rule);
10954
+ this.ruleProblems.set(rule, problems);
10955
+ }
10956
+ return problems;
10957
+ }
10544
10958
  /**
10545
10959
  * Evaluate an array of profile names (AND semantics).
10546
10960
  */
@@ -10589,7 +11003,7 @@ var AuthoritiesEngine = class {
10589
11003
  results.push(await this.evaluateNot(policy.not, ctx));
10590
11004
  }
10591
11005
  if (results.length === 0) {
10592
- return granted([]);
11006
+ return invalidRule(["checks nothing"]);
10593
11007
  }
10594
11008
  return this.combineResults(results, operator);
10595
11009
  }
@@ -10640,14 +11054,15 @@ var AuthoritiesEngine = class {
10640
11054
  }
10641
11055
  /**
10642
11056
  * Evaluate async guard functions in sequence.
10643
- * Each guard returns true (granted) or a string (denial message).
11057
+ * Only `true` grants. A string is the denial message; anything else (`false`, but also
11058
+ * `undefined` from a guard that forgot to return, `null`, `0` or an object) denies.
10644
11059
  */
10645
11060
  async evaluateGuards(guards, ctx) {
10646
11061
  for (let i = 0; i < guards.length; i++) {
10647
11062
  const guard = guards[i];
10648
11063
  const result = await guard(ctx);
10649
- if (result === false || typeof result === "string") {
10650
- const denialMessage = typeof result === "string" ? result : `guard[${i}] denied`;
11064
+ if (result !== true) {
11065
+ const denialMessage = typeof result === "string" ? result : result === false ? `guard[${i}] denied` : `guard[${i}] did not return true (returned ${result === null ? "null" : typeof result})`;
10651
11066
  return {
10652
11067
  granted: false,
10653
11068
  deniedBy: `guards[${i}]: ${denialMessage}`,
@@ -10705,6 +11120,13 @@ var AuthoritiesEngine = class {
10705
11120
  evaluatedPolicies: ["not", ...result.evaluatedPolicies]
10706
11121
  };
10707
11122
  }
11123
+ /**
11124
+ * The policy a profile name stands for when `evaluate()` is given that name, or `undefined`
11125
+ * when no such profile is registered. Lets callers inspect a named rule without evaluating it.
11126
+ */
11127
+ resolveProfile(name) {
11128
+ return this.profiles.resolve(name);
11129
+ }
10708
11130
  };
10709
11131
 
10710
11132
  // libs/auth/src/authorities/authorities.registry.ts
@@ -10949,6 +11371,7 @@ export {
10949
11371
  AuthoritiesProfileRegistry,
10950
11372
  AuthorityDeniedError,
10951
11373
  AuthorizationBase,
11374
+ BUILT_IN_AUTH_PAGE_CSP,
10952
11375
  CDN,
10953
11376
  CREDENTIALS_ACCESSOR,
10954
11377
  CimdClientIdMismatchError,
@@ -10963,6 +11386,7 @@ export {
10963
11386
  CredentialProviderAlreadyRegisteredError,
10964
11387
  CredentialStorageError,
10965
11388
  CredentialsAccessorImpl,
11389
+ DEFAULT_ALLOWED_SCOPES,
10966
11390
  DEFAULT_INCREMENTAL_TICKET_TTL_MS,
10967
11391
  DEFAULT_RESUME_TTL_MS,
10968
11392
  DEFAULT_THEME,
@@ -11001,6 +11425,9 @@ export {
11001
11425
  RedisAuthorizationStore,
11002
11426
  RedisSessionStore,
11003
11427
  SECURE_STORE_ACCESSOR,
11428
+ SIGNIN_BINDING_COOKIE_PREFIX,
11429
+ SIGNIN_BINDING_MAX_AGE_SECONDS,
11430
+ SIGNIN_BINDING_SECURE_COOKIE_PREFIX,
11004
11431
  ScopeDeniedError,
11005
11432
  SecureStoreAccessorImpl,
11006
11433
  SessionCredentialVault,
@@ -11028,6 +11455,7 @@ export {
11028
11455
  abacConditionSchema,
11029
11456
  abacOperatorSchema,
11030
11457
  abacPolicySchema,
11458
+ advertisedScopes,
11031
11459
  aiPlatformTypeSchema,
11032
11460
  allowsPublicAccess,
11033
11461
  apiKeyCredentialSchema,
@@ -11076,6 +11504,7 @@ export {
11076
11504
  buildToolConsentPage,
11077
11505
  buildUnauthorizedHeader,
11078
11506
  buildWwwAuthenticate,
11507
+ builtInAuthPageHeaders,
11079
11508
  centeredCardLayout,
11080
11509
  checkSsrfProtection,
11081
11510
  cimdCacheConfigSchema,
@@ -11095,6 +11524,7 @@ export {
11095
11524
  createLayout,
11096
11525
  createSecureStore,
11097
11526
  createSessionCredentialVault,
11527
+ createSigninBinding,
11098
11528
  createTokenStorageAdapter,
11099
11529
  credentialProviderConfigSchema,
11100
11530
  credentialSchema,
@@ -11102,7 +11532,7 @@ export {
11102
11532
  credentialTypeSchema,
11103
11533
  customCredentialSchema,
11104
11534
  decodeJwtPayloadSafe,
11105
- decryptAesGcm5 as decryptAesGcm,
11535
+ decryptAesGcm6 as decryptAesGcm,
11106
11536
  decryptSessionJson,
11107
11537
  decryptValue2 as decryptValue,
11108
11538
  defaultSessionRateLimiter,
@@ -11112,7 +11542,7 @@ export {
11112
11542
  deriveTypedUser,
11113
11543
  detectAuthProviders,
11114
11544
  detectedAuthProviderSchema,
11115
- encryptAesGcm5 as encryptAesGcm,
11545
+ encryptAesGcm6 as encryptAesGcm,
11116
11546
  encryptJson,
11117
11547
  encryptValue2 as encryptValue,
11118
11548
  encryptedBlobSchema,
@@ -11127,6 +11557,8 @@ export {
11127
11557
  federatedLoginStateSchema,
11128
11558
  federatedProviderItemSchema,
11129
11559
  federatedSelectionSchema,
11560
+ findAuthoritiesProfileProblems,
11561
+ findAuthoritiesRuleProblems,
11130
11562
  flatRemoteProviderFields,
11131
11563
  fromSessionRecord,
11132
11564
  generateAuthorizationCode,
@@ -11138,15 +11570,17 @@ export {
11138
11570
  getProviderApps,
11139
11571
  getProviderScopes,
11140
11572
  getTokenSignatureFingerprint,
11573
+ grantScopes,
11141
11574
  hasOnlyLocalhostRedirectUris,
11142
11575
  headerApplier,
11143
- hkdfSha2564 as hkdfSha256,
11576
+ hkdfSha2565 as hkdfSha256,
11144
11577
  incrementalAuthConfigSchema,
11145
11578
  isAnonymousSubject,
11146
11579
  isCimdClientId,
11147
11580
  isFrontMcpCredentials,
11148
11581
  isJwt,
11149
11582
  isLocalMode,
11583
+ isLoopbackRedirectUri,
11150
11584
  isOrchestratedLocal,
11151
11585
  isOrchestratedMode,
11152
11586
  isOrchestratedRemote,
@@ -11176,6 +11610,7 @@ export {
11176
11610
  noopLogger,
11177
11611
  normalizeIssuer,
11178
11612
  oauthCredentialSchema,
11613
+ openPendingLogin,
11179
11614
  orchestratedLocalSchema,
11180
11615
  orchestratedRemoteSchema,
11181
11616
  parseAuthOptions,
@@ -11203,6 +11638,7 @@ export {
11203
11638
  resolveRequiredScopes,
11204
11639
  resourceIdRefSchema,
11205
11640
  safeDecrypt,
11641
+ sealPendingLogin,
11206
11642
  secureStoreConfigSchema,
11207
11643
  secureStoreCustomBackendSchema,
11208
11644
  secureStoreEncryptionSchema,
@@ -11213,6 +11649,8 @@ export {
11213
11649
  signCredentialResumeToken,
11214
11650
  signIncrementalAuthTicket,
11215
11651
  signSession,
11652
+ signinBindingCookieName,
11653
+ signinBindingMatches,
11216
11654
  skippedAppBehaviorSchema,
11217
11655
  sseTransportStateSchema,
11218
11656
  sshKeyCredentialSchema,
@@ -11245,5 +11683,6 @@ export {
11245
11683
  verifyOrParseSession,
11246
11684
  verifyPkce,
11247
11685
  verifySession,
11248
- wideLayout
11686
+ wideLayout,
11687
+ withoutSecretFieldValues
11249
11688
  };