@frontmcp/auth 1.8.1 → 1.8.3

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 (49) hide show
  1. package/authorities/authorities.engine.d.ts +24 -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 +497 -85
  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 +581 -160
  14. package/jwks/jwks.service.d.ts.map +1 -1
  15. package/options/app-auth.schema.d.ts +4 -0
  16. package/options/app-auth.schema.d.ts.map +1 -1
  17. package/options/interfaces.d.ts +48 -7
  18. package/options/interfaces.d.ts.map +1 -1
  19. package/options/orchestrated.schema.d.ts +8 -0
  20. package/options/orchestrated.schema.d.ts.map +1 -1
  21. package/options/schema.d.ts +4 -0
  22. package/options/schema.d.ts.map +1 -1
  23. package/options/shared.schemas.d.ts +2 -0
  24. package/options/shared.schemas.d.ts.map +1 -1
  25. package/package.json +5 -5
  26. package/session/authorization.store.d.ts +19 -3
  27. package/session/authorization.store.d.ts.map +1 -1
  28. package/session/federated-auth.session.d.ts +16 -0
  29. package/session/federated-auth.session.d.ts.map +1 -1
  30. package/session/index.d.ts +4 -0
  31. package/session/index.d.ts.map +1 -1
  32. package/session/pending-login.d.ts +23 -0
  33. package/session/pending-login.d.ts.map +1 -0
  34. package/session/signin-binding.d.ts +39 -0
  35. package/session/signin-binding.d.ts.map +1 -0
  36. package/ui/base-layout.d.ts +26 -0
  37. package/ui/base-layout.d.ts.map +1 -1
  38. package/ui/index.d.ts +2 -2
  39. package/ui/index.d.ts.map +1 -1
  40. package/ui/local-login.helper.d.ts +9 -2
  41. package/ui/local-login.helper.d.ts.map +1 -1
  42. package/ui/templates.d.ts +5 -4
  43. package/ui/templates.d.ts.map +1 -1
  44. package/utils/index.d.ts +2 -0
  45. package/utils/index.d.ts.map +1 -1
  46. package/utils/redirect-uri.utils.d.ts +16 -0
  47. package/utils/redirect-uri.utils.d.ts.map +1 -0
  48. package/utils/scope-grant.utils.d.ts +11 -0
  49. package/utils/scope-grant.utils.d.ts.map +1 -0
package/esm/index.mjs CHANGED
@@ -699,7 +699,9 @@ var JwksService = class {
699
699
  // Pin to asymmetric algorithms so a symmetric key in the provider JWKS
700
700
  // can't be used to accept an HMAC-signed token, and `none` is refused.
701
701
  algorithms: [...TRANSPARENT_JWT_ALGS],
702
- ...issuerConstraint ? { issuer: issuerConstraint } : {}
702
+ ...issuerConstraint ? { issuer: issuerConstraint } : {},
703
+ // A bearer token without `exp` would be valid forever (#272).
704
+ requiredClaims: ["exp"]
703
705
  });
704
706
  return {
705
707
  ok: true,
@@ -802,7 +804,10 @@ var JwksService = class {
802
804
  }
803
805
  }
804
806
  const nowSec = Math.floor(Date.now() / 1e3);
805
- if (payload.exp !== void 0 && (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp < nowSec)) {
807
+ if (payload.exp === void 0) {
808
+ return { ok: false, error: "missing_exp" };
809
+ }
810
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= nowSec) {
806
811
  return { ok: false, error: "token_expired" };
807
812
  }
808
813
  if (payload.nbf !== void 0 && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > nowSec)) {
@@ -1058,6 +1063,26 @@ var CDN = {
1058
1063
  stylesheet: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
1059
1064
  }
1060
1065
  };
