@authup/access 1.0.0-beta.65 → 1.0.0-beta.67

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/dist/index.d.mts +557 -65
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +838 -30
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/permission/authorization/check-schema.d.ts +23 -0
  6. package/dist/permission/authorization/check-schema.d.ts.map +1 -0
  7. package/dist/permission/authorization/check.d.ts +7 -0
  8. package/dist/permission/authorization/check.d.ts.map +1 -0
  9. package/dist/permission/authorization/constants.d.ts +14 -0
  10. package/dist/permission/authorization/constants.d.ts.map +1 -0
  11. package/dist/permission/authorization/error.d.ts +18 -0
  12. package/dist/permission/authorization/error.d.ts.map +1 -0
  13. package/dist/permission/authorization/index.d.ts +9 -0
  14. package/dist/permission/authorization/index.d.ts.map +1 -0
  15. package/dist/permission/authorization/module.d.ts +77 -0
  16. package/dist/permission/authorization/module.d.ts.map +1 -0
  17. package/dist/permission/authorization/policy.d.ts +31 -0
  18. package/dist/permission/authorization/policy.d.ts.map +1 -0
  19. package/dist/permission/authorization/schema.d.ts +107 -0
  20. package/dist/permission/authorization/schema.d.ts.map +1 -0
  21. package/dist/permission/authorization/types.d.ts +136 -0
  22. package/dist/permission/authorization/types.d.ts.map +1 -0
  23. package/dist/permission/evaluator/module.d.ts.map +1 -1
  24. package/dist/permission/evaluator/types.d.ts +16 -1
  25. package/dist/permission/evaluator/types.d.ts.map +1 -1
  26. package/dist/permission/index.d.ts +1 -0
  27. package/dist/permission/index.d.ts.map +1 -1
  28. package/dist/permission/realm-scope/helpers.d.ts +1 -1
  29. package/dist/permission/realm-scope/helpers.d.ts.map +1 -1
  30. package/dist/policy/built-in/date/evaluator.d.ts.map +1 -1
  31. package/dist/policy/built-in/permission-binding/identity-evaluator.d.ts +21 -0
  32. package/dist/policy/built-in/permission-binding/identity-evaluator.d.ts.map +1 -0
  33. package/dist/policy/built-in/permission-binding/index.d.ts +1 -0
  34. package/dist/policy/built-in/permission-binding/index.d.ts.map +1 -1
  35. package/dist/policy/built-in/permission-binding/validator.d.ts.map +1 -1
  36. package/dist/policy/built-in/time/evaluator.d.ts.map +1 -1
  37. package/dist/policy/constants.d.ts +11 -0
  38. package/dist/policy/constants.d.ts.map +1 -1
  39. package/dist/policy/data.d.ts +2 -0
  40. package/dist/policy/data.d.ts.map +1 -1
  41. package/dist/policy/evaluation/context/define.d.ts.map +1 -1
  42. package/dist/policy/evaluation/context/index.d.ts +1 -0
  43. package/dist/policy/evaluation/context/index.d.ts.map +1 -1
  44. package/dist/policy/evaluation/context/transition.d.ts +13 -0
  45. package/dist/policy/evaluation/context/transition.d.ts.map +1 -0
  46. package/dist/policy/evaluation/context/types.d.ts +27 -0
  47. package/dist/policy/evaluation/context/types.d.ts.map +1 -1
  48. package/dist/policy/types.d.ts +5 -0
  49. package/dist/policy/types.d.ts.map +1 -1
  50. package/package.json +8 -8
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { AuthupError, ErrorCode, isAuthupError, markInstanceof, matchesInstanceof } from "@authup/errors";
1
+ import { AuthupError, ErrorCode, InternalError, isAuthupError, markInstanceof, matchesInstanceof } from "@authup/errors";
2
2
  import { DecisionStrategy, flattenObject, hasOwnProperty, isObject } from "@authup/kit";
3
3
  import { isEqual } from "smob";
4
4
  import { and, eq, inArray, nin, not, or } from "@rapiq/core";
5
- import { defineIssueGroup, defineIssueItem } from "@ebec/core";
6
- import { createValidator } from "@validup/zod";
7
- import { Container } from "validup";
5
+ import { defineIssueGroup, defineIssueItem, prefixIssuePath } from "@ebec/core";
6
+ import { buildIssuesForZodError, createValidator } from "@validup/zod";
7
+ import { Container, ValidupError, isValidupError } from "validup";
8
8
  import { z } from "zod";
9
9
  import { compileFilters } from "@rapiq/adapter-memory";
10
10
  import { MongoFiltersParser } from "@rapiq/parser-mongo";
@@ -515,6 +515,10 @@ var PolicyData = class PolicyData {
515
515
  has(key) {
516
516
  return key in this.data;
517
517
  }
518
+ delete(key) {
519
+ delete this.data[key];
520
+ this.validated.delete(key);
521
+ }
518
522
  setValidated(key) {
519
523
  this.validated.add(key);
520
524
  }
@@ -534,7 +538,26 @@ function definePolicyEvaluationContext(input = {}) {
534
538
  data: input.data || new PolicyData(),
535
539
  include: input.include || [],
536
540
  exclude: input.exclude || [],
537
- withConditions: input.withConditions ?? false
541
+ withConditions: input.withConditions ?? false,
542
+ realmAttributeName: input.realmAttributeName,
543
+ transitions: input.transitions
544
+ };
545
+ }
546
+ //#endregion
547
+ //#region src/policy/evaluation/context/transition.ts
548
+ /**
549
+ * A sink keeping the earliest reported instant, which is the moment an answer
550
+ * built from every evaluation it was handed to stops being current.
551
+ */
552
+ function createPolicyTransitionCollector() {
553
+ let next;
554
+ return {
555
+ report(at) {
556
+ if (!next || at.getTime() < next.getTime()) next = at;
557
+ },
558
+ get next() {
559
+ return next;
560
+ }
538
561
  };
539
562
  }
540
563
  //#endregion
@@ -757,7 +780,7 @@ var CompositePolicyValidator = class extends Container {
757
780
  initialize() {
758
781
  super.initialize();
759
782
  this.mount("decisionStrategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
760
- this.mount("children", createValidator(z.array(z.looseObject({ type: z.string().min(3) }))));
783
+ this.mount("children", createValidator(z.array(z.looseObject({ type: z.string().min(1) }))));
761
784
  this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
762
785
  }
763
786
  };
@@ -895,6 +918,22 @@ function toDate$1(input) {
895
918
  if (typeof input === "number") return new Date(input);
896
919
  return input;
897
920
  }
921
+ /**
922
+ * The next instant at which a date policy's verdict could change, seen from the
923
+ * (day-normalized) `now`: the start day while it is still ahead, else the
924
+ * midnight after the end day while that day has not passed. Nothing once the
925
+ * end day is behind, since the verdict is final from then on.
926
+ */
927
+ function nextDateTransition(policy, now) {
928
+ if (policy.start) {
929
+ const start = normalizeDate$1(toDate$1(policy.start));
930
+ if (now < start) return start;
931
+ }
932
+ if (policy.end) {
933
+ const end = normalizeDate$1(toDate$1(policy.end));
934
+ if (now <= end) return new Date(end.getFullYear(), end.getMonth(), end.getDate() + 1);
935
+ }
936
+ }
898
937
  var DatePolicyEvaluator = class {
899
938
  validator;
900
939
  constructor() {
@@ -910,7 +949,13 @@ var DatePolicyEvaluator = class {
910
949
  ctx.data.set("date", now);
911
950
  ctx.data.setValidated("date");
912
951
  }
913
- } else now = normalizeDate$1(/* @__PURE__ */ new Date());
952
+ } else {
953
+ now = normalizeDate$1(/* @__PURE__ */ new Date());
954
+ if (ctx.transitions) {
955
+ const next = nextDateTransition(policy, now);
956
+ if (next) ctx.transitions.report(next);
957
+ }
958
+ }
914
959
  if (policy.start) {
915
960
  const start = normalizeDate$1(toDate$1(policy.start));
916
961
  if (now < start) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
@@ -987,25 +1032,6 @@ var IdentityPolicyEvaluator = class {
987
1032
  }
988
1033
  };
989
1034
  //#endregion
990
- //#region src/policy/built-in/permission-binding/validator.ts
991
- var PermissionBindingPolicyValidator = class extends Container {
992
- initialize() {}
993
- };
994
- //#endregion
995
- //#region src/policy/built-in/permission-binding/module.ts
996
- var PermissionBindingPolicyEvaluator = class {
997
- validator;
998
- constructor() {
999
- this.validator = new PermissionBindingPolicyValidator();
1000
- }
1001
- requires() {
1002
- return ["permissionBinding"];
1003
- }
1004
- async evaluate(value) {
1005
- return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
1006
- }
1007
- };
1008
- //#endregion
1009
1035
  //#region src/policy/built-in/realm-match/validator.ts
1010
1036
  var RealmMatchPolicyValidator = class extends Container {
1011
1037
  initialize() {
@@ -1062,7 +1088,7 @@ var RealmMatchPolicyEvaluator = class {
1062
1088
  else {
1063
1089
  const terms = [];
1064
1090
  if (realmId !== null && scope !== "none") terms.push(eq(field, realmId));
1065
- if (scope === "ownOrNull") terms.push(eq(field, null));
1091
+ if (scope === "ownOrNull" && (realmId !== null || identity.realmName != null)) terms.push(eq(field, null));
1066
1092
  if (terms.length === 0) condition = inArray(field, []);
1067
1093
  else if (terms.length === 1) condition = terms[0];
1068
1094
  else condition = or(...terms);
@@ -1161,6 +1187,159 @@ var RealmMatchPolicyEvaluator = class {
1161
1187
  }
1162
1188
  };
1163
1189
  //#endregion
1190
+ //#region src/policy/built-in/permission-binding/validator.ts
1191
+ var PermissionBindingPolicyValidator = class extends Container {
1192
+ initialize() {
1193
+ this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
1194
+ }
1195
+ };
1196
+ //#endregion
1197
+ //#region src/policy/built-in/permission-binding/identity-evaluator.ts
1198
+ function containsPermissionBinding(policy) {
1199
+ if (policy.type === "permissionBinding") return true;
1200
+ if (policy.type !== "composite") return false;
1201
+ const children = policy.children;
1202
+ return Array.isArray(children) && children.some((child) => !!child && typeof child === "object" && containsPermissionBinding(child));
1203
+ }
1204
+ var IdentityPermissionBindingPolicyEvaluator = class {
1205
+ validator;
1206
+ identityEvaluator;
1207
+ realmMatchEvaluator;
1208
+ identityPermissionProvider;
1209
+ constructor(identityPermissionProvider) {
1210
+ this.validator = new PermissionBindingPolicyValidator();
1211
+ this.identityEvaluator = new IdentityPolicyEvaluator();
1212
+ this.realmMatchEvaluator = new RealmMatchPolicyEvaluator();
1213
+ this.identityPermissionProvider = identityPermissionProvider;
1214
+ }
1215
+ requires() {
1216
+ return ["permissionBinding"];
1217
+ }
1218
+ async accessData(ctx) {
1219
+ if (!ctx.data.has("permissionBinding")) return null;
1220
+ if (ctx.data.isValidated("permissionBinding")) return ctx.data.get("permissionBinding");
1221
+ const data = ctx.data.get("permissionBinding");
1222
+ ctx.data.set("permissionBinding", data);
1223
+ ctx.data.setValidated("permissionBinding");
1224
+ return data;
1225
+ }
1226
+ async evaluate(value, ctx) {
1227
+ const policy = await this.validator.run(value);
1228
+ const identity = await this.identityEvaluator.accessData(ctx);
1229
+ if (!identity) return {
1230
+ success: false,
1231
+ issues: [definePolicyIssueItem({
1232
+ code: "DATA_MISSING",
1233
+ message: "The data property identity is missing",
1234
+ path: ctx.path
1235
+ })]
1236
+ };
1237
+ const binding = await this.accessData(ctx);
1238
+ if (!binding) return {
1239
+ success: false,
1240
+ issues: [definePolicyIssueItem({
1241
+ code: "DATA_MISSING",
1242
+ message: "The data property permission is missing",
1243
+ path: ctx.path
1244
+ })]
1245
+ };
1246
+ const identityBindings = await this.identityPermissionProvider.getFor(identity).then((bindings) => bindings.filter((item) => {
1247
+ if (item.permission.name !== binding.permission.name) return false;
1248
+ return (binding.permission.realmId ?? null) === (item.permission.realmId ?? null) && (binding.permission.clientId ?? null) === (item.permission.clientId ?? null);
1249
+ }));
1250
+ if (identityBindings.length === 0) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1251
+ const [aggr] = aggregatePermissionPolicyBindings(identityBindings);
1252
+ if (!aggr) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1253
+ const issues = [];
1254
+ const pendingIssues = [];
1255
+ const conditions = [];
1256
+ let pending = false;
1257
+ let lowerable = true;
1258
+ for (const grant of aggr.grants) {
1259
+ let reachCondition = null;
1260
+ const realmOutcome = await this.realmMatchEvaluator.evaluate({
1261
+ scope: grant.realmScope,
1262
+ attributeName: ctx.realmAttributeName
1263
+ }, ctx);
1264
+ if (realmOutcome.pending) {
1265
+ const scope = normalizeRealmScope(grant.realmScope);
1266
+ if (scope === "none") continue;
1267
+ if (scope !== "any") {
1268
+ if (!realmOutcome.condition) {
1269
+ pending = true;
1270
+ lowerable = false;
1271
+ continue;
1272
+ }
1273
+ reachCondition = realmOutcome.condition;
1274
+ }
1275
+ } else if (!realmOutcome.success) continue;
1276
+ if (!grant.policy) {
1277
+ if (!reachCondition) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
1278
+ conditions.push(reachCondition);
1279
+ pending = true;
1280
+ continue;
1281
+ }
1282
+ if (containsPermissionBinding(grant.policy)) {
1283
+ issues.push(definePolicyIssueItem({
1284
+ code: "INVALID",
1285
+ message: "A grant policy must not evaluate the permission binding.",
1286
+ path: ctx.path
1287
+ }));
1288
+ continue;
1289
+ }
1290
+ const outcome = await new PolicyEngine(ctx.evaluators).evaluate(grant.policy, {
1291
+ ...ctx,
1292
+ path: [...ctx.path || []]
1293
+ });
1294
+ if (outcome.success) {
1295
+ if (!reachCondition) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
1296
+ conditions.push(reachCondition);
1297
+ pending = true;
1298
+ continue;
1299
+ }
1300
+ if (outcome.pending) {
1301
+ pending = true;
1302
+ if (ctx.withConditions && outcome.condition && !ctx.realmAttributeName) conditions.push(reachCondition ? and(reachCondition, outcome.condition) : outcome.condition);
1303
+ else lowerable = false;
1304
+ if (outcome.issues) pendingIssues.push(...outcome.issues);
1305
+ continue;
1306
+ }
1307
+ if (outcome.issues) issues.push(...outcome.issues);
1308
+ }
1309
+ if (pending) {
1310
+ const result = {
1311
+ success: false,
1312
+ pending: true,
1313
+ issues: pendingIssues
1314
+ };
1315
+ if (lowerable && ctx.withConditions && conditions.length > 0) {
1316
+ const condition = conditions.length === 1 ? conditions[0] : or(...conditions);
1317
+ result.condition = policy.invert ? not(condition) : condition;
1318
+ }
1319
+ return result;
1320
+ }
1321
+ const success = maybeInvertPolicyOutcome(false, policy.invert);
1322
+ return {
1323
+ success,
1324
+ issues: success ? [] : issues
1325
+ };
1326
+ }
1327
+ };
1328
+ //#endregion
1329
+ //#region src/policy/built-in/permission-binding/module.ts
1330
+ var PermissionBindingPolicyEvaluator = class {
1331
+ validator;
1332
+ constructor() {
1333
+ this.validator = new PermissionBindingPolicyValidator();
1334
+ }
1335
+ requires() {
1336
+ return ["permissionBinding"];
1337
+ }
1338
+ async evaluate(value) {
1339
+ return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
1340
+ }
1341
+ };
1342
+ //#endregion
1164
1343
  //#region src/policy/built-in/time/constants.ts
1165
1344
  let TimePolicyInterval = /* @__PURE__ */ function(TimePolicyInterval) {
1166
1345
  TimePolicyInterval["DAILY"] = "daily";
@@ -1220,6 +1399,30 @@ function toDate(input, dateRef) {
1220
1399
  if (typeof input === "number") return new Date(input);
1221
1400
  return input;
1222
1401
  }
1402
+ /**
1403
+ * The next instant at which a time policy's verdict could change, seen from
1404
+ * `now`: today's start while it is ahead, the minute after today's end while
1405
+ * that is ahead, and the next local midnight, where the daily window moves on
1406
+ * and the day-of-week/month/year checks roll over. The earliest of those; a
1407
+ * policy with no constraint never changes.
1408
+ *
1409
+ * `normalizeDate` keeps the seconds and milliseconds of the instant it is
1410
+ * evaluated at and drops the policy's own, so the comparisons below are made
1411
+ * at minute precision: `now < start` stops holding at HH:MM:00.000 of the
1412
+ * start, and `now > end` starts holding at the following minute. Reading the
1413
+ * candidates off `normalizeDate` instead would report the seconds of this
1414
+ * evaluation, up to a minute after the verdict already flipped.
1415
+ */
1416
+ function nextTimeTransition(policy, now) {
1417
+ const candidates = [];
1418
+ const atMinute = (input) => new Date(now.getFullYear(), now.getMonth(), now.getDate(), input.getHours(), input.getMinutes());
1419
+ if (policy.start) candidates.push(atMinute(toDate(policy.start, now)));
1420
+ if (policy.end) candidates.push(new Date(atMinute(toDate(policy.end, now)).getTime() + 6e4));
1421
+ if (policy.start || policy.end || policy.interval) candidates.push(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1));
1422
+ let next;
1423
+ for (const candidate of candidates) if (candidate.getTime() > now.getTime() && (!next || candidate.getTime() < next.getTime())) next = candidate;
1424
+ return next;
1425
+ }
1223
1426
  var TimePolicyEvaluator = class {
1224
1427
  validator;
1225
1428
  constructor() {
@@ -1235,7 +1438,13 @@ var TimePolicyEvaluator = class {
1235
1438
  ctx.data.set("time", now);
1236
1439
  ctx.data.setValidated("time");
1237
1440
  }
1238
- } else now = /* @__PURE__ */ new Date();
1441
+ } else {
1442
+ now = /* @__PURE__ */ new Date();
1443
+ if (ctx.transitions) {
1444
+ const next = nextTimeTransition(policy, now);
1445
+ if (next) ctx.transitions.report(next);
1446
+ }
1447
+ }
1239
1448
  if (policy.start) {
1240
1449
  const start = normalizeDate(toDate(policy.start, now), now);
1241
1450
  if (now < start) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
@@ -1280,6 +1489,25 @@ const PolicyDefaultEvaluators = {
1280
1489
  ["realmMatch"]: new RealmMatchPolicyEvaluator(),
1281
1490
  ["time"]: new TimePolicyEvaluator()
1282
1491
  };
1492
+ /**
1493
+ * The configuration validator of every built-in policy type, keyed like
1494
+ * `PolicyDefaultEvaluators`. It is what `projectAuthorizationPolicy` runs a
1495
+ * node through, so a type absent here cannot travel in the authorization
1496
+ * catalog. The registry is open like the evaluator one (#3635): spread it and
1497
+ * add a custom type's validator to let that type travel. It is frozen, since
1498
+ * a write into it would make the type projectable for every caller in the
1499
+ * process.
1500
+ */
1501
+ const PolicyDefaultValidators = Object.freeze({
1502
+ ["composite"]: new CompositePolicyValidator(),
1503
+ ["attributes"]: new AttributesPolicyValidator(),
1504
+ ["attributeNames"]: new AttributeNamesPolicyValidator(),
1505
+ ["date"]: new DatePolicyValidator(),
1506
+ ["identity"]: new IdentityPolicyValidator(),
1507
+ ["permissionBinding"]: new PermissionBindingPolicyValidator(),
1508
+ ["realmMatch"]: new RealmMatchPolicyValidator(),
1509
+ ["time"]: new TimePolicyValidator()
1510
+ });
1283
1511
  //#endregion
1284
1512
  //#region src/policy/input.ts
1285
1513
  /**
@@ -1356,6 +1584,7 @@ var PermissionEvaluator = class {
1356
1584
  const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
1357
1585
  include: options.policiesIncluded,
1358
1586
  exclude: options.policiesExcluded,
1587
+ transitions: options.transitions,
1359
1588
  data
1360
1589
  }));
1361
1590
  if (evaluationResult.success || options.pendingPolicies === "permit" && !!evaluationResult.pending) {
@@ -1435,7 +1664,8 @@ var PermissionEvaluator = class {
1435
1664
  };
1436
1665
  const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
1437
1666
  data,
1438
- withConditions: true
1667
+ withConditions: true,
1668
+ realmAttributeName: ctx.realmAttributeName
1439
1669
  }));
1440
1670
  if (evaluationResult.success) return { verdict: "allow" };
1441
1671
  if (evaluationResult.pending) {
@@ -1459,6 +1689,584 @@ var PermissionEvaluator = class {
1459
1689
  }
1460
1690
  };
1461
1691
  //#endregion
1462
- export { AttributeNamesPolicyEvaluator, AttributeNamesPolicyValidator, AttributesPolicyEvaluator, AttributesPolicyValidator, BuiltInPolicyType, CompositePolicyEvaluator, CompositePolicyValidator, DatePolicyEvaluator, DatePolicyValidator, IdentityPolicyEvaluator, IdentityPolicyValidator, PERMISSION_ERROR_INSTANCE, POLICY_ERROR_INSTANCE, PermissionBindingPolicyEvaluator, PermissionBindingPolicyValidator, PermissionError, PermissionEvaluator, PermissionMemoryProvider, PolicyData, PolicyDefaultEvaluators, PolicyEngine, PolicyError, PolicyIssueCode, PolicyIssueSeverity, REALM_SCOPE_ORDER, RealmMatchPolicyEvaluator, RealmMatchPolicyValidator, RealmScope, SystemPolicyName, TimePolicyEvaluator, TimePolicyInterval, TimePolicyValidator, aggregatePermissionPolicyBindings, buildPermissionKey, compareRealmScope, defineAttributesPolicy, definePolicyData, definePolicyEvaluationContext, definePolicyIssueGroup, definePolicyIssueItem, definePolicyWithType, grantDominates, isCompositePolicy, isIntervalForDayOfMonth, isIntervalForDayOfWeek, isIntervalForDayOfYear, isPermissionError, isPermissionPolicyBindingEqual, isPolicyError, maxRealmScope, maybeInvertPolicyOutcome, minRealmScope, normalizeRealmScope, realmScopeMatches };
1692
+ //#region src/permission/authorization/error.ts
1693
+ const AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE = Symbol.for("@authup/access/AuthorizationCatalogStaleError");
1694
+ /**
1695
+ * A grant references a definition or a policy the supplied catalog does not
1696
+ * carry: the caller's cached catalog predates the definition or the junction
1697
+ * row and must be refetched. A definition the catalog carries with
1698
+ * `policies: null`, and one whose tree this copy cannot project, are not
1699
+ * stale but unevaluable, and a grant of either is dropped.
1700
+ *
1701
+ * Recognize it with `isAuthorizationCatalogStaleError`, never `instanceof`:
1702
+ * this package is a plain caret dependency of several workspaces, so a
1703
+ * consumer tree may resolve two copies of it and turn the one recoverable
1704
+ * state into an unrecoverable one.
1705
+ */
1706
+ var AuthorizationCatalogStaleError = class extends AuthupError {
1707
+ constructor(message) {
1708
+ super({ message });
1709
+ this.name = "AuthorizationCatalogStaleError";
1710
+ markInstanceof(this, AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE);
1711
+ }
1712
+ };
1713
+ //#endregion
1714
+ //#region src/permission/authorization/check.ts
1715
+ /**
1716
+ * Marker-only: the error carries no code of its own, and it never crosses a
1717
+ * wire boundary, so there is no shape to fall back to.
1718
+ */
1719
+ function isAuthorizationCatalogStaleError(input) {
1720
+ return matchesInstanceof(input, AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE);
1721
+ }
1722
+ //#endregion
1723
+ //#region src/permission/authorization/schema.ts
1724
+ const id = z.string().min(1);
1725
+ const namespaceId = id.nullable();
1726
+ /**
1727
+ * A policy node as it travels. Deliberately LOOSE: every key beyond `type` is
1728
+ * that type's own configuration, and `projectAuthorizationPolicy` is what
1729
+ * validates it, by running the type's own validator. Mounting a node in a
1730
+ * validup container would strip every key the container does not mount, and
1731
+ * the whole configuration would go with it.
1732
+ */
1733
+ const authorizationPolicySchema = z.looseObject({ type: z.string().min(1) });
1734
+ /**
1735
+ * One permission definition. `policies` carries the ids of its
1736
+ * definition-layer trees, or `null` when the server could not project one.
1737
+ */
1738
+ const authorizationDefinitionSchema = z.object({
1739
+ name: z.string().min(1),
1740
+ realm_id: namespaceId,
1741
+ client_id: namespaceId,
1742
+ decision_strategy: z.enum(DecisionStrategy).nullable(),
1743
+ policies: z.array(id).nullable()
1744
+ });
1745
+ const authorizationCatalogSchema = z.object({
1746
+ policies: z.record(id, authorizationPolicySchema),
1747
+ permissions: z.array(authorizationDefinitionSchema)
1748
+ });
1749
+ /**
1750
+ * One grant of the identity, as the introspection endpoints report it.
1751
+ * Whether a grant's policy ids are declared is checked against the catalog
1752
+ * the consumer holds, since the two travel separately.
1753
+ */
1754
+ const authorizationGrantSchema = z.object({
1755
+ name: z.string().min(1),
1756
+ realm_id: namespaceId.optional(),
1757
+ client_id: namespaceId.optional(),
1758
+ realm_scope: z.enum(RealmScope).nullish(),
1759
+ policies: z.array(id).nullish()
1760
+ });
1761
+ const authorizationIdentitySchema = z.object({
1762
+ id: z.string().min(1),
1763
+ type: z.enum(["user", "client"]),
1764
+ realmId: z.string().nullish(),
1765
+ realmName: z.string().nullish(),
1766
+ clientId: z.string().nullish()
1767
+ });
1768
+ /**
1769
+ * The whole input `createAuthorizationEvaluator` takes: the catalog
1770
+ * `GET /authorization` serves, the grants an introspection reports and the
1771
+ * identity it names. A validup container over zod mounts, the shape every
1772
+ * validator in this package uses, so a consumer can reuse or override one
1773
+ * member's rules instead of restating the document.
1774
+ */
1775
+ var AuthorizationEvaluatorInputValidator = class extends Container {
1776
+ initialize() {
1777
+ super.initialize();
1778
+ this.mount("catalog", createValidator(authorizationCatalogSchema));
1779
+ this.mount("grants", createValidator(z.array(authorizationGrantSchema).optional()));
1780
+ this.mount("identity", createValidator(authorizationIdentitySchema.optional()));
1781
+ }
1782
+ };
1783
+ const validator = new AuthorizationEvaluatorInputValidator();
1784
+ /**
1785
+ * Validate the evaluator's input, plus the two rules per-key validators
1786
+ * cannot express.
1787
+ *
1788
+ * Every id a definition names must be a key of `policies`: a dangling id
1789
+ * resolves to `undefined`, which reads as "this layer carries no policy", so
1790
+ * a catalog that merely omitted a tree would grant unrestricted access rather
1791
+ * than fail. The lookup is own-property only, so an id such as `constructor`
1792
+ * cannot be answered by a member of `Object.prototype`.
1793
+ *
1794
+ * And the identity and its grants travel together: grants without an identity
1795
+ * belong to nobody, while an identity without a grant list is the shape an
1796
+ * INACTIVE introspection produces (`permissions` is absent unless the
1797
+ * credential is active), which read as "holds nothing" would authorize every
1798
+ * definition that carries no binding check. An identity holding no grant says
1799
+ * so with an empty array.
1800
+ */
1801
+ async function parseAuthorizationEvaluatorInput(input) {
1802
+ const identityGiven = typeof input.identity !== "undefined";
1803
+ const grantsGiven = typeof input.grants !== "undefined";
1804
+ if (!identityGiven && grantsGiven) throw new ValidupError([defineIssueItem({
1805
+ message: "Grants require the identity they belong to.",
1806
+ path: ["identity"]
1807
+ })]);
1808
+ if (identityGiven && !grantsGiven) throw new ValidupError([defineIssueItem({
1809
+ message: "An identity requires its grant list; pass an empty array for an identity holding none.",
1810
+ path: ["grants"]
1811
+ })]);
1812
+ const { catalog, grants, identity } = await validator.run(structuredClone(input));
1813
+ for (const [i, definition] of catalog.permissions.entries()) for (const [j, policyId] of (definition.policies ?? []).entries()) {
1814
+ if (Object.hasOwn(catalog.policies, policyId)) continue;
1815
+ throw new ValidupError([defineIssueItem({
1816
+ message: `The policy ${policyId} is not declared by the catalog.`,
1817
+ path: [
1818
+ "catalog",
1819
+ "permissions",
1820
+ i,
1821
+ "policies",
1822
+ j
1823
+ ]
1824
+ })]);
1825
+ }
1826
+ return {
1827
+ catalog,
1828
+ grants: grants ?? [],
1829
+ identity
1830
+ };
1831
+ }
1832
+ /**
1833
+ * One verdict: the permission namespace and the requested realms it holds in.
1834
+ * A realm value is a realm key the caller itself supplied, or `null` for the
1835
+ * global rows, so nothing here is resolved or looked up.
1836
+ */
1837
+ const authorizationCheckPermissionSchema = z.object({
1838
+ name: z.string().min(1),
1839
+ realms: z.array(z.string().min(1).nullable()).min(1)
1840
+ });
1841
+ const authorizationCheckPermissionsSchema = z.array(authorizationCheckPermissionSchema);
1842
+ //#endregion
1843
+ //#region src/permission/authorization/check-schema.ts
1844
+ /**
1845
+ * The whole input `createAuthorizationCheckEvaluator` takes: the answer
1846
+ * `POST /authorization/check` served and the identity it belongs to. Same
1847
+ * validup-over-zod shape as the catalog's input, so a consumer can override
1848
+ * one member's rules instead of restating the answer.
1849
+ */
1850
+ var AuthorizationCheckEvaluatorInputValidator = class extends Container {
1851
+ initialize() {
1852
+ super.initialize();
1853
+ this.mount("permissions", createValidator(authorizationCheckPermissionsSchema));
1854
+ this.mount("identity", createValidator(authorizationIdentitySchema.optional()));
1855
+ }
1856
+ };
1857
+ const checkValidator = new AuthorizationCheckEvaluatorInputValidator();
1858
+ /**
1859
+ * Validate the check evaluator's input, plus the one rule a per-key validator
1860
+ * cannot express: a permission namespace appears at most once. Two entries for
1861
+ * one name would make the answer depend on which the consumer looked up first,
1862
+ * and the server emits one entry per name by construction.
1863
+ */
1864
+ async function parseAuthorizationCheckEvaluatorInput(input) {
1865
+ const { permissions, identity } = await checkValidator.run(structuredClone(input));
1866
+ const seen = /* @__PURE__ */ new Set();
1867
+ for (const [i, permission] of permissions.entries()) {
1868
+ if (seen.has(permission.name)) throw new ValidupError([defineIssueItem({
1869
+ message: `The permission ${permission.name} is answered more than once.`,
1870
+ path: [
1871
+ "permissions",
1872
+ i,
1873
+ "name"
1874
+ ]
1875
+ })]);
1876
+ seen.add(permission.name);
1877
+ }
1878
+ return {
1879
+ permissions,
1880
+ identity
1881
+ };
1882
+ }
1883
+ //#endregion
1884
+ //#region src/permission/authorization/constants.ts
1885
+ /**
1886
+ * The node a policy tree travels as when the server withholds it, because the
1887
+ * caller's realm reach does not cover the row it was read from.
1888
+ *
1889
+ * It is deliberately a type no consumer can project: `projectAuthorizationPolicy`
1890
+ * refuses it, which is already the state a definition referencing it denies
1891
+ * through and a grant naming it is dropped through, so withholding a body needs
1892
+ * no rule of its own on the consumer side. That is a contract between the two
1893
+ * halves rather than a server detail, which is why it is declared HERE, next to
1894
+ * the projection that has to keep refusing it, and pinned by this package's own
1895
+ * test.
1896
+ */
1897
+ const AUTHORIZATION_POLICY_WITHHELD_TYPE = "withheld";
1898
+ //#endregion
1899
+ //#region src/permission/authorization/policy.ts
1900
+ /**
1901
+ * The two keys a node carries whatever its type is. Deliberately NOT the whole
1902
+ * node: every other key is that type's own configuration, which the type's own
1903
+ * validator reads, and a container strips every key it does not mount.
1904
+ */
1905
+ var AuthorizationPolicyHeadValidator = class extends Container {
1906
+ initialize() {
1907
+ super.initialize();
1908
+ this.mount("type", createValidator(z.string().min(1)));
1909
+ this.mount("children", createValidator(z.array(z.unknown()).optional()));
1910
+ }
1911
+ };
1912
+ /**
1913
+ * The one definition of what a policy looks like on the wire, used by the server
1914
+ * to build a document and by the consumer to validate one. A node is the OUTPUT
1915
+ * of its type's validator (validup strips every unmounted key, so entity columns
1916
+ * such as `id`, `name`, `builtIn` or `createdAt` never travel), plus its type
1917
+ * and, for a composite, its projected children.
1918
+ *
1919
+ * The type set is the `validators` registry, `PolicyDefaultValidators` unless
1920
+ * the caller hands another (#3635): a type the registry lacks is refused, so
1921
+ * the server and a consumer that register the same custom type let it travel,
1922
+ * and one that does not denies it. The withheld node is refused whatever the
1923
+ * registry says, since projecting it would evaluate a tree the caller was not
1924
+ * allowed to see.
1925
+ *
1926
+ * Every refusal is a `ValidupError`, the node's head and the type's own
1927
+ * validator alike, and a child's issues are rebased onto the position it sits
1928
+ * at, so one message names the node in the tree that is wrong.
1929
+ */
1930
+ async function projectAuthorizationPolicy(input, validators = PolicyDefaultValidators) {
1931
+ const headValidator = new AuthorizationPolicyHeadValidator();
1932
+ const project = async (value) => {
1933
+ const head = await headValidator.run(value);
1934
+ const validator = head.type !== "withheld" && Object.hasOwn(validators, head.type) ? validators[head.type] : void 0;
1935
+ if (!validator) throw new ValidupError([defineIssueItem({
1936
+ message: `The policy type ${head.type} is not supported.`,
1937
+ path: ["type"]
1938
+ })]);
1939
+ const output = {
1940
+ ...await validator.run(value),
1941
+ type: head.type
1942
+ };
1943
+ for (const key of Object.keys(output)) if (typeof output[key] === "undefined") delete output[key];
1944
+ if (head.type === "composite") output.children = await Promise.all((head.children ?? []).map((child, index) => projectChild(child, index)));
1945
+ else delete output.children;
1946
+ return output;
1947
+ };
1948
+ const projectChild = async (child, index) => {
1949
+ try {
1950
+ return await project(child);
1951
+ } catch (e) {
1952
+ if (isValidupError(e)) throw new ValidupError(e.issues.map((issue) => prefixIssuePath(issue, ["children", index])));
1953
+ throw e;
1954
+ }
1955
+ };
1956
+ return project(input);
1957
+ }
1958
+ /**
1959
+ * Whether a projected tree evaluates the permission binding at any depth. A
1960
+ * grant policy carrying one would re-enter its own permission's grants, so
1961
+ * `createAuthorizationEvaluator` drops the grant that names it and evaluates
1962
+ * the permission from the rest. Its one caller is that consumer: the tree is
1963
+ * legal in a definition, where the binding check is what reach is enforced
1964
+ * by, so the producer carries it and only the grant side is refused.
1965
+ */
1966
+ function containsBindingCheck(policy) {
1967
+ if (policy.type === "permissionBinding") return true;
1968
+ return (policy.children ?? []).some((child) => containsBindingCheck(child));
1969
+ }
1970
+ //#endregion
1971
+ //#region src/permission/authorization/module.ts
1972
+ /**
1973
+ * Every value `realmScopeMatches` decides, and nothing it would have to guess
1974
+ * at (#3636): a realm key, `null` for a global row, or a list of either for a
1975
+ * row spanning several realms. An empty key and an empty list are legal and
1976
+ * DENY there (no identity's realm is the empty string, and the empty set is
1977
+ * fail-closed), so refusing them here would make the consumer the one side
1978
+ * that errors where the server answers. `undefined` stays refused: the server
1979
+ * coerces it to `null`, which reads as a global row, and a caller passing an
1980
+ * absent column should hear about it rather than reach the global rows.
1981
+ */
1982
+ const realmMatchSchema = z.union([
1983
+ z.string(),
1984
+ z.array(z.string().nullable()),
1985
+ z.null()
1986
+ ]);
1987
+ /**
1988
+ * Every refusal of the DOCUMENT is a `ValidupError` carrying the path of the
1989
+ * member at fault, the shape `parseAuthorizationEvaluatorInput` raises and the
1990
+ * one a consumer can render. A bare `Error` carries neither a code nor a path,
1991
+ * so nothing downstream can normalize it.
1992
+ */
1993
+ function refuseDocument(message, path) {
1994
+ throw new ValidupError([defineIssueItem({
1995
+ message,
1996
+ path
1997
+ })]);
1998
+ }
1999
+ function refuseDocumentWithIssues(issues) {
2000
+ throw new ValidupError(issues);
2001
+ }
2002
+ function toPolicy(tree) {
2003
+ const { invert, children, ...policy } = tree;
2004
+ return {
2005
+ ...policy,
2006
+ ...typeof invert === "boolean" ? { invert } : {},
2007
+ ...children ? { children: children.map((child) => toPolicy(child)) } : {}
2008
+ };
2009
+ }
2010
+ /**
2011
+ * Build a resource-server (or console) evaluator from the catalog
2012
+ * `GET /authorization` serves, the identity's grants as the introspection
2013
+ * endpoints report them, and the identity itself. Rejects legacy, incomplete
2014
+ * and unsupported input. A grant referencing a definition or a policy the
2015
+ * catalog lacks throws `AuthorizationCatalogStaleError`, the signal to
2016
+ * refetch the catalog: the grants come from a fresh introspection while the
2017
+ * catalog may predate the definition or the junction row. A definition the
2018
+ * server could not project travels with `policies: null` instead of being
2019
+ * left out, so a grant of it is dropped rather than reported stale: the
2020
+ * definition layer cannot be evaluated here, a refetch changes nothing, and
2021
+ * the permission denies until the policy is fixed.
2022
+ *
2023
+ * A tree THIS copy cannot project is the same condition read from the other
2024
+ * side, and it is per tree: a policy type the validator registry lacks (a
2025
+ * type newer than this package, or a custom one the caller did not register)
2026
+ * or a configuration its validator refuses denies the definitions that reference
2027
+ * it, exactly as `policies: null` does, and drops a grant that names it,
2028
+ * exactly as a binding check does. One such tree never takes the evaluator
2029
+ * down for the permissions that do not use it. A malformed catalog still
2030
+ * throws: a duplicate permission namespace is not a data condition.
2031
+ *
2032
+ * Both the identity and the grants are optional. Without an identity no grant
2033
+ * is bound and any IDENTITY key the caller supplied is REMOVED, so the
2034
+ * binding evaluator answers with missing data exactly as the server does for
2035
+ * an anonymous request, only a definition whose policies need no identity can
2036
+ * pass, and a caller can never inject an identity the document did not prove.
2037
+ * Grants without the identity they belong to are refused.
2038
+ *
2039
+ * The REALM_MATCH data key follows the server's own three-way rule: a
2040
+ * resource that carries a realm passes it (null for a global row, which
2041
+ * `own` denies), a resource with no realm dimension passes nothing and reach
2042
+ * neutral-passes. `preEvaluate` / `preEvaluateOneOf` are the pre-gate and
2043
+ * enforce reach only when that key is present. `compile` returns the
2044
+ * allow/deny/conditional/post contract; a `post` collection query must be
2045
+ * rejected or evaluated over every candidate before paging. The supplied
2046
+ * identity is authoritative; `options.decisionStrategy` is forwarded and the
2047
+ * policy include, exclude and pending options are refused.
2048
+ *
2049
+ * The policy type set is open (#3635): `options.validators` decides which
2050
+ * trees project and `options.evaluators` how they evaluate, the built-in
2051
+ * registries by default. A custom type needs both, the validator the server
2052
+ * projected it with and the evaluator the server decides it with; given only
2053
+ * the first it projects and then denies. The binding evaluator is always this
2054
+ * function's own, since it is what binds the introspected grants.
2055
+ */
2056
+ async function createAuthorizationEvaluator(input, options = {}) {
2057
+ const { catalog, grants, identity } = await parseAuthorizationEvaluatorInput(input);
2058
+ const trees = /* @__PURE__ */ new Map();
2059
+ const unevaluable = /* @__PURE__ */ new Set();
2060
+ for (const [id, raw] of Object.entries(catalog.policies)) try {
2061
+ trees.set(id, await projectAuthorizationPolicy(raw, options.validators));
2062
+ } catch {
2063
+ unevaluable.add(id);
2064
+ }
2065
+ const definitions = [];
2066
+ const permissions = /* @__PURE__ */ new Map();
2067
+ for (const [index, entry] of catalog.permissions.entries()) {
2068
+ const permission = {
2069
+ name: entry.name,
2070
+ realmId: entry.realm_id,
2071
+ clientId: entry.client_id,
2072
+ decisionStrategy: entry.decision_strategy ?? void 0
2073
+ };
2074
+ const key = buildPermissionKey(permission);
2075
+ if (permissions.has(key)) refuseDocument(`The catalog defines the permission ${key} more than once.`, [
2076
+ "catalog",
2077
+ "permissions",
2078
+ index
2079
+ ]);
2080
+ if (entry.policies === null || entry.policies.some((id) => unevaluable.has(id))) {
2081
+ permissions.set(key, null);
2082
+ continue;
2083
+ }
2084
+ permissions.set(key, permission);
2085
+ const policies = [];
2086
+ for (const [i, id] of entry.policies.entries()) {
2087
+ const tree = trees.get(id);
2088
+ if (!tree) refuseDocument(`The catalog does not declare the policy ${id}.`, [
2089
+ "catalog",
2090
+ "permissions",
2091
+ index,
2092
+ "policies",
2093
+ i
2094
+ ]);
2095
+ policies.push(toPolicy(tree));
2096
+ }
2097
+ definitions.push({
2098
+ permission,
2099
+ policies: policies.length > 0 ? policies : void 0
2100
+ });
2101
+ }
2102
+ const bindings = [];
2103
+ for (const grant of grants) {
2104
+ const key = buildPermissionKey({
2105
+ name: grant.name,
2106
+ realmId: grant.realm_id ?? null,
2107
+ clientId: grant.client_id ?? null
2108
+ });
2109
+ const permission = permissions.get(key);
2110
+ if (typeof permission === "undefined") throw new AuthorizationCatalogStaleError(`The catalog does not define the permission ${key}.`);
2111
+ if (permission === null) continue;
2112
+ const policies = [];
2113
+ let evaluable = true;
2114
+ for (const id of grant.policies ?? []) {
2115
+ if (unevaluable.has(id)) {
2116
+ evaluable = false;
2117
+ break;
2118
+ }
2119
+ const tree = trees.get(id);
2120
+ if (!tree) throw new AuthorizationCatalogStaleError(`The catalog does not declare the policy ${id}.`);
2121
+ if (containsBindingCheck(tree)) {
2122
+ evaluable = false;
2123
+ break;
2124
+ }
2125
+ policies.push(toPolicy(tree));
2126
+ }
2127
+ if (!evaluable) continue;
2128
+ bindings.push({
2129
+ permission,
2130
+ realmScope: normalizeRealmScope(grant.realm_scope),
2131
+ policies: policies.length > 0 ? policies : void 0
2132
+ });
2133
+ }
2134
+ const engine = new PolicyEngine(options.evaluators ?? PolicyDefaultEvaluators);
2135
+ engine.registerEvaluator("permissionBinding", new IdentityPermissionBindingPolicyEvaluator({ getFor: async () => bindings }));
2136
+ const evaluator = new PermissionEvaluator({
2137
+ provider: new PermissionMemoryProvider(definitions),
2138
+ policyEngine: engine
2139
+ });
2140
+ const withIdentity = (input) => {
2141
+ const data = input?.clone() ?? new PolicyData();
2142
+ if (!identity) {
2143
+ data.delete("identity");
2144
+ return data;
2145
+ }
2146
+ data.set("identity", {
2147
+ id: identity.id,
2148
+ type: identity.type,
2149
+ realmId: identity.realmId ?? void 0,
2150
+ realmName: identity.realmName ?? void 0,
2151
+ clientId: identity.clientId ?? null
2152
+ });
2153
+ return data;
2154
+ };
2155
+ const forGate = (ctx) => {
2156
+ if (ctx.options?.policiesIncluded || ctx.options?.policiesExcluded || ctx.options?.pendingPolicies) throw new InternalError("The authorization evaluator does not accept policy bypass options.");
2157
+ return {
2158
+ name: ctx.name,
2159
+ realmId: ctx.realmId,
2160
+ clientId: ctx.clientId,
2161
+ data: withIdentity(ctx.data),
2162
+ ...ctx.options?.decisionStrategy ? { options: { decisionStrategy: ctx.options.decisionStrategy } } : {}
2163
+ };
2164
+ };
2165
+ const forResource = (ctx) => {
2166
+ const next = forGate(ctx);
2167
+ if (next.data.has("realmMatch")) {
2168
+ const value = next.data.get("realmMatch");
2169
+ const outcome = realmMatchSchema.safeParse(value);
2170
+ if (!outcome.success) refuseDocumentWithIssues(buildIssuesForZodError(outcome.error, value).map((issue) => prefixIssuePath(issue, ["data", "realmMatch"])));
2171
+ }
2172
+ return next;
2173
+ };
2174
+ return {
2175
+ async evaluate(ctx) {
2176
+ return evaluator.evaluate(forResource(ctx));
2177
+ },
2178
+ async evaluateOneOf(ctx) {
2179
+ return evaluator.evaluateOneOf(forResource(ctx));
2180
+ },
2181
+ async preEvaluate(ctx) {
2182
+ return evaluator.preEvaluate(forGate(ctx));
2183
+ },
2184
+ async preEvaluateOneOf(ctx) {
2185
+ return evaluator.preEvaluateOneOf(forGate(ctx));
2186
+ },
2187
+ async compile(ctx) {
2188
+ if (ctx.data?.has("realmMatch") || ctx.data?.has("attributes")) throw new InternalError("Compile authorization without resource realm or row attributes.");
2189
+ return evaluator.compile({
2190
+ name: ctx.name,
2191
+ realmId: ctx.realmId,
2192
+ clientId: ctx.clientId,
2193
+ data: withIdentity(ctx.data),
2194
+ realmAttributeName: ctx.realmAttributeName
2195
+ });
2196
+ }
2197
+ };
2198
+ }
2199
+ /**
2200
+ * An `IPermissionEvaluator` over the answer `POST /authorization/check`
2201
+ * served: the caller's own verdicts, paired with the realms they hold in.
2202
+ *
2203
+ * It is what a PUBLIC client gets where the catalog is out of reach. A public
2204
+ * client holds no secret, so it can obtain no `client_credentials` token and
2205
+ * has no credential of its own for the catalog's gate to be satisfied by;
2206
+ * publishing every policy predicate to it instead would be the wrong trade.
2207
+ * The verdicts are computed by the server from the same grants and the same
2208
+ * evaluators a request runs, so this is authoritative where the name-only
2209
+ * fallback it replaces is merely coarse.
2210
+ *
2211
+ * Two properties follow from the answer being a pre-gate:
2212
+ *
2213
+ * It is an UPPER BOUND, so `evaluate` and `evaluateOneOf` answer exactly what
2214
+ * the pre-gate pair answers. A check that depends on a resource row belongs to
2215
+ * whoever holds the row, and the server decides it there; answering such a
2216
+ * check here would mean inventing a verdict. That is the same bound the
2217
+ * name-only fallback already had, so nothing that gates on it loosens.
2218
+ *
2219
+ * And a resource realm is matched against what the REQUEST asked about: a
2220
+ * `realmMatch` naming a realm the request never carried is in no entry's list
2221
+ * and therefore denies. That is fail-closed by construction rather than by a
2222
+ * rule, and it is why a caller must ask about the realms its UI will ask
2223
+ * about.
2224
+ */
2225
+ async function createAuthorizationCheckEvaluator(input) {
2226
+ const { permissions, identity } = await parseAuthorizationCheckEvaluatorInput(input);
2227
+ const verdicts = /* @__PURE__ */ new Map();
2228
+ for (const permission of permissions) verdicts.set(permission.name, new Set(permission.realms));
2229
+ const ownKeys = /* @__PURE__ */ new Set();
2230
+ if (identity?.realmId) ownKeys.add(identity.realmId);
2231
+ if (identity?.realmName) ownKeys.add(identity.realmName);
2232
+ const matchesRealm = (realms, value) => {
2233
+ if (realms.has(value)) return true;
2234
+ if (value === null || !ownKeys.has(value)) return false;
2235
+ for (const key of ownKeys) if (realms.has(key)) return true;
2236
+ return false;
2237
+ };
2238
+ const holds = (name, data) => {
2239
+ const realms = verdicts.get(name);
2240
+ if (!realms) return false;
2241
+ if (!data || !data.has("realmMatch")) return true;
2242
+ const value = data.get("realmMatch");
2243
+ if (Array.isArray(value)) {
2244
+ if (value.length === 0) return false;
2245
+ return value.every((entry) => matchesRealm(realms, entry ?? null));
2246
+ }
2247
+ return matchesRealm(realms, value ?? null);
2248
+ };
2249
+ const names = (ctx) => Array.isArray(ctx.name) ? ctx.name : [ctx.name];
2250
+ const assertEvery = async (ctx) => {
2251
+ for (const name of names(ctx)) if (!holds(name, ctx.data)) throw PermissionError.evaluationFailed(ctx.name);
2252
+ };
2253
+ const assertSome = async (ctx) => {
2254
+ const list = names(ctx);
2255
+ for (const name of list) if (holds(name, ctx.data)) return;
2256
+ throw PermissionError.evaluationFailed(ctx.name);
2257
+ };
2258
+ return {
2259
+ evaluate: assertEvery,
2260
+ evaluateOneOf: assertSome,
2261
+ preEvaluate: assertEvery,
2262
+ preEvaluateOneOf: assertSome,
2263
+ async compile(ctx) {
2264
+ if ((Array.isArray(ctx.name) ? ctx.name : [ctx.name]).some((name) => holds(name, ctx.data))) return { verdict: "post" };
2265
+ return { verdict: "deny" };
2266
+ }
2267
+ };
2268
+ }
2269
+ //#endregion
2270
+ export { AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE, AUTHORIZATION_POLICY_WITHHELD_TYPE, AttributeNamesPolicyEvaluator, AttributeNamesPolicyValidator, AttributesPolicyEvaluator, AttributesPolicyValidator, AuthorizationCatalogStaleError, AuthorizationCheckEvaluatorInputValidator, AuthorizationEvaluatorInputValidator, BuiltInPolicyType, CompositePolicyEvaluator, CompositePolicyValidator, DatePolicyEvaluator, DatePolicyValidator, IdentityPermissionBindingPolicyEvaluator, IdentityPolicyEvaluator, IdentityPolicyValidator, PERMISSION_ERROR_INSTANCE, POLICY_ERROR_INSTANCE, PermissionBindingPolicyEvaluator, PermissionBindingPolicyValidator, PermissionError, PermissionEvaluator, PermissionMemoryProvider, PolicyData, PolicyDefaultEvaluators, PolicyDefaultValidators, PolicyEngine, PolicyError, PolicyIssueCode, PolicyIssueSeverity, REALM_SCOPE_ORDER, RealmMatchPolicyEvaluator, RealmMatchPolicyValidator, RealmScope, SystemPolicyName, TimePolicyEvaluator, TimePolicyInterval, TimePolicyValidator, aggregatePermissionPolicyBindings, authorizationCatalogSchema, authorizationCheckPermissionSchema, authorizationCheckPermissionsSchema, authorizationDefinitionSchema, authorizationGrantSchema, authorizationIdentitySchema, authorizationPolicySchema, buildPermissionKey, compareRealmScope, containsBindingCheck, createAuthorizationCheckEvaluator, createAuthorizationEvaluator, createPolicyTransitionCollector, defineAttributesPolicy, definePolicyData, definePolicyEvaluationContext, definePolicyIssueGroup, definePolicyIssueItem, definePolicyWithType, grantDominates, isAuthorizationCatalogStaleError, isCompositePolicy, isIntervalForDayOfMonth, isIntervalForDayOfWeek, isIntervalForDayOfYear, isPermissionError, isPermissionPolicyBindingEqual, isPolicyError, maxRealmScope, maybeInvertPolicyOutcome, minRealmScope, normalizeRealmScope, parseAuthorizationCheckEvaluatorInput, parseAuthorizationEvaluatorInput, projectAuthorizationPolicy, realmScopeMatches };
1463
2271
 
1464
2272
  //# sourceMappingURL=index.mjs.map