1066
+ var BUILT_IN_AUTH_PAGE_CSP = [
1067
+ "default-src 'self'",
1068
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
1069
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
1070
+ "font-src 'self' https://fonts.gstatic.com",
1071
+ "img-src 'self' data: https:",
1072
+ "connect-src 'self'",
1073
+ "object-src 'none'",
1074
+ "base-uri 'none'",
1075
+ "frame-ancestors 'none'"
1076
+ ].join("; ");
1077
+ function builtInAuthPageHeaders(options = {}) {
1078
+ return {
1079
+ ...options.csp === false ? {} : { "Content-Security-Policy": BUILT_IN_AUTH_PAGE_CSP },
1080
+ "X-Frame-Options": "DENY",
1081
+ "X-Content-Type-Options": "nosniff",
1082
+ "Referrer-Policy": "same-origin",
1083
+ "Cache-Control": "no-store"
1084
+ };
1085
+ }
1061
1086
  var DEFAULT_THEME = {
1062
1087
  colors: {
1063
1088
  primary: "#3b82f6",
@@ -1295,7 +1320,7 @@ function buildIncrementalAuthPage(params) {
1295
1320
  </div>
1296
1321
  </div>
1297
1322
 
1298
- <form method="GET" action="${escapeHtml3(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
1323
+ <form method="POST" action="${escapeHtml3(callbackPath)}" class="flex gap-3 pt-4 border-t border-gray-100">
1299
1324
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(sessionHint)}">
1300
1325
  <button type="button" onclick="window.close()"
1301
1326
  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 +1366,7 @@ function buildFederatedLoginPage(params) {
1341
1366
  ${escapeHtml3(clientName)} uses multiple authentication providers. Select which ones you want to authorize.
1342
1367
  </p>
1343
1368
 
1344
- <form method="GET" action="${escapeHtml3(callbackPath)}" id="federated-form">
1369
+ <form method="POST" action="${escapeHtml3(callbackPath)}" id="federated-form">
1345
1370
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1346
1371
  <input type="hidden" name="federated" value="true">
1347
1372
 
@@ -1486,7 +1511,7 @@ function buildToolConsentPage(params) {
1486
1511
  ${errorBanner}
1487
1512
  ${userInfo}
1488
1513
 
1489
- <form method="GET" action="${escapeHtml3(callbackPath)}" id="consent-form">
1514
+ <form method="POST" action="${escapeHtml3(callbackPath)}" id="consent-form">
1490
1515
  <input type="hidden" name="csrf" value="${escapeHtml3(csrfToken)}">
1491
1516
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1492
1517
  <input type="hidden" name="consent_submitted" value="1">
@@ -1584,7 +1609,7 @@ function buildLoginPage(params) {
1584
1609
 
1585
1610
  ${scopesHtml}
1586
1611
 
1587
- <form method="GET" action="${escapeHtml3(callbackPath)}">
1612
+ <form method="POST" action="${escapeHtml3(callbackPath)}">
1588
1613
  <input type="hidden" name="pending_auth_id" value="${escapeHtml3(pendingAuthId)}">
1589
1614
  ${fieldsHtml}
1590
1615
 
@@ -1681,7 +1706,17 @@ function toLoginExtraFields(fields, values) {
1681
1706
  value: values?.[name]
1682
1707
  }));
1683
1708
  }
1684
- function renderLocalLoginPage(login, ctx, values) {
1709
+ function withoutSecretFieldValues(values, fields) {
1710
+ if (!values) return void 0;
1711
+ const kept = {};
1712
+ for (const [name, value] of Object.entries(values)) {
1713
+ if (fields?.[name]?.type === "password") continue;
1714
+ kept[name] = value;
1715
+ }
1716
+ return kept;
1717
+ }
1718
+ function renderLocalLoginPage(login, ctx, submitted) {
1719
+ const values = withoutSecretFieldValues(submitted, login?.fields);
1685
1720
  if (login?.render) {
1686
1721
  return login.render({ ...ctx, values });
1687
1722
  }
@@ -1788,7 +1823,8 @@ function buildPendingRecord(params) {
1788
1823
  priorAuthorizedAppIds: params.priorAuthorizedAppIds,
1789
1824
  federatedLogin: params.federatedLogin,
1790
1825
  consent: params.consent,
1791
- authUiCsrf: params.authUiCsrf
1826
+ authUiCsrf: params.authUiCsrf,
1827
+ signinBinding: params.signinBinding
1792
1828
  };
1793
1829
  }
1794
1830
  function buildRefreshTokenRecord(params) {
@@ -2018,6 +2054,63 @@ var RedisAuthorizationStore = class {
2018
2054
  }
2019
2055
  };
2020
2056
 
2057
+ // libs/auth/src/session/pending-login.ts
2058
+ import {
2059
+ base64urlDecode,
2060
+ base64urlEncode,
2061
+ decryptAesGcm,
2062
+ encryptAesGcm,
2063
+ hkdfSha256,
2064
+ randomBytes as randomBytes2
2065
+ } from "@frontmcp/utils";
2066
+ var SALT = new TextEncoder().encode("frontmcp-pending-login");
2067
+ function keyFor(secret, pendingAuthId) {
2068
+ return hkdfSha256(secret, SALT, new TextEncoder().encode(`pending-login:${pendingAuthId}`), 32);
2069
+ }
2070
+ function sealPendingLogin(secret, pendingAuthId, state) {
2071
+ const iv = randomBytes2(12);
2072
+ const { ciphertext, tag } = encryptAesGcm(
2073
+ keyFor(secret, pendingAuthId),
2074
+ new TextEncoder().encode(JSON.stringify(state)),
2075
+ iv
2076
+ );
2077
+ return [iv, tag, ciphertext].map((part) => base64urlEncode(part)).join(".");
2078
+ }
2079
+ function openPendingLogin(secret, pendingAuthId, sealed) {
2080
+ try {
2081
+ const [iv, tag, ciphertext] = sealed.split(".").map((part) => base64urlDecode(part));
2082
+ if (!iv || !tag || !ciphertext) return void 0;
2083
+ const plaintext = decryptAesGcm(keyFor(secret, pendingAuthId), ciphertext, iv, tag);
2084
+ const state = JSON.parse(new TextDecoder().decode(plaintext));
2085
+ return typeof state?.sub === "string" && state.sub.length > 0 ? state : void 0;
2086
+ } catch {
2087
+ return void 0;
2088
+ }
2089
+ }
2090
+
2091
+ // libs/auth/src/session/signin-binding.ts
2092
+ import { base64urlEncode as base64urlEncode2, getCookie, randomBytes as randomBytes3, sha256Hex, timingSafeEqual } from "@frontmcp/utils";
2093
+ var SIGNIN_BINDING_COOKIE_PREFIX = "frontmcp_signin_";
2094
+ var SIGNIN_BINDING_SECURE_COOKIE_PREFIX = `__Host-${SIGNIN_BINDING_COOKIE_PREFIX}`;
2095
+ var SIGNIN_BINDING_MAX_AGE_SECONDS = 30 * 60;
2096
+ function signinBindingCookieName(pendingAuthId, options = {}) {
2097
+ const prefix = options.secure ? SIGNIN_BINDING_SECURE_COOKIE_PREFIX : SIGNIN_BINDING_COOKIE_PREFIX;
2098
+ return `${prefix}${sha256Hex(`signin:${pendingAuthId}`).slice(0, 16)}`;
2099
+ }
2100
+ function createSigninBinding(pendingAuthId, options = {}) {
2101
+ const value = base64urlEncode2(randomBytes3(32));
2102
+ return { cookieName: signinBindingCookieName(pendingAuthId, options), value, hash: sha256Hex(value) };
2103
+ }
2104
+ function signinBindingMatches(cookieHeader, pendingAuthId, expectedHash, options = {}) {
2105
+ if (!expectedHash) return false;
2106
+ const value = getCookie(cookieHeader, signinBindingCookieName(pendingAuthId, options));
2107
+ if (!value) return false;
2108
+ const encoder = new TextEncoder();
2109
+ const actual = encoder.encode(sha256Hex(value));
2110
+ const expected = encoder.encode(expectedHash);
2111
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
2112
+ }
2113
+
2021
2114
  // libs/auth/src/session/storage-authorization.store.ts
2022
2115
  import { TypedStorage } from "@frontmcp/utils";
2023
2116
  var StorageAuthorizationStore = class {
@@ -2410,12 +2503,12 @@ import { getEnv, randomUUID as randomUUID2 } from "@frontmcp/utils";
2410
2503
  // libs/auth/src/session/vault-encryption.ts
2411
2504
  import { z as z3 } from "@frontmcp/lazy-zod";
2412
2505
  import {
2413
- base64urlDecode,
2414
- base64urlEncode,
2415
- decryptAesGcm,
2416
- encryptAesGcm,
2417
- hkdfSha256,
2418
- randomBytes as randomBytes2
2506
+ base64urlDecode as base64urlDecode2,
2507
+ base64urlEncode as base64urlEncode3,
2508
+ decryptAesGcm as decryptAesGcm2,
2509
+ encryptAesGcm as encryptAesGcm2,
2510
+ hkdfSha256 as hkdfSha2562,
2511
+ randomBytes as randomBytes4
2419
2512
  } from "@frontmcp/utils";
2420
2513
  var encryptedDataSchema = z3.object({
2421
2514
  /** Version for future algorithm changes */
@@ -2456,7 +2549,7 @@ var VaultEncryption = class {
2456
2549
  ];
2457
2550
  const ikm = new TextEncoder().encode(ikmParts.join(""));
2458
2551
  const infoBytes = new TextEncoder().encode(this.hkdfInfo);
2459
- const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
2552
+ const key = await hkdfSha2562(ikm, new Uint8Array(0), infoBytes, 32);
2460
2553
  return key;
2461
2554
  }
2462
2555
  /**
@@ -2483,7 +2576,7 @@ var VaultEncryption = class {
2483
2576
  ];
2484
2577
  const ikm = new TextEncoder().encode(ikmParts.join(""));
2485
2578
  const infoBytes = new TextEncoder().encode(this.hkdfInfo);
2486
- const key = await hkdfSha256(ikm, new Uint8Array(0), infoBytes, 32);
2579
+ const key = await hkdfSha2562(ikm, new Uint8Array(0), infoBytes, 32);
2487
2580
  return key;
2488
2581
  }
2489
2582
  /**
@@ -2497,14 +2590,14 @@ var VaultEncryption = class {
2497
2590
  if (key.length !== 32) {
2498
2591
  throw new Error("Encryption key must be 32 bytes");
2499
2592
  }
2500
- const iv = randomBytes2(12);
2501
- const { ciphertext, tag } = await encryptAesGcm(key, new TextEncoder().encode(plaintext), iv);
2593
+ const iv = randomBytes4(12);
2594
+ const { ciphertext, tag } = await encryptAesGcm2(key, new TextEncoder().encode(plaintext), iv);
2502
2595
  return {
2503
2596
  v: 1,
2504
2597
  alg: "aes-256-gcm",
2505
- iv: base64urlEncode(iv),
2506
- ct: base64urlEncode(ciphertext),
2507
- tag: base64urlEncode(tag)
2598
+ iv: base64urlEncode3(iv),
2599
+ ct: base64urlEncode3(ciphertext),
2600
+ tag: base64urlEncode3(tag)
2508
2601
  };
2509
2602
  }
2510
2603
  /**
@@ -2524,11 +2617,11 @@ var VaultEncryption = class {
2524
2617
  throw new Error("Invalid encrypted data format");
2525
2618
  }
2526
2619
  const { iv, ct, tag } = parsed.data;
2527
- const ivBuffer = base64urlDecode(iv);
2528
- const ciphertext = base64urlDecode(ct);
2529
- const tagBuffer = base64urlDecode(tag);
2620
+ const ivBuffer = base64urlDecode2(iv);
2621
+ const ciphertext = base64urlDecode2(ct);
2622
+ const tagBuffer = base64urlDecode2(tag);
2530
2623
  try {
2531
- const plaintext = await decryptAesGcm(key, ciphertext, ivBuffer, tagBuffer);
2624
+ const plaintext = await decryptAesGcm2(key, ciphertext, ivBuffer, tagBuffer);
2532
2625
  return new TextDecoder().decode(plaintext);
2533
2626
  } catch (_error) {
2534
2627
  throw new Error("Decryption failed: invalid key or corrupted data");
@@ -3124,7 +3217,7 @@ function errMsg2(err) {
3124
3217
  var SECURE_STORE_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:SECURE_STORE_ACCESSOR");
3125
3218
 
3126
3219
  // libs/auth/src/session/secure-store-accessor.impl.ts
3127
- import { sha256Hex } from "@frontmcp/utils";
3220
+ import { sha256Hex as sha256Hex2 } from "@frontmcp/utils";
3128
3221
  var GLOBAL_NAMESPACE = "global";
3129
3222
  var SecureStoreAccessorImpl = class {
3130
3223
  backend;
@@ -3153,12 +3246,12 @@ var SecureStoreAccessorImpl = class {
3153
3246
  return GLOBAL_NAMESPACE;
3154
3247
  case "session": {
3155
3248
  const sid = this.resolveSessionId();
3156
- return sid ? `s:${sha256Hex(sid).slice(0, 32)}` : void 0;
3249
+ return sid ? `s:${sha256Hex2(sid).slice(0, 32)}` : void 0;
3157
3250
  }
3158
3251
  case "user":
3159
3252
  default: {
3160
3253
  const sub = this.resolveSub();
3161
- return sub ? `u:${sha256Hex(sub).slice(0, 32)}` : void 0;
3254
+ return sub ? `u:${sha256Hex2(sub).slice(0, 32)}` : void 0;
3162
3255
  }
3163
3256
  }
3164
3257
  }
@@ -3262,7 +3355,7 @@ async function createSecureStore(options) {
3262
3355
  }
3263
3356
 
3264
3357
  // 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";
3358
+ import { encryptAesGcm as encryptAesGcm3, decryptAesGcm as decryptAesGcm3, randomBytes as randomBytes5, base64urlEncode as base64urlEncode4, base64urlDecode as base64urlDecode3 } from "@frontmcp/utils";
3266
3359
  var TokenVault = class {
3267
3360
  /** Active key used for new encryptions */
3268
3361
  active;
@@ -3291,14 +3384,14 @@ var TokenVault = class {
3291
3384
  this.keys.set(k.kid, k.key);
3292
3385
  }
3293
3386
  async encrypt(plaintext, opts) {
3294
- const iv = randomBytes3(12);
3295
- const { ciphertext, tag } = await encryptAesGcm2(this.active.key, new TextEncoder().encode(plaintext), iv);
3387
+ const iv = randomBytes5(12);
3388
+ const { ciphertext, tag } = await encryptAesGcm3(this.active.key, new TextEncoder().encode(plaintext), iv);
3296
3389
  return {
3297
3390
  alg: "A256GCM",
3298
3391
  kid: this.active.kid,
3299
- iv: base64urlEncode2(iv),
3300
- tag: base64urlEncode2(tag),
3301
- data: base64urlEncode2(ciphertext),
3392
+ iv: base64urlEncode4(iv),
3393
+ tag: base64urlEncode4(tag),
3394
+ data: base64urlEncode4(ciphertext),
3302
3395
  exp: opts?.exp,
3303
3396
  meta: opts?.meta
3304
3397
  };
@@ -3312,21 +3405,21 @@ var TokenVault = class {
3312
3405
  }
3313
3406
  const key = this.keys.get(blob.kid);
3314
3407
  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);
3408
+ const iv = base64urlDecode3(blob.iv);
3409
+ const tag = base64urlDecode3(blob.tag);
3410
+ const data = base64urlDecode3(blob.data);
3411
+ const plaintext = await decryptAesGcm3(key, data, iv, tag);
3319
3412
  return new TextDecoder().decode(plaintext);
3320
3413
  }
3321
3414
  };
3322
3415
 
3323
3416
  // libs/auth/src/session/index.ts
3324
3417
  import {
3325
- hkdfSha256 as hkdfSha2564,
3418
+ hkdfSha256 as hkdfSha2565,
3326
3419
  encryptValue as encryptValue2,
3327
3420
  decryptValue as decryptValue2,
3328
- encryptAesGcm as encryptAesGcm5,
3329
- decryptAesGcm as decryptAesGcm5
3421
+ encryptAesGcm as encryptAesGcm6,
3422
+ decryptAesGcm as decryptAesGcm6
3330
3423
  } from "@frontmcp/utils";
3331
3424
 
3332
3425
  // libs/auth/src/session/transport-session.types.ts
@@ -3433,7 +3526,7 @@ import {
3433
3526
  } from "@frontmcp/utils";
3434
3527
 
3435
3528
  // libs/auth/src/errors/auth-internal.error.ts
3436
- import { randomBytes as randomBytes4, bytesToHex as bytesToHex2 } from "@frontmcp/utils";
3529
+ import { randomBytes as randomBytes6, bytesToHex as bytesToHex2 } from "@frontmcp/utils";
3437
3530
  var AuthInternalError = class extends Error {
3438
3531
  /**
3439
3532
  * Unique error ID for tracking in logs.
@@ -3455,7 +3548,7 @@ var AuthInternalError = class extends Error {
3455
3548
  super(message);
3456
3549
  this.name = this.constructor.name;
3457
3550
  this.code = code;
3458
- this.errorId = `err_${bytesToHex2(randomBytes4(8))}`;
3551
+ this.errorId = `err_${bytesToHex2(randomBytes6(8))}`;
3459
3552
  Error.captureStackTrace(this, this.constructor);
3460
3553
  }
3461
3554
  /**
@@ -4761,7 +4854,7 @@ var VercelKvSessionStore = class {
4761
4854
  };
4762
4855
 
4763
4856
  // 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";
4857
+ import { encryptAesGcm as encryptAesGcm4, decryptAesGcm as decryptAesGcm4, randomBytes as randomBytes7, hkdfSha256 as hkdfSha2563 } from "@frontmcp/utils";
4765
4858
  var InMemoryOrchestratedTokenStore = class {
4766
4859
  /** Token storage: Map<compositeKey, ProviderTokenRecord> */
4767
4860
  tokens = /* @__PURE__ */ new Map();
@@ -4803,7 +4896,7 @@ var InMemoryOrchestratedTokenStore = class {
4803
4896
  }
4804
4897
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
4805
4898
  const salt = new TextEncoder().encode("frontmcp-token-store");
4806
- const derivedKey = hkdfSha2562(this.encryptionKey, salt, info, 32);
4899
+ const derivedKey = hkdfSha2563(this.encryptionKey, salt, info, 32);
4807
4900
  this.derivedKeys.set(compositeKey, derivedKey);
4808
4901
  return derivedKey;
4809
4902
  }
@@ -4813,8 +4906,8 @@ var InMemoryOrchestratedTokenStore = class {
4813
4906
  async encryptRecord(compositeKey, record) {
4814
4907
  const key = await this.deriveKeyForRecord(compositeKey);
4815
4908
  const plaintext = JSON.stringify(record);
4816
- const iv = randomBytes5(12);
4817
- const { ciphertext, tag } = encryptAesGcm3(key, new TextEncoder().encode(plaintext), iv);
4909
+ const iv = randomBytes7(12);
4910
+ const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
4818
4911
  return JSON.stringify({
4819
4912
  iv: Buffer.from(iv).toString("base64url"),
4820
4913
  tag: Buffer.from(tag).toString("base64url"),
@@ -4830,7 +4923,7 @@ var InMemoryOrchestratedTokenStore = class {
4830
4923
  const ivBytes = Buffer.from(iv, "base64url");
4831
4924
  const tagBytes = Buffer.from(tag, "base64url");
4832
4925
  const ciphertextBytes = Buffer.from(data, "base64url");
4833
- const plaintext = decryptAesGcm3(key, ciphertextBytes, ivBytes, tagBytes);
4926
+ const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
4834
4927
  return JSON.parse(new TextDecoder().decode(plaintext));
4835
4928
  }
4836
4929
  /**
@@ -5032,7 +5125,7 @@ var InMemoryOrchestratedTokenStore = class {
5032
5125
  };
5033
5126
 
5034
5127
  // 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";
5128
+ import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
5036
5129
  var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
5037
5130
  function deriveKeyForRecord(masterKey, compositeKey, cache) {
5038
5131
  const cached = cache.get(compositeKey);
@@ -5040,14 +5133,14 @@ function deriveKeyForRecord(masterKey, compositeKey, cache) {
5040
5133
  return cached;
5041
5134
  }
5042
5135
  const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
5043
- const derived = hkdfSha2563(masterKey, HKDF_SALT, info, 32);
5136
+ const derived = hkdfSha2564(masterKey, HKDF_SALT, info, 32);
5044
5137
  cache.set(compositeKey, derived);
5045
5138
  return derived;
5046
5139
  }
5047
5140
  function encryptRecord(key, record) {
5048
5141
  const plaintext = JSON.stringify(record);
5049
- const iv = randomBytes6(12);
5050
- const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
5142
+ const iv = randomBytes8(12);
5143
+ const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
5051
5144
  return JSON.stringify({
5052
5145
  iv: Buffer.from(iv).toString("base64url"),
5053
5146
  tag: Buffer.from(tag).toString("base64url"),
@@ -5059,7 +5152,7 @@ function decryptRecord(key, encrypted) {
5059
5152
  const ivBytes = Buffer.from(iv, "base64url");
5060
5153
  const tagBytes = Buffer.from(tag, "base64url");
5061
5154
  const ciphertextBytes = Buffer.from(data, "base64url");
5062
- const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
5155
+ const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
5063
5156
  return JSON.parse(new TextDecoder().decode(plaintext));
5064
5157
  }
5065
5158
 
@@ -5325,6 +5418,7 @@ var InMemoryFederatedAuthSessionStore = class {
5325
5418
  providerQueue: [...params.providerIds],
5326
5419
  completedProviders: /* @__PURE__ */ new Map(),
5327
5420
  skippedProviders: [],
5421
+ signinBinding: params.signinBinding,
5328
5422
  createdAt: now,
5329
5423
  expiresAt: now + this.sessionTtlMs
5330
5424
  };
@@ -5357,6 +5451,7 @@ function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
5357
5451
  providerQueue: [...params.providerIds],
5358
5452
  completedProviders: /* @__PURE__ */ new Map(),
5359
5453
  skippedProviders: [],
5454
+ signinBinding: params.signinBinding,
5360
5455
  createdAt: now,
5361
5456
  expiresAt: now + ttlMs
5362
5457
  };
@@ -5911,7 +6006,7 @@ function createEncryptedVault(redis, config = {}) {
5911
6006
  }
5912
6007
 
5913
6008
  // libs/auth/src/session/token.refresh.ts
5914
- import { base64urlDecode as base64urlDecode3 } from "@frontmcp/utils";
6009
+ import { base64urlDecode as base64urlDecode4 } from "@frontmcp/utils";
5915
6010
  function toEpochSeconds(exp) {
5916
6011
  if (exp == null) return void 0;
5917
6012
  if (exp instanceof Date) return Math.floor(exp.getTime() / 1e3);
@@ -5933,7 +6028,7 @@ function tryJwtExp(token) {
5933
6028
  const parts = token.split(".");
5934
6029
  if (parts.length < 2) return void 0;
5935
6030
  try {
5936
- const json = JSON.parse(new TextDecoder().decode(base64urlDecode3(parts[1])));
6031
+ const json = JSON.parse(new TextDecoder().decode(base64urlDecode4(parts[1])));
5937
6032
  const e = json?.exp;
5938
6033
  return typeof e === "number" ? toEpochSeconds(e) : void 0;
5939
6034
  } catch {
@@ -5996,7 +6091,7 @@ var progressiveAuthStateSchema = z6.object({
5996
6091
  });
5997
6092
 
5998
6093
  // libs/auth/src/authorization/authorization.class.ts
5999
- import { base64urlDecode as base64urlDecode4, getMachineId as getMachineId2, randomUUID as randomUUID11 } from "@frontmcp/utils";
6094
+ import { base64urlDecode as base64urlDecode5, getMachineId as getMachineId2, randomUUID as randomUUID11 } from "@frontmcp/utils";
6000
6095
  var AuthorizationBase = class _AuthorizationBase {
6001
6096
  id;
6002
6097
  isAnonymous;
@@ -6192,7 +6287,7 @@ var AuthorizationBase = class _AuthorizationBase {
6192
6287
  */
6193
6288
  static isJwtHeader(segment) {
6194
6289
  try {
6195
- const bytes = base64urlDecode4(segment);
6290
+ const bytes = base64urlDecode5(segment);
6196
6291
  const text = new TextDecoder().decode(bytes);
6197
6292
  const parsed = JSON.parse(text);
6198
6293
  return typeof parsed === "object" && parsed !== null && typeof parsed.alg === "string";
@@ -6364,7 +6459,7 @@ var PublicAuthorization = class _PublicAuthorization extends AuthorizationBase {
6364
6459
  };
6365
6460
 
6366
6461
  // libs/auth/src/authorization/transparent.authorization.ts
6367
- import { sha256Hex as sha256Hex2 } from "@frontmcp/utils";
6462
+ import { sha256Hex as sha256Hex3 } from "@frontmcp/utils";
6368
6463
  var TransparentAuthorization = class _TransparentAuthorization extends AuthorizationBase {
6369
6464
  mode = "transparent";
6370
6465
  /**
@@ -6464,7 +6559,7 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
6464
6559
  static generateAuthorizationId(token) {
6465
6560
  const parts = token.split(".");
6466
6561
  const signature = parts[2] || token;
6467
- return sha256Hex2(signature).substring(0, 16);
6562
+ return sha256Hex3(signature).substring(0, 16);
6468
6563
  }
6469
6564
  /**
6470
6565
  * Get the issuer from the token claims
@@ -6490,11 +6585,11 @@ var TransparentAuthorization = class _TransparentAuthorization extends Authoriza
6490
6585
  };
6491
6586
 
6492
6587
  // libs/auth/src/utils/authorization-id.utils.ts
6493
- import { sha256Hex as sha256Hex3 } from "@frontmcp/utils";
6588
+ import { sha256Hex as sha256Hex4 } from "@frontmcp/utils";
6494
6589
  function deriveAuthorizationId(token) {
6495
6590
  const parts = token.split(".");
6496
6591
  const signature = parts[2] || token;
6497
- return sha256Hex3(signature).substring(0, 16);
6592
+ return sha256Hex4(signature).substring(0, 16);
6498
6593
  }
6499
6594
 
6500
6595
  // libs/auth/src/authorization/orchestrated.authorization.ts
@@ -6863,7 +6958,7 @@ var OrchestratedAuthAccessorAdapter = class {
6863
6958
  };
6864
6959
 
6865
6960
  // libs/auth/src/authorization/dcr-client.registry.ts
6866
- import { sha256 as sha2562, timingSafeEqual } from "@frontmcp/utils";
6961
+ import { sha256 as sha2562, timingSafeEqual as timingSafeEqual2 } from "@frontmcp/utils";
6867
6962
  var DEFAULT_MAX_DYNAMIC_CLIENTS = 1e3;
6868
6963
  function globMatch(pattern, input) {
6869
6964
  let p = 0;
@@ -6989,7 +7084,7 @@ var DcrClientRegistry = class {
6989
7084
  if (!isConfidential) return "ok";
6990
7085
  if (!client.client_secret) return "invalid";
6991
7086
  if (!presentedSecret) return "invalid";
6992
- return timingSafeEqual(sha2562(presentedSecret), sha2562(client.client_secret)) ? "ok" : "invalid";
7087
+ return timingSafeEqual2(sha2562(presentedSecret), sha2562(client.client_secret)) ? "ok" : "invalid";
6993
7088
  }
6994
7089
  /** Whether a client id is known to the registry. */
6995
7090
  has(clientId) {
@@ -7077,7 +7172,7 @@ var DcrClientRegistry = class {
7077
7172
  if (typeof presented !== "string" || presented.length === 0) {
7078
7173
  return false;
7079
7174
  }
7080
- return timingSafeEqual(sha2562(presented), sha2562(expected));
7175
+ return timingSafeEqual2(sha2562(presented), sha2562(expected));
7081
7176
  }
7082
7177
  };
7083
7178
 
@@ -7894,7 +7989,16 @@ var upstreamProviderSchema = z10.object({
7894
7989
  /** User info endpoint (optional, used to enrich the session identity). */
7895
7990
  userInfoEndpoint: z10.string().url().optional(),
7896
7991
  /** JWKS URI for upstream id_token validation (optional). */
7897
- jwksUri: z10.string().url().optional()
7992
+ jwksUri: z10.string().url().optional(),
7993
+ /**
7994
+ * The provider's issuer identifier (its OIDC `iss`). When set, an RFC 9207
7995
+ * `iss` on the provider's callback and the `iss` of its `id_token` must name
7996
+ * it (or one of {@link additionalIssuers}). Without it the provider's
7997
+ * `id_token` is not used for identity; `userInfoEndpoint` is asked instead.
7998
+ */
7999
+ issuer: z10.string().url().optional(),
8000
+ /** Other issuer values this provider legitimately uses. */
8001
+ additionalIssuers: z10.array(z10.string().trim().min(1, "additionalIssuers entries must be non-empty")).optional()
7898
8002
  }).refine((p) => !!(p.authorizationEndpoint ?? p.authorizeUrl), {
7899
8003
  message: "authorizationEndpoint (or authorizeUrl) is required",
7900
8004
  path: ["authorizationEndpoint"]
@@ -8303,17 +8407,21 @@ var sharedAuthFields = {
8303
8407
  * Require the OAuth client to be KNOWN (registered via DCR / pre-registered,
8304
8408
  * or a CIMD client-id URL) before an authorization request is accepted.
8305
8409
  *
8306
- * @default false (backward compatible — unknown client ids are accepted).
8410
+ * @default true
8307
8411
  *
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),
8412
+ * SECURITY: an unregistered client id has no trusted `redirect_uris` to
8413
+ * validate against, so accepting it lets anyone start a sign-in whose
8414
+ * authorization code is delivered to a redirect_uri of their choosing
8415
+ * (auth-code interception). `false` is for local development only; a
8416
+ * `dcr.allowedRedirectUris` allowlist still constrains such clients.
8417
+ */
8418
+ requireRegisteredClients: z14.boolean().default(true),
8419
+ /**
8420
+ * Scopes the server may grant (exact values or `*` globs). Requested scopes
8421
+ * outside this list are dropped from the grant. Unset → the standard OpenID
8422
+ * scopes only (see `DEFAULT_ALLOWED_SCOPES`).
8423
+ */
8424
+ allowedScopes: z14.array(z14.string().trim().min(1, "allowedScopes entries must be non-empty")).optional(),
8317
8425
  cimd: cimdConfigSchema.optional(),
8318
8426
  // #469 — custom auth UI as a slot→file map + extras name→handler map (per-app
8319
8427
  // under splitByApp). Both optional; omitting them serves the built-in pages.
@@ -8924,6 +9032,43 @@ var AudienceValidator = class _AudienceValidator {
8924
9032
  }
8925
9033
  };
8926
9034
 
9035
+ // libs/auth/src/utils/scope-grant.utils.ts
9036
+ var DEFAULT_ALLOWED_SCOPES = Object.freeze([
9037
+ "openid",
9038
+ "profile",
9039
+ "email",
9040
+ "offline_access"
9041
+ ]);
9042
+ function grantScopes(requested, allowed) {
9043
+ const patterns = allowed ?? DEFAULT_ALLOWED_SCOPES;
9044
+ const granted = [];
9045
+ for (const scope of requested) {
9046
+ if (!scope || granted.includes(scope)) continue;
9047
+ if (patterns.some((pattern) => globMatch(pattern, scope))) granted.push(scope);
9048
+ }
9049
+ return granted;
9050
+ }
9051
+
9052
+ // libs/auth/src/utils/redirect-uri.utils.ts
9053
+ function isLoopbackRedirectUri(uri) {
9054
+ let url;
9055
+ try {
9056
+ url = new URL(uri);
9057
+ } catch {
9058
+ return false;
9059
+ }
9060
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
9061
+ if (url.username || url.password) return false;
9062
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
9063
+ if (host === "localhost" || host === "::1") return true;
9064
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
9065
+ if (m) {
9066
+ const octets = m.slice(1).map((n) => Number(n));
9067
+ return octets.every((o) => o >= 0 && o <= 255) && octets[0] === 127;
9068
+ }
9069
+ return false;
9070
+ }
9071
+
8927
9072
  // libs/auth/src/vault/auth-providers.types.ts
8928
9073
  import { z as z19 } from "@frontmcp/lazy-zod";
8929
9074
  var credentialScopeSchema = z19.enum(["global", "user", "session"]);
@@ -10489,6 +10634,211 @@ async function evaluateRebac(policy, ctx) {
10489
10634
  return { granted: true, evaluatedPolicies: ["relationships"] };
10490
10635
  }
10491
10636
 
10637
+ // libs/auth/src/authorities/authorities.validation.ts
10638
+ var ABAC_OPERATORS = /* @__PURE__ */ new Set([
10639
+ "eq",
10640
+ "neq",
10641
+ "in",
10642
+ "notIn",
10643
+ "gt",
10644
+ "gte",
10645
+ "lt",
10646
+ "lte",
10647
+ "contains",
10648
+ "startsWith",
10649
+ "endsWith",
10650
+ "exists",
10651
+ "matches"
10652
+ ]);
10653
+ var CHECK_FIELDS = [
10654
+ "roles",
10655
+ "permissions",
10656
+ "attributes",
10657
+ "relationships",
10658
+ "custom",
10659
+ "guards",
10660
+ "allOf",
10661
+ "anyOf",
10662
+ "not"
10663
+ ];
10664
+ var RULE_FIELDS = /* @__PURE__ */ new Set([...CHECK_FIELDS, "operator"]);
10665
+ function isPlainObject(value) {
10666
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10667
+ }
10668
+ function isNonEmptyString(value) {
10669
+ return typeof value === "string" && value.length > 0;
10670
+ }
10671
+ function unknownFields(value, known, path) {
10672
+ const prefix = path ? `${path} has` : "has";
10673
+ return Object.keys(value).filter((key) => !known.has(key)).map((key) => `${prefix} an unknown field "${key}"`);
10674
+ }
10675
+ function checkStringList(value, path) {
10676
+ if (!Array.isArray(value)) return [`${path} must be a list of names`];
10677
+ if (value.length === 0) return [`${path} is empty`];
10678
+ return value.every(isNonEmptyString) ? [] : [`${path} must contain only non-empty names`];
10679
+ }
10680
+ var ALL_OR_ANY = /* @__PURE__ */ new Set(["all", "any"]);
10681
+ function checkAllOrAny(value, path) {
10682
+ if (!isPlainObject(value)) return [`${path} must be an object with "all" or "any"`];
10683
+ const problems = unknownFields(value, ALL_OR_ANY, path);
10684
+ if (value["all"] === void 0 && value["any"] === void 0) {
10685
+ problems.push(`${path} needs "all" or "any"`);
10686
+ }
10687
+ if (value["all"] !== void 0) problems.push(...checkStringList(value["all"], `${path}.all`));
10688
+ if (value["any"] !== void 0) problems.push(...checkStringList(value["any"], `${path}.any`));
10689
+ return problems;
10690
+ }
10691
+ var ATTRIBUTE_FIELDS = /* @__PURE__ */ new Set(["match", "conditions"]);
10692
+ var CONDITION_FIELDS = /* @__PURE__ */ new Set(["path", "op", "value"]);
10693
+ function isValueReference(value) {
10694
+ return isPlainObject(value) && ("fromInput" in value || "fromClaims" in value);
10695
+ }
10696
+ function checkValueReference(value, path) {
10697
+ const ref = value["fromInput"] ?? value["fromClaims"];
10698
+ return Object.keys(value).length === 1 && isNonEmptyString(ref) ? [] : [`${path} must be { fromInput: "<name>" } or { fromClaims: "<path>" }`];
10699
+ }
10700
+ var OPERATOR_VALUES = {
10701
+ in: { accepts: Array.isArray, needs: "a list" },
10702
+ notIn: { accepts: Array.isArray, needs: "a list" },
10703
+ gt: { accepts: (value) => typeof value === "number", needs: "a number" },
10704
+ gte: { accepts: (value) => typeof value === "number", needs: "a number" },
10705
+ lt: { accepts: (value) => typeof value === "number", needs: "a number" },
10706
+ lte: { accepts: (value) => typeof value === "number", needs: "a number" },
10707
+ startsWith: { accepts: (value) => typeof value === "string", needs: "a string" },
10708
+ endsWith: { accepts: (value) => typeof value === "string", needs: "a string" },
10709
+ matches: { accepts: (value) => typeof value === "string", needs: "a string" }
10710
+ };
10711
+ function checkConditionValue(op, condition, path) {
10712
+ const value = condition["value"];
10713
+ if (value === void 0) return [`${path} is missing`];
10714
+ if (op === "exists") {
10715
+ return typeof value === "boolean" ? [] : [`${path} must be true or false for "exists"`];
10716
+ }
10717
+ if (isValueReference(value)) return checkValueReference(value, path);
10718
+ const expected = OPERATOR_VALUES[op];
10719
+ if (expected && !expected.accepts(value)) return [`${path} must be ${expected.needs} for "${op}"`];
10720
+ if (Array.isArray(value) && value.length === 0 && (op === "in" || op === "notIn")) return [`${path} is empty`];
10721
+ return [];
10722
+ }
10723
+ function checkAttributes(value, path) {
10724
+ if (!isPlainObject(value)) return [`${path} must be an object with "match" or "conditions"`];
10725
+ const problems = unknownFields(value, ATTRIBUTE_FIELDS, path);
10726
+ const { match, conditions } = value;
10727
+ if (match === void 0 && conditions === void 0) {
10728
+ problems.push(`${path} needs "match" or "conditions"`);
10729
+ }
10730
+ if (match !== void 0) {
10731
+ if (!isPlainObject(match)) problems.push(`${path}.match must be an object`);
10732
+ else if (Object.keys(match).length === 0) problems.push(`${path}.match is empty`);
10733
+ else {
10734
+ for (const [attribute, expected] of Object.entries(match)) {
10735
+ const at = `${path}.match[${JSON.stringify(attribute)}]`;
10736
+ if (expected === void 0) problems.push(`${at} has no value`);
10737
+ else if (isValueReference(expected)) problems.push(...checkValueReference(expected, at));
10738
+ }
10739
+ }
10740
+ }
10741
+ if (conditions !== void 0) {
10742
+ if (!Array.isArray(conditions)) problems.push(`${path}.conditions must be a list`);
10743
+ else if (conditions.length === 0) problems.push(`${path}.conditions is empty`);
10744
+ else {
10745
+ conditions.forEach((condition, index) => {
10746
+ const at = `${path}.conditions[${index}]`;
10747
+ if (!isPlainObject(condition)) {
10748
+ problems.push(`${at} must be an object with "path" and "op"`);
10749
+ return;
10750
+ }
10751
+ problems.push(...unknownFields(condition, CONDITION_FIELDS, at));
10752
+ if (!isNonEmptyString(condition["path"])) problems.push(`${at}.path must be a non-empty string`);
10753
+ const op = condition["op"];
10754
+ if (!ABAC_OPERATORS.has(op)) {
10755
+ problems.push(`${at}.op "${String(condition["op"])}" is not an operator`);
10756
+ } else {
10757
+ problems.push(...checkConditionValue(op, condition, `${at}.value`));
10758
+ }
10759
+ });
10760
+ }
10761
+ }
10762
+ return problems;
10763
+ }
10764
+ var RELATIONSHIP_FIELDS = /* @__PURE__ */ new Set(["type", "resource", "resourceId"]);
10765
+ function checkResourceId(value, path) {
10766
+ if (isNonEmptyString(value)) return [];
10767
+ if (isPlainObject(value) && Object.keys(value).length === 1) {
10768
+ const ref = value["fromInput"] ?? value["fromClaims"];
10769
+ if (isNonEmptyString(ref)) return [];
10770
+ }
10771
+ return [`${path} must be a string, { fromInput } or { fromClaims }`];
10772
+ }
10773
+ function checkRelationship(value, path) {
10774
+ if (!isPlainObject(value)) return [`${path} must be an object with "type", "resource" and "resourceId"`];
10775
+ const problems = unknownFields(value, RELATIONSHIP_FIELDS, path);
10776
+ if (!isNonEmptyString(value["type"])) problems.push(`${path}.type must be a non-empty string`);
10777
+ if (!isNonEmptyString(value["resource"])) problems.push(`${path}.resource must be a non-empty string`);
10778
+ problems.push(...checkResourceId(value["resourceId"], `${path}.resourceId`));
10779
+ return problems;
10780
+ }
10781
+ function checkRelationships(value, path) {
10782
+ if (!Array.isArray(value)) return checkRelationship(value, path);
10783
+ if (value.length === 0) return [`${path} is empty`];
10784
+ return value.flatMap((relationship, index) => checkRelationship(relationship, `${path}[${index}]`));
10785
+ }
10786
+ function checkRuleList(value, path) {
10787
+ if (!Array.isArray(value)) return [`${path} must be a list of rules`];
10788
+ if (value.length === 0) return [`${path} is empty`];
10789
+ return value.flatMap((rule, index) => checkRule(rule, `${path}[${index}]`));
10790
+ }
10791
+ function checkRule(rule, path) {
10792
+ if (!isPlainObject(rule)) {
10793
+ return [`${path || "the rule"} must be a rule object${typeof rule === "string" ? ", not a profile name" : ""}`];
10794
+ }
10795
+ const problems = unknownFields(rule, RULE_FIELDS, path);
10796
+ if (!CHECK_FIELDS.some((field) => rule[field] !== void 0)) {
10797
+ problems.push(`${path ? `${path} ` : ""}checks nothing`);
10798
+ }
10799
+ if (rule["operator"] !== void 0 && rule["operator"] !== "AND" && rule["operator"] !== "OR") {
10800
+ problems.push(`${path}.operator must be "AND" or "OR"`);
10801
+ }
10802
+ if (rule["roles"] !== void 0) problems.push(...checkAllOrAny(rule["roles"], `${path}.roles`));
10803
+ if (rule["permissions"] !== void 0) problems.push(...checkAllOrAny(rule["permissions"], `${path}.permissions`));
10804
+ if (rule["attributes"] !== void 0) problems.push(...checkAttributes(rule["attributes"], `${path}.attributes`));
10805
+ if (rule["relationships"] !== void 0) {
10806
+ problems.push(...checkRelationships(rule["relationships"], `${path}.relationships`));
10807
+ }
10808
+ if (rule["custom"] !== void 0) {
10809
+ if (!isPlainObject(rule["custom"])) problems.push(`${path}.custom must be an object`);
10810
+ else if (Object.keys(rule["custom"]).length === 0) problems.push(`${path}.custom is empty`);
10811
+ }
10812
+ if (rule["guards"] !== void 0) {
10813
+ const guards = rule["guards"];
10814
+ if (!Array.isArray(guards)) problems.push(`${path}.guards must be a list of functions`);
10815
+ else if (guards.length === 0) problems.push(`${path}.guards is empty`);
10816
+ else if (!guards.every((guard) => typeof guard === "function")) {
10817
+ problems.push(`${path}.guards must contain only functions`);
10818
+ }
10819
+ }
10820
+ if (rule["allOf"] !== void 0) problems.push(...checkRuleList(rule["allOf"], `${path}.allOf`));
10821
+ if (rule["anyOf"] !== void 0) problems.push(...checkRuleList(rule["anyOf"], `${path}.anyOf`));
10822
+ if (rule["not"] !== void 0) problems.push(...checkRule(rule["not"], `${path}.not`));
10823
+ return problems;
10824
+ }
10825
+ function findAuthoritiesRuleProblems(authorities) {
10826
+ if (typeof authorities === "string") {
10827
+ return authorities.length > 0 ? [] : ["the profile name is empty"];
10828
+ }
10829
+ if (Array.isArray(authorities)) {
10830
+ if (authorities.length === 0) return ["the list of profiles is empty"];
10831
+ return authorities.every(isNonEmptyString) ? [] : ["a list of profiles must contain only non-empty profile names"];
10832
+ }
10833
+ if (!isPlainObject(authorities)) {
10834
+ return ["must be a profile name, a list of profile names, or a rule object"];
10835
+ }
10836
+ return checkRule(authorities, "");
10837
+ }
10838
+ function findAuthoritiesProfileProblems(name, policy) {
10839
+ return checkRule(policy, "").map((problem) => `profile "${name}": ${problem.replace(/^\./, "")}`);
10840
+ }
10841
+
10492
10842
  // libs/auth/src/authorities/authorities.engine.ts
10493
10843
  function mergeResult(base, ...others) {
10494
10844
  const evaluatedPolicies = [...base.evaluatedPolicies];
@@ -10497,8 +10847,12 @@ function mergeResult(base, ...others) {
10497
10847
  }
10498
10848
  return { ...base, evaluatedPolicies };
10499
10849
  }
10500
- function granted(policies) {
10501
- return { granted: true, evaluatedPolicies: policies };
10850
+ function invalidRule(problems, policies = []) {
10851
+ return {
10852
+ granted: false,
10853
+ deniedBy: `invalid authorities rule: ${problems.join("; ")}`,
10854
+ evaluatedPolicies: policies
10855
+ };
10502
10856
  }
10503
10857
  var AuthoritiesEngine = class {
10504
10858
  constructor(profiles, evaluators) {
@@ -10507,10 +10861,32 @@ var AuthoritiesEngine = class {
10507
10861
  }
10508
10862
  profiles;
10509
10863
  evaluators;
10864
+ /** Problems found per rule object, so a rule is validated once however often it is evaluated. */
10865
+ ruleProblems = /* @__PURE__ */ new WeakMap();
10866
+ /**
10867
+ * Problems with an `authorities` value declared on an entry (see {@link findAuthoritiesRuleProblems}).
10868
+ * The server reports them when it starts; {@link evaluate} denies such a value.
10869
+ */
10870
+ findRuleProblems(authorities) {
10871
+ return findAuthoritiesRuleProblems(authorities);
10872
+ }
10873
+ /**
10874
+ * Problems with every registered profile's rule (see {@link findAuthoritiesRuleProblems}).
10875
+ * The server reports them when it starts.
10876
+ */
10877
+ findProfileProblems() {
10878
+ return Object.entries(this.profiles.getAll()).flatMap(
10879
+ ([name, policy]) => findAuthoritiesProfileProblems(name, policy)
10880
+ );
10881
+ }
10510
10882
  /**
10511
10883
  * Evaluate an AuthoritiesMetadata value (string, string[], or policy object).
10884
+ *
10885
+ * A value that is malformed or checks nothing (see {@link findAuthoritiesRuleProblems}) is denied.
10512
10886
  */
10513
10887
  async evaluate(authorities, ctx) {
10888
+ const problems = this.problemsOf(authorities, findAuthoritiesRuleProblems);
10889
+ if (problems.length > 0) return invalidRule(problems);
10514
10890
  if (typeof authorities === "string") {
10515
10891
  return this.evaluateProfile(authorities, ctx);
10516
10892
  }
@@ -10531,6 +10907,8 @@ var AuthoritiesEngine = class {
10531
10907
  evaluatedPolicies: [`profile:${name}`]
10532
10908
  };
10533
10909
  }
10910
+ const problems = this.problemsOf(policy, (rule) => findAuthoritiesProfileProblems(name, rule));
10911
+ if (problems.length > 0) return invalidRule(problems, [`profile:${name}`]);
10534
10912
  const result = await this.evaluatePolicy(policy, ctx);
10535
10913
  if (!result.granted) {
10536
10914
  return {
@@ -10541,6 +10919,16 @@ var AuthoritiesEngine = class {
10541
10919
  }
10542
10920
  return mergeResult(result, { granted: true, evaluatedPolicies: [`profile:${name}`] });
10543
10921
  }
10922
+ /** Validate a rule, reusing the result for a rule object seen before. */
10923
+ problemsOf(rule, find) {
10924
+ if (typeof rule !== "object" || rule === null) return find(rule);
10925
+ let problems = this.ruleProblems.get(rule);
10926
+ if (!problems) {
10927
+ problems = find(rule);
10928
+ this.ruleProblems.set(rule, problems);
10929
+ }
10930
+ return problems;
10931
+ }
10544
10932
  /**
10545
10933
  * Evaluate an array of profile names (AND semantics).
10546
10934
  */
@@ -10589,7 +10977,7 @@ var AuthoritiesEngine = class {
10589
10977
  results.push(await this.evaluateNot(policy.not, ctx));
10590
10978
  }
10591
10979
  if (results.length === 0) {
10592
- return granted([]);
10980
+ return invalidRule(["checks nothing"]);
10593
10981
  }
10594
10982
  return this.combineResults(results, operator);
10595
10983
  }
@@ -10640,14 +11028,15 @@ var AuthoritiesEngine = class {
10640
11028
  }
10641
11029
  /**
10642
11030
  * Evaluate async guard functions in sequence.
10643
- * Each guard returns true (granted) or a string (denial message).
11031
+ * Only `true` grants. A string is the denial message; anything else (`false`, but also
11032
+ * `undefined` from a guard that forgot to return, `null`, `0` or an object) denies.
10644
11033
  */
10645
11034
  async evaluateGuards(guards, ctx) {
10646
11035
  for (let i = 0; i < guards.length; i++) {
10647
11036
  const guard = guards[i];
10648
11037
  const result = await guard(ctx);
10649
- if (result === false || typeof result === "string") {
10650
- const denialMessage = typeof result === "string" ? result : `guard[${i}] denied`;
11038
+ if (result !== true) {
11039
+ const denialMessage = typeof result === "string" ? result : result === false ? `guard[${i}] denied` : `guard[${i}] did not return true (returned ${result === null ? "null" : typeof result})`;
10651
11040
  return {
10652
11041
  granted: false,
10653
11042
  deniedBy: `guards[${i}]: ${denialMessage}`,
@@ -10705,6 +11094,13 @@ var AuthoritiesEngine = class {
10705
11094
  evaluatedPolicies: ["not", ...result.evaluatedPolicies]
10706
11095
  };
10707
11096
  }
11097
+ /**
11098
+ * The policy a profile name stands for when `evaluate()` is given that name, or `undefined`
11099
+ * when no such profile is registered. Lets callers inspect a named rule without evaluating it.
11100
+ */
11101
+ resolveProfile(name) {
11102
+ return this.profiles.resolve(name);
11103
+ }
10708
11104
  };
10709
11105
 
10710
11106
  // libs/auth/src/authorities/authorities.registry.ts
@@ -10949,6 +11345,7 @@ export {
10949
11345
  AuthoritiesProfileRegistry,
10950
11346
  AuthorityDeniedError,
10951
11347
  AuthorizationBase,
11348
+ BUILT_IN_AUTH_PAGE_CSP,
10952
11349
  CDN,
10953
11350
  CREDENTIALS_ACCESSOR,
10954
11351
  CimdClientIdMismatchError,
@@ -10963,6 +11360,7 @@ export {
10963
11360
  CredentialProviderAlreadyRegisteredError,
10964
11361
  CredentialStorageError,
10965
11362
  CredentialsAccessorImpl,
11363
+ DEFAULT_ALLOWED_SCOPES,
10966
11364
  DEFAULT_INCREMENTAL_TICKET_TTL_MS,
10967
11365
  DEFAULT_RESUME_TTL_MS,
10968
11366
  DEFAULT_THEME,
@@ -11001,6 +11399,9 @@ export {
11001
11399
  RedisAuthorizationStore,
11002
11400
  RedisSessionStore,
11003
11401
  SECURE_STORE_ACCESSOR,
11402
+ SIGNIN_BINDING_COOKIE_PREFIX,
11403
+ SIGNIN_BINDING_MAX_AGE_SECONDS,
11404
+ SIGNIN_BINDING_SECURE_COOKIE_PREFIX,
11004
11405
  ScopeDeniedError,
11005
11406
  SecureStoreAccessorImpl,
11006
11407
  SessionCredentialVault,
@@ -11076,6 +11477,7 @@ export {
11076
11477
  buildToolConsentPage,
11077
11478
  buildUnauthorizedHeader,
11078
11479
  buildWwwAuthenticate,
11480
+ builtInAuthPageHeaders,
11079
11481
  centeredCardLayout,
11080
11482
  checkSsrfProtection,
11081
11483
  cimdCacheConfigSchema,
@@ -11095,6 +11497,7 @@ export {
11095
11497
  createLayout,
11096
11498
  createSecureStore,
11097
11499
  createSessionCredentialVault,
11500
+ createSigninBinding,
11098
11501
  createTokenStorageAdapter,
11099
11502
  credentialProviderConfigSchema,
11100
11503
  credentialSchema,
@@ -11102,7 +11505,7 @@ export {
11102
11505
  credentialTypeSchema,
11103
11506
  customCredentialSchema,
11104
11507
  decodeJwtPayloadSafe,
11105
- decryptAesGcm5 as decryptAesGcm,
11508
+ decryptAesGcm6 as decryptAesGcm,
11106
11509
  decryptSessionJson,
11107
11510
  decryptValue2 as decryptValue,
11108
11511
  defaultSessionRateLimiter,
@@ -11112,7 +11515,7 @@ export {
11112
11515
  deriveTypedUser,
11113
11516
  detectAuthProviders,
11114
11517
  detectedAuthProviderSchema,
11115
- encryptAesGcm5 as encryptAesGcm,
11518
+ encryptAesGcm6 as encryptAesGcm,
11116
11519
  encryptJson,
11117
11520
  encryptValue2 as encryptValue,
11118
11521
  encryptedBlobSchema,
@@ -11127,6 +11530,8 @@ export {
11127
11530
  federatedLoginStateSchema,
11128
11531
  federatedProviderItemSchema,
11129
11532
  federatedSelectionSchema,
11533
+ findAuthoritiesProfileProblems,
11534
+ findAuthoritiesRuleProblems,
11130
11535
  flatRemoteProviderFields,
11131
11536
  fromSessionRecord,
11132
11537
  generateAuthorizationCode,
@@ -11138,15 +11543,17 @@ export {
11138
11543
  getProviderApps,
11139
11544
  getProviderScopes,
11140
11545
  getTokenSignatureFingerprint,
11546
+ grantScopes,
11141
11547
  hasOnlyLocalhostRedirectUris,
11142
11548
  headerApplier,
11143
- hkdfSha2564 as hkdfSha256,
11549
+ hkdfSha2565 as hkdfSha256,
11144
11550
  incrementalAuthConfigSchema,
11145
11551
  isAnonymousSubject,
11146
11552
  isCimdClientId,
11147
11553
  isFrontMcpCredentials,
11148
11554
  isJwt,
11149
11555
  isLocalMode,
11556
+ isLoopbackRedirectUri,
11150
11557
  isOrchestratedLocal,
11151
11558
  isOrchestratedMode,
11152
11559
  isOrchestratedRemote,
@@ -11176,6 +11583,7 @@ export {
11176
11583
  noopLogger,
11177
11584
  normalizeIssuer,
11178
11585
  oauthCredentialSchema,
11586
+ openPendingLogin,
11179
11587
  orchestratedLocalSchema,
11180
11588
  orchestratedRemoteSchema,
11181
11589
  parseAuthOptions,
@@ -11203,6 +11611,7 @@ export {
11203
11611
  resolveRequiredScopes,
11204
11612
  resourceIdRefSchema,
11205
11613
  safeDecrypt,
11614
+ sealPendingLogin,
11206
11615
  secureStoreConfigSchema,
11207
11616
  secureStoreCustomBackendSchema,
11208
11617
  secureStoreEncryptionSchema,
@@ -11213,6 +11622,8 @@ export {
11213
11622
  signCredentialResumeToken,
11214
11623
  signIncrementalAuthTicket,
11215
11624
  signSession,
11625
+ signinBindingCookieName,
11626
+ signinBindingMatches,
11216
11627
  skippedAppBehaviorSchema,
11217
11628
  sseTransportStateSchema,
11218
11629
  sshKeyCredentialSchema,
@@ -11245,5 +11656,6 @@ export {
11245
11656
  verifyOrParseSession,
11246
11657
  verifyPkce,
11247
11658
  verifySession,
11248
- wideLayout
11659
+ wideLayout,
11660
+ withoutSecretFieldValues
11249
11661
  };