@authup/access 1.0.0-beta.64 → 1.0.0-beta.66

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 (38) hide show
  1. package/dist/index.d.mts +560 -149
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +715 -27
  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 +70 -0
  16. package/dist/permission/authorization/module.d.ts.map +1 -0
  17. package/dist/permission/authorization/policy.d.ts +23 -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 +122 -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 +8 -0
  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/policy/built-in/permission-binding/identity-evaluator.d.ts +21 -0
  29. package/dist/policy/built-in/permission-binding/identity-evaluator.d.ts.map +1 -0
  30. package/dist/policy/built-in/permission-binding/index.d.ts +1 -0
  31. package/dist/policy/built-in/permission-binding/index.d.ts.map +1 -1
  32. package/dist/policy/built-in/permission-binding/validator.d.ts.map +1 -1
  33. package/dist/policy/data.d.ts +2 -0
  34. package/dist/policy/data.d.ts.map +1 -1
  35. package/dist/policy/evaluation/context/define.d.ts.map +1 -1
  36. package/dist/policy/evaluation/context/types.d.ts +9 -0
  37. package/dist/policy/evaluation/context/types.d.ts.map +1 -1
  38. package/package.json +5 -5
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,8 @@ 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
538
543
  };
539
544
  }
540
545
  //#endregion
@@ -987,25 +992,6 @@ var IdentityPolicyEvaluator = class {
987
992
  }
988
993
  };
989
994
  //#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
995
  //#region src/policy/built-in/realm-match/validator.ts
1010
996
  var RealmMatchPolicyValidator = class extends Container {
1011
997
  initialize() {
@@ -1062,7 +1048,7 @@ var RealmMatchPolicyEvaluator = class {
1062
1048
  else {
1063
1049
  const terms = [];
1064
1050
  if (realmId !== null && scope !== "none") terms.push(eq(field, realmId));
1065
- if (scope === "ownOrNull") terms.push(eq(field, null));
1051
+ if (scope === "ownOrNull" && (realmId !== null || identity.realmName != null)) terms.push(eq(field, null));
1066
1052
  if (terms.length === 0) condition = inArray(field, []);
1067
1053
  else if (terms.length === 1) condition = terms[0];
1068
1054
  else condition = or(...terms);
@@ -1161,6 +1147,145 @@ var RealmMatchPolicyEvaluator = class {
1161
1147
  }
1162
1148
  };
1163
1149
  //#endregion
1150
+ //#region src/policy/built-in/permission-binding/validator.ts
1151
+ var PermissionBindingPolicyValidator = class extends Container {
1152
+ initialize() {
1153
+ this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
1154
+ }
1155
+ };
1156
+ //#endregion
1157
+ //#region src/policy/built-in/permission-binding/identity-evaluator.ts
1158
+ var IdentityPermissionBindingPolicyEvaluator = class {
1159
+ validator;
1160
+ identityEvaluator;
1161
+ realmMatchEvaluator;
1162
+ identityPermissionProvider;
1163
+ constructor(identityPermissionProvider) {
1164
+ this.validator = new PermissionBindingPolicyValidator();
1165
+ this.identityEvaluator = new IdentityPolicyEvaluator();
1166
+ this.realmMatchEvaluator = new RealmMatchPolicyEvaluator();
1167
+ this.identityPermissionProvider = identityPermissionProvider;
1168
+ }
1169
+ requires() {
1170
+ return ["permissionBinding"];
1171
+ }
1172
+ async accessData(ctx) {
1173
+ if (!ctx.data.has("permissionBinding")) return null;
1174
+ if (ctx.data.isValidated("permissionBinding")) return ctx.data.get("permissionBinding");
1175
+ const data = ctx.data.get("permissionBinding");
1176
+ ctx.data.set("permissionBinding", data);
1177
+ ctx.data.setValidated("permissionBinding");
1178
+ return data;
1179
+ }
1180
+ async evaluate(value, ctx) {
1181
+ const policy = await this.validator.run(value);
1182
+ const identity = await this.identityEvaluator.accessData(ctx);
1183
+ if (!identity) return {
1184
+ success: false,
1185
+ issues: [definePolicyIssueItem({
1186
+ code: "DATA_MISSING",
1187
+ message: "The data property identity is missing",
1188
+ path: ctx.path
1189
+ })]
1190
+ };
1191
+ const binding = await this.accessData(ctx);
1192
+ if (!binding) return {
1193
+ success: false,
1194
+ issues: [definePolicyIssueItem({
1195
+ code: "DATA_MISSING",
1196
+ message: "The data property permission is missing",
1197
+ path: ctx.path
1198
+ })]
1199
+ };
1200
+ const identityBindings = await this.identityPermissionProvider.getFor(identity).then((bindings) => bindings.filter((item) => {
1201
+ if (item.permission.name !== binding.permission.name) return false;
1202
+ return (binding.permission.realmId ?? null) === (item.permission.realmId ?? null) && (binding.permission.clientId ?? null) === (item.permission.clientId ?? null);
1203
+ }));
1204
+ if (identityBindings.length === 0) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1205
+ const [aggr] = aggregatePermissionPolicyBindings(identityBindings);
1206
+ if (!aggr) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1207
+ const issues = [];
1208
+ const pendingIssues = [];
1209
+ const conditions = [];
1210
+ let pending = false;
1211
+ let lowerable = true;
1212
+ for (const grant of aggr.grants) {
1213
+ let reachCondition = null;
1214
+ const realmOutcome = await this.realmMatchEvaluator.evaluate({
1215
+ scope: grant.realmScope,
1216
+ attributeName: ctx.realmAttributeName
1217
+ }, ctx);
1218
+ if (realmOutcome.pending) {
1219
+ const scope = normalizeRealmScope(grant.realmScope);
1220
+ if (scope === "none") continue;
1221
+ if (scope !== "any") {
1222
+ if (!realmOutcome.condition) {
1223
+ pending = true;
1224
+ lowerable = false;
1225
+ continue;
1226
+ }
1227
+ reachCondition = realmOutcome.condition;
1228
+ }
1229
+ } else if (!realmOutcome.success) continue;
1230
+ if (!grant.policy) {
1231
+ if (!reachCondition) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
1232
+ conditions.push(reachCondition);
1233
+ pending = true;
1234
+ continue;
1235
+ }
1236
+ const outcome = await new PolicyEngine(ctx.evaluators).evaluate(grant.policy, {
1237
+ ...ctx,
1238
+ path: [...ctx.path || []]
1239
+ });
1240
+ if (outcome.success) {
1241
+ if (!reachCondition) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
1242
+ conditions.push(reachCondition);
1243
+ pending = true;
1244
+ continue;
1245
+ }
1246
+ if (outcome.pending) {
1247
+ pending = true;
1248
+ if (ctx.withConditions && outcome.condition && !ctx.realmAttributeName) conditions.push(reachCondition ? and(reachCondition, outcome.condition) : outcome.condition);
1249
+ else lowerable = false;
1250
+ if (outcome.issues) pendingIssues.push(...outcome.issues);
1251
+ continue;
1252
+ }
1253
+ if (outcome.issues) issues.push(...outcome.issues);
1254
+ }
1255
+ if (pending) {
1256
+ const result = {
1257
+ success: false,
1258
+ pending: true,
1259
+ issues: pendingIssues
1260
+ };
1261
+ if (lowerable && ctx.withConditions && conditions.length > 0) {
1262
+ const condition = conditions.length === 1 ? conditions[0] : or(...conditions);
1263
+ result.condition = policy.invert ? not(condition) : condition;
1264
+ }
1265
+ return result;
1266
+ }
1267
+ const success = maybeInvertPolicyOutcome(false, policy.invert);
1268
+ return {
1269
+ success,
1270
+ issues: success ? [] : issues
1271
+ };
1272
+ }
1273
+ };
1274
+ //#endregion
1275
+ //#region src/policy/built-in/permission-binding/module.ts
1276
+ var PermissionBindingPolicyEvaluator = class {
1277
+ validator;
1278
+ constructor() {
1279
+ this.validator = new PermissionBindingPolicyValidator();
1280
+ }
1281
+ requires() {
1282
+ return ["permissionBinding"];
1283
+ }
1284
+ async evaluate(value) {
1285
+ return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
1286
+ }
1287
+ };
1288
+ //#endregion
1164
1289
  //#region src/policy/built-in/time/constants.ts
1165
1290
  let TimePolicyInterval = /* @__PURE__ */ function(TimePolicyInterval) {
1166
1291
  TimePolicyInterval["DAILY"] = "daily";
@@ -1435,7 +1560,8 @@ var PermissionEvaluator = class {
1435
1560
  };
1436
1561
  const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
1437
1562
  data,
1438
- withConditions: true
1563
+ withConditions: true,
1564
+ realmAttributeName: ctx.realmAttributeName
1439
1565
  }));
1440
1566
  if (evaluationResult.success) return { verdict: "allow" };
1441
1567
  if (evaluationResult.pending) {
@@ -1459,6 +1585,568 @@ var PermissionEvaluator = class {
1459
1585
  }
1460
1586
  };
1461
1587
  //#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 };
1588
+ //#region src/permission/authorization/error.ts
1589
+ const AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE = Symbol.for("@authup/access/AuthorizationCatalogStaleError");
1590
+ /**
1591
+ * A grant references a definition or a policy the supplied catalog does not
1592
+ * carry: the caller's cached catalog predates the definition or the junction
1593
+ * row and must be refetched. A definition the catalog carries with
1594
+ * `policies: null`, and one whose tree this copy cannot project, are not
1595
+ * stale but unevaluable, and a grant of either is dropped.
1596
+ *
1597
+ * Recognize it with `isAuthorizationCatalogStaleError`, never `instanceof`:
1598
+ * this package is a plain caret dependency of several workspaces, so a
1599
+ * consumer tree may resolve two copies of it and turn the one recoverable
1600
+ * state into an unrecoverable one.
1601
+ */
1602
+ var AuthorizationCatalogStaleError = class extends AuthupError {
1603
+ constructor(message) {
1604
+ super({ message });
1605
+ this.name = "AuthorizationCatalogStaleError";
1606
+ markInstanceof(this, AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE);
1607
+ }
1608
+ };
1609
+ //#endregion
1610
+ //#region src/permission/authorization/check.ts
1611
+ /**
1612
+ * Marker-only: the error carries no code of its own, and it never crosses a
1613
+ * wire boundary, so there is no shape to fall back to.
1614
+ */
1615
+ function isAuthorizationCatalogStaleError(input) {
1616
+ return matchesInstanceof(input, AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE);
1617
+ }
1618
+ //#endregion
1619
+ //#region src/permission/authorization/schema.ts
1620
+ const id = z.string().min(1);
1621
+ const namespaceId = id.nullable();
1622
+ /**
1623
+ * A policy node as it travels. Deliberately LOOSE: every key beyond `type` is
1624
+ * that type's own configuration, and `projectAuthorizationPolicy` is what
1625
+ * validates it, by running the type's own validator. Mounting a node in a
1626
+ * validup container would strip every key the container does not mount, and
1627
+ * the whole configuration would go with it.
1628
+ */
1629
+ const authorizationPolicySchema = z.looseObject({ type: z.string().min(1) });
1630
+ /**
1631
+ * One permission definition. `policies` carries the ids of its
1632
+ * definition-layer trees, or `null` when the server could not project one.
1633
+ */
1634
+ const authorizationDefinitionSchema = z.object({
1635
+ name: z.string().min(1),
1636
+ realm_id: namespaceId,
1637
+ client_id: namespaceId,
1638
+ decision_strategy: z.enum(DecisionStrategy).nullable(),
1639
+ policies: z.array(id).nullable()
1640
+ });
1641
+ const authorizationCatalogSchema = z.object({
1642
+ policies: z.record(id, authorizationPolicySchema),
1643
+ permissions: z.array(authorizationDefinitionSchema)
1644
+ });
1645
+ /**
1646
+ * One grant of the identity, as the introspection endpoints report it.
1647
+ * Whether a grant's policy ids are declared is checked against the catalog
1648
+ * the consumer holds, since the two travel separately.
1649
+ */
1650
+ const authorizationGrantSchema = z.object({
1651
+ name: z.string().min(1),
1652
+ realm_id: namespaceId.optional(),
1653
+ client_id: namespaceId.optional(),
1654
+ realm_scope: z.enum(RealmScope).nullish(),
1655
+ policies: z.array(id).nullish()
1656
+ });
1657
+ const authorizationIdentitySchema = z.object({
1658
+ id: z.string().min(1),
1659
+ type: z.enum(["user", "client"]),
1660
+ realmId: z.string().nullish(),
1661
+ realmName: z.string().nullish(),
1662
+ clientId: z.string().nullish()
1663
+ });
1664
+ /**
1665
+ * The whole input `createAuthorizationEvaluator` takes: the catalog
1666
+ * `GET /authorization` serves, the grants an introspection reports and the
1667
+ * identity it names. A validup container over zod mounts, the shape every
1668
+ * validator in this package uses, so a consumer can reuse or override one
1669
+ * member's rules instead of restating the document.
1670
+ */
1671
+ var AuthorizationEvaluatorInputValidator = class extends Container {
1672
+ initialize() {
1673
+ super.initialize();
1674
+ this.mount("catalog", createValidator(authorizationCatalogSchema));
1675
+ this.mount("grants", createValidator(z.array(authorizationGrantSchema).optional()));
1676
+ this.mount("identity", createValidator(authorizationIdentitySchema.optional()));
1677
+ }
1678
+ };
1679
+ const validator = new AuthorizationEvaluatorInputValidator();
1680
+ /**
1681
+ * Validate the evaluator's input, plus the two rules per-key validators
1682
+ * cannot express.
1683
+ *
1684
+ * Every id a definition names must be a key of `policies`: a dangling id
1685
+ * resolves to `undefined`, which reads as "this layer carries no policy", so
1686
+ * a catalog that merely omitted a tree would grant unrestricted access rather
1687
+ * than fail. The lookup is own-property only, so an id such as `constructor`
1688
+ * cannot be answered by a member of `Object.prototype`.
1689
+ *
1690
+ * And the identity and its grants travel together: grants without an identity
1691
+ * belong to nobody, while an identity without a grant list is the shape an
1692
+ * INACTIVE introspection produces (`permissions` is absent unless the
1693
+ * credential is active), which read as "holds nothing" would authorize every
1694
+ * definition that carries no binding check. An identity holding no grant says
1695
+ * so with an empty array.
1696
+ */
1697
+ async function parseAuthorizationEvaluatorInput(input) {
1698
+ const identityGiven = typeof input.identity !== "undefined";
1699
+ const grantsGiven = typeof input.grants !== "undefined";
1700
+ if (!identityGiven && grantsGiven) throw new ValidupError([defineIssueItem({
1701
+ message: "Grants require the identity they belong to.",
1702
+ path: ["identity"]
1703
+ })]);
1704
+ if (identityGiven && !grantsGiven) throw new ValidupError([defineIssueItem({
1705
+ message: "An identity requires its grant list; pass an empty array for an identity holding none.",
1706
+ path: ["grants"]
1707
+ })]);
1708
+ const { catalog, grants, identity } = await validator.run(structuredClone(input));
1709
+ for (const [i, definition] of catalog.permissions.entries()) for (const [j, policyId] of (definition.policies ?? []).entries()) {
1710
+ if (Object.hasOwn(catalog.policies, policyId)) continue;
1711
+ throw new ValidupError([defineIssueItem({
1712
+ message: `The policy ${policyId} is not declared by the catalog.`,
1713
+ path: [
1714
+ "catalog",
1715
+ "permissions",
1716
+ i,
1717
+ "policies",
1718
+ j
1719
+ ]
1720
+ })]);
1721
+ }
1722
+ return {
1723
+ catalog,
1724
+ grants: grants ?? [],
1725
+ identity
1726
+ };
1727
+ }
1728
+ /**
1729
+ * One verdict: the permission namespace and the requested realms it holds in.
1730
+ * A realm value is a realm key the caller itself supplied, or `null` for the
1731
+ * global rows, so nothing here is resolved or looked up.
1732
+ */
1733
+ const authorizationCheckPermissionSchema = z.object({
1734
+ name: z.string().min(1),
1735
+ realms: z.array(z.string().min(1).nullable()).min(1)
1736
+ });
1737
+ const authorizationCheckPermissionsSchema = z.array(authorizationCheckPermissionSchema);
1738
+ //#endregion
1739
+ //#region src/permission/authorization/check-schema.ts
1740
+ /**
1741
+ * The whole input `createAuthorizationCheckEvaluator` takes: the answer
1742
+ * `POST /authorization/check` served and the identity it belongs to. Same
1743
+ * validup-over-zod shape as the catalog's input, so a consumer can override
1744
+ * one member's rules instead of restating the answer.
1745
+ */
1746
+ var AuthorizationCheckEvaluatorInputValidator = class extends Container {
1747
+ initialize() {
1748
+ super.initialize();
1749
+ this.mount("permissions", createValidator(authorizationCheckPermissionsSchema));
1750
+ this.mount("identity", createValidator(authorizationIdentitySchema.optional()));
1751
+ }
1752
+ };
1753
+ const checkValidator = new AuthorizationCheckEvaluatorInputValidator();
1754
+ /**
1755
+ * Validate the check evaluator's input, plus the one rule a per-key validator
1756
+ * cannot express: a permission namespace appears at most once. Two entries for
1757
+ * one name would make the answer depend on which the consumer looked up first,
1758
+ * and the server emits one entry per name by construction.
1759
+ */
1760
+ async function parseAuthorizationCheckEvaluatorInput(input) {
1761
+ const { permissions, identity } = await checkValidator.run(structuredClone(input));
1762
+ const seen = /* @__PURE__ */ new Set();
1763
+ for (const [i, permission] of permissions.entries()) {
1764
+ if (seen.has(permission.name)) throw new ValidupError([defineIssueItem({
1765
+ message: `The permission ${permission.name} is answered more than once.`,
1766
+ path: [
1767
+ "permissions",
1768
+ i,
1769
+ "name"
1770
+ ]
1771
+ })]);
1772
+ seen.add(permission.name);
1773
+ }
1774
+ return {
1775
+ permissions,
1776
+ identity
1777
+ };
1778
+ }
1779
+ //#endregion
1780
+ //#region src/permission/authorization/constants.ts
1781
+ /**
1782
+ * The node a policy tree travels as when the server withholds it, because the
1783
+ * caller's realm reach does not cover the row it was read from.
1784
+ *
1785
+ * It is deliberately a type no consumer can project: `projectAuthorizationPolicy`
1786
+ * refuses it, which is already the state a definition referencing it denies
1787
+ * through and a grant naming it is dropped through, so withholding a body needs
1788
+ * no rule of its own on the consumer side. That is a contract between the two
1789
+ * halves rather than a server detail, which is why it is declared HERE, next to
1790
+ * the projection that has to keep refusing it, and pinned by this package's own
1791
+ * test.
1792
+ */
1793
+ const AUTHORIZATION_POLICY_WITHHELD_TYPE = "withheld";
1794
+ //#endregion
1795
+ //#region src/permission/authorization/policy.ts
1796
+ /**
1797
+ * The two keys a node carries whatever its type is. Deliberately NOT the whole
1798
+ * node: every other key is that type's own configuration, which the type's own
1799
+ * validator reads, and a container strips every key it does not mount.
1800
+ */
1801
+ var AuthorizationPolicyHeadValidator = class extends Container {
1802
+ initialize() {
1803
+ super.initialize();
1804
+ this.mount("type", createValidator(z.enum(BuiltInPolicyType)));
1805
+ this.mount("children", createValidator(z.array(z.unknown()).optional()));
1806
+ }
1807
+ };
1808
+ function createValidators() {
1809
+ return {
1810
+ ["attributes"]: new AttributesPolicyValidator(),
1811
+ ["attributeNames"]: new AttributeNamesPolicyValidator(),
1812
+ ["composite"]: new CompositePolicyValidator(),
1813
+ ["date"]: new DatePolicyValidator(),
1814
+ ["time"]: new TimePolicyValidator(),
1815
+ ["identity"]: new IdentityPolicyValidator(),
1816
+ ["permissionBinding"]: new PermissionBindingPolicyValidator(),
1817
+ ["realmMatch"]: new RealmMatchPolicyValidator()
1818
+ };
1819
+ }
1820
+ /**
1821
+ * The one definition of what a policy looks like on the wire, used by the server
1822
+ * to build a document and by the consumer to validate one. A node is the OUTPUT
1823
+ * of its type's validator (validup strips every unmounted key, so entity columns
1824
+ * such as `id`, `name`, `builtIn` or `createdAt` never travel), plus its type
1825
+ * and, for a composite, its projected children.
1826
+ *
1827
+ * Every refusal is a `ValidupError`, the node's head and the type's own
1828
+ * validator alike, and a child's issues are rebased onto the position it sits
1829
+ * at, so one message names the node in the tree that is wrong.
1830
+ */
1831
+ async function projectAuthorizationPolicy(input) {
1832
+ const headValidator = new AuthorizationPolicyHeadValidator();
1833
+ const validators = createValidators();
1834
+ const project = async (value) => {
1835
+ const head = await headValidator.run(value);
1836
+ const output = {
1837
+ ...await validators[head.type].run(value),
1838
+ type: head.type
1839
+ };
1840
+ for (const key of Object.keys(output)) if (typeof output[key] === "undefined") delete output[key];
1841
+ if (head.type === "composite") output.children = await Promise.all((head.children ?? []).map((child, index) => projectChild(child, index)));
1842
+ else delete output.children;
1843
+ return output;
1844
+ };
1845
+ const projectChild = async (child, index) => {
1846
+ try {
1847
+ return await project(child);
1848
+ } catch (e) {
1849
+ if (isValidupError(e)) throw new ValidupError(e.issues.map((issue) => prefixIssuePath(issue, ["children", index])));
1850
+ throw e;
1851
+ }
1852
+ };
1853
+ return project(input);
1854
+ }
1855
+ /**
1856
+ * Whether a projected tree evaluates the permission binding at any depth. A
1857
+ * grant policy carrying one would re-enter its own permission's grants, so
1858
+ * `createAuthorizationEvaluator` drops the grant that names it and evaluates
1859
+ * the permission from the rest. Its one caller is that consumer: the tree is
1860
+ * legal in a definition, where the binding check is what reach is enforced
1861
+ * by, so the producer carries it and only the grant side is refused.
1862
+ */
1863
+ function containsBindingCheck(policy) {
1864
+ if (policy.type === "permissionBinding") return true;
1865
+ return (policy.children ?? []).some((child) => containsBindingCheck(child));
1866
+ }
1867
+ //#endregion
1868
+ //#region src/permission/authorization/module.ts
1869
+ const realmMatchSchema = z.union([
1870
+ z.string().min(1),
1871
+ z.array(z.string().min(1)).min(1),
1872
+ z.null()
1873
+ ]);
1874
+ /**
1875
+ * Every refusal of the DOCUMENT is a `ValidupError` carrying the path of the
1876
+ * member at fault, the shape `parseAuthorizationEvaluatorInput` raises and the
1877
+ * one a consumer can render. A bare `Error` carries neither a code nor a path,
1878
+ * so nothing downstream can normalize it.
1879
+ */
1880
+ function refuseDocument(message, path) {
1881
+ throw new ValidupError([defineIssueItem({
1882
+ message,
1883
+ path
1884
+ })]);
1885
+ }
1886
+ function refuseDocumentWithIssues(issues) {
1887
+ throw new ValidupError(issues);
1888
+ }
1889
+ function toPolicy(tree) {
1890
+ const { invert, children, ...policy } = tree;
1891
+ return {
1892
+ ...policy,
1893
+ ...typeof invert === "boolean" ? { invert } : {},
1894
+ ...children ? { children: children.map((child) => toPolicy(child)) } : {}
1895
+ };
1896
+ }
1897
+ /**
1898
+ * Build a resource-server (or console) evaluator from the catalog
1899
+ * `GET /authorization` serves, the identity's grants as the introspection
1900
+ * endpoints report them, and the identity itself. Rejects legacy, incomplete
1901
+ * and unsupported input. A grant referencing a definition or a policy the
1902
+ * catalog lacks throws `AuthorizationCatalogStaleError`, the signal to
1903
+ * refetch the catalog: the grants come from a fresh introspection while the
1904
+ * catalog may predate the definition or the junction row. A definition the
1905
+ * server could not project travels with `policies: null` instead of being
1906
+ * left out, so a grant of it is dropped rather than reported stale: the
1907
+ * definition layer cannot be evaluated here, a refetch changes nothing, and
1908
+ * the permission denies until the policy is fixed.
1909
+ *
1910
+ * A tree THIS copy cannot project is the same condition read from the other
1911
+ * side, and it is per tree: a policy type newer than this package (the
1912
+ * built-in type enum is closed while the column is a free string) or a
1913
+ * configuration its validator refuses denies the definitions that reference
1914
+ * it, exactly as `policies: null` does, and drops a grant that names it,
1915
+ * exactly as a binding check does. One such tree never takes the evaluator
1916
+ * down for the permissions that do not use it. A malformed catalog still
1917
+ * throws: a duplicate permission namespace is not a data condition.
1918
+ *
1919
+ * Both the identity and the grants are optional. Without an identity no grant
1920
+ * is bound and any IDENTITY key the caller supplied is REMOVED, so the
1921
+ * binding evaluator answers with missing data exactly as the server does for
1922
+ * an anonymous request, only a definition whose policies need no identity can
1923
+ * pass, and a caller can never inject an identity the document did not prove.
1924
+ * Grants without the identity they belong to are refused.
1925
+ *
1926
+ * The REALM_MATCH data key follows the server's own three-way rule: a
1927
+ * resource that carries a realm passes it (null for a global row, which
1928
+ * `own` denies), a resource with no realm dimension passes nothing and reach
1929
+ * neutral-passes. `preEvaluate` / `preEvaluateOneOf` are the pre-gate and
1930
+ * enforce reach only when that key is present. `compile` returns the
1931
+ * allow/deny/conditional/post contract; a `post` collection query must be
1932
+ * rejected or evaluated over every candidate before paging. The supplied
1933
+ * identity is authoritative; `options.decisionStrategy` is forwarded and the
1934
+ * policy include, exclude and pending options are refused.
1935
+ */
1936
+ async function createAuthorizationEvaluator(input) {
1937
+ const { catalog, grants, identity } = await parseAuthorizationEvaluatorInput(input);
1938
+ const trees = /* @__PURE__ */ new Map();
1939
+ const unevaluable = /* @__PURE__ */ new Set();
1940
+ for (const [id, raw] of Object.entries(catalog.policies)) try {
1941
+ trees.set(id, await projectAuthorizationPolicy(raw));
1942
+ } catch {
1943
+ unevaluable.add(id);
1944
+ }
1945
+ const definitions = [];
1946
+ const permissions = /* @__PURE__ */ new Map();
1947
+ for (const [index, entry] of catalog.permissions.entries()) {
1948
+ const permission = {
1949
+ name: entry.name,
1950
+ realmId: entry.realm_id,
1951
+ clientId: entry.client_id,
1952
+ decisionStrategy: entry.decision_strategy ?? void 0
1953
+ };
1954
+ const key = buildPermissionKey(permission);
1955
+ if (permissions.has(key)) refuseDocument(`The catalog defines the permission ${key} more than once.`, [
1956
+ "catalog",
1957
+ "permissions",
1958
+ index
1959
+ ]);
1960
+ if (entry.policies === null || entry.policies.some((id) => unevaluable.has(id))) {
1961
+ permissions.set(key, null);
1962
+ continue;
1963
+ }
1964
+ permissions.set(key, permission);
1965
+ const policies = [];
1966
+ for (const [i, id] of entry.policies.entries()) {
1967
+ const tree = trees.get(id);
1968
+ if (!tree) refuseDocument(`The catalog does not declare the policy ${id}.`, [
1969
+ "catalog",
1970
+ "permissions",
1971
+ index,
1972
+ "policies",
1973
+ i
1974
+ ]);
1975
+ policies.push(toPolicy(tree));
1976
+ }
1977
+ definitions.push({
1978
+ permission,
1979
+ policies: policies.length > 0 ? policies : void 0
1980
+ });
1981
+ }
1982
+ const bindings = [];
1983
+ for (const grant of grants) {
1984
+ const key = buildPermissionKey({
1985
+ name: grant.name,
1986
+ realmId: grant.realm_id ?? null,
1987
+ clientId: grant.client_id ?? null
1988
+ });
1989
+ const permission = permissions.get(key);
1990
+ if (typeof permission === "undefined") throw new AuthorizationCatalogStaleError(`The catalog does not define the permission ${key}.`);
1991
+ if (permission === null) continue;
1992
+ const policies = [];
1993
+ let evaluable = true;
1994
+ for (const id of grant.policies ?? []) {
1995
+ if (unevaluable.has(id)) {
1996
+ evaluable = false;
1997
+ break;
1998
+ }
1999
+ const tree = trees.get(id);
2000
+ if (!tree) throw new AuthorizationCatalogStaleError(`The catalog does not declare the policy ${id}.`);
2001
+ if (containsBindingCheck(tree)) {
2002
+ evaluable = false;
2003
+ break;
2004
+ }
2005
+ policies.push(toPolicy(tree));
2006
+ }
2007
+ if (!evaluable) continue;
2008
+ bindings.push({
2009
+ permission,
2010
+ realmScope: normalizeRealmScope(grant.realm_scope),
2011
+ policies: policies.length > 0 ? policies : void 0
2012
+ });
2013
+ }
2014
+ const engine = new PolicyEngine(PolicyDefaultEvaluators);
2015
+ engine.registerEvaluator("permissionBinding", new IdentityPermissionBindingPolicyEvaluator({ getFor: async () => bindings }));
2016
+ const evaluator = new PermissionEvaluator({
2017
+ provider: new PermissionMemoryProvider(definitions),
2018
+ policyEngine: engine
2019
+ });
2020
+ const withIdentity = (input) => {
2021
+ const data = input?.clone() ?? new PolicyData();
2022
+ if (!identity) {
2023
+ data.delete("identity");
2024
+ return data;
2025
+ }
2026
+ data.set("identity", {
2027
+ id: identity.id,
2028
+ type: identity.type,
2029
+ realmId: identity.realmId ?? void 0,
2030
+ realmName: identity.realmName ?? void 0,
2031
+ clientId: identity.clientId ?? null
2032
+ });
2033
+ return data;
2034
+ };
2035
+ const forGate = (ctx) => {
2036
+ if (ctx.options?.policiesIncluded || ctx.options?.policiesExcluded || ctx.options?.pendingPolicies) throw new InternalError("The authorization evaluator does not accept policy bypass options.");
2037
+ return {
2038
+ name: ctx.name,
2039
+ realmId: ctx.realmId,
2040
+ clientId: ctx.clientId,
2041
+ data: withIdentity(ctx.data),
2042
+ ...ctx.options?.decisionStrategy ? { options: { decisionStrategy: ctx.options.decisionStrategy } } : {}
2043
+ };
2044
+ };
2045
+ const forResource = (ctx) => {
2046
+ const next = forGate(ctx);
2047
+ if (next.data.has("realmMatch")) {
2048
+ const value = next.data.get("realmMatch");
2049
+ const outcome = realmMatchSchema.safeParse(value);
2050
+ if (!outcome.success) refuseDocumentWithIssues(buildIssuesForZodError(outcome.error, value).map((issue) => prefixIssuePath(issue, ["data", "realmMatch"])));
2051
+ }
2052
+ return next;
2053
+ };
2054
+ return {
2055
+ async evaluate(ctx) {
2056
+ return evaluator.evaluate(forResource(ctx));
2057
+ },
2058
+ async evaluateOneOf(ctx) {
2059
+ return evaluator.evaluateOneOf(forResource(ctx));
2060
+ },
2061
+ async preEvaluate(ctx) {
2062
+ return evaluator.preEvaluate(forGate(ctx));
2063
+ },
2064
+ async preEvaluateOneOf(ctx) {
2065
+ return evaluator.preEvaluateOneOf(forGate(ctx));
2066
+ },
2067
+ async compile(ctx) {
2068
+ if (ctx.data?.has("realmMatch") || ctx.data?.has("attributes")) throw new InternalError("Compile authorization without resource realm or row attributes.");
2069
+ return evaluator.compile({
2070
+ name: ctx.name,
2071
+ realmId: ctx.realmId,
2072
+ clientId: ctx.clientId,
2073
+ data: withIdentity(ctx.data),
2074
+ realmAttributeName: ctx.realmAttributeName
2075
+ });
2076
+ }
2077
+ };
2078
+ }
2079
+ /**
2080
+ * An `IPermissionEvaluator` over the answer `POST /authorization/check`
2081
+ * served: the caller's own verdicts, paired with the realms they hold in.
2082
+ *
2083
+ * It is what a PUBLIC client gets where the catalog is out of reach. A public
2084
+ * client holds no secret, so it can obtain no `client_credentials` token and
2085
+ * has no credential of its own for the catalog's gate to be satisfied by;
2086
+ * publishing every policy predicate to it instead would be the wrong trade.
2087
+ * The verdicts are computed by the server from the same grants and the same
2088
+ * evaluators a request runs, so this is authoritative where the name-only
2089
+ * fallback it replaces is merely coarse.
2090
+ *
2091
+ * Two properties follow from the answer being a pre-gate:
2092
+ *
2093
+ * It is an UPPER BOUND, so `evaluate` and `evaluateOneOf` answer exactly what
2094
+ * the pre-gate pair answers. A check that depends on a resource row belongs to
2095
+ * whoever holds the row, and the server decides it there; answering such a
2096
+ * check here would mean inventing a verdict. That is the same bound the
2097
+ * name-only fallback already had, so nothing that gates on it loosens.
2098
+ *
2099
+ * And a resource realm is matched against what the REQUEST asked about: a
2100
+ * `realmMatch` naming a realm the request never carried is in no entry's list
2101
+ * and therefore denies. That is fail-closed by construction rather than by a
2102
+ * rule, and it is why a caller must ask about the realms its UI will ask
2103
+ * about.
2104
+ */
2105
+ async function createAuthorizationCheckEvaluator(input) {
2106
+ const { permissions, identity } = await parseAuthorizationCheckEvaluatorInput(input);
2107
+ const verdicts = /* @__PURE__ */ new Map();
2108
+ for (const permission of permissions) verdicts.set(permission.name, new Set(permission.realms));
2109
+ const ownKeys = /* @__PURE__ */ new Set();
2110
+ if (identity?.realmId) ownKeys.add(identity.realmId);
2111
+ if (identity?.realmName) ownKeys.add(identity.realmName);
2112
+ const matchesRealm = (realms, value) => {
2113
+ if (realms.has(value)) return true;
2114
+ if (value === null || !ownKeys.has(value)) return false;
2115
+ for (const key of ownKeys) if (realms.has(key)) return true;
2116
+ return false;
2117
+ };
2118
+ const holds = (name, data) => {
2119
+ const realms = verdicts.get(name);
2120
+ if (!realms) return false;
2121
+ if (!data || !data.has("realmMatch")) return true;
2122
+ const value = data.get("realmMatch");
2123
+ if (Array.isArray(value)) {
2124
+ if (value.length === 0) return false;
2125
+ return value.every((entry) => matchesRealm(realms, entry ?? null));
2126
+ }
2127
+ return matchesRealm(realms, value ?? null);
2128
+ };
2129
+ const names = (ctx) => Array.isArray(ctx.name) ? ctx.name : [ctx.name];
2130
+ const assertEvery = async (ctx) => {
2131
+ for (const name of names(ctx)) if (!holds(name, ctx.data)) throw PermissionError.evaluationFailed(ctx.name);
2132
+ };
2133
+ const assertSome = async (ctx) => {
2134
+ const list = names(ctx);
2135
+ for (const name of list) if (holds(name, ctx.data)) return;
2136
+ throw PermissionError.evaluationFailed(ctx.name);
2137
+ };
2138
+ return {
2139
+ evaluate: assertEvery,
2140
+ evaluateOneOf: assertSome,
2141
+ preEvaluate: assertEvery,
2142
+ preEvaluateOneOf: assertSome,
2143
+ async compile(ctx) {
2144
+ if ((Array.isArray(ctx.name) ? ctx.name : [ctx.name]).some((name) => holds(name, ctx.data))) return { verdict: "post" };
2145
+ return { verdict: "deny" };
2146
+ }
2147
+ };
2148
+ }
2149
+ //#endregion
2150
+ 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, 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, defineAttributesPolicy, definePolicyData, definePolicyEvaluationContext, definePolicyIssueGroup, definePolicyIssueItem, definePolicyWithType, grantDominates, isAuthorizationCatalogStaleError, isCompositePolicy, isIntervalForDayOfMonth, isIntervalForDayOfWeek, isIntervalForDayOfYear, isPermissionError, isPermissionPolicyBindingEqual, isPolicyError, maxRealmScope, maybeInvertPolicyOutcome, minRealmScope, normalizeRealmScope, parseAuthorizationCheckEvaluatorInput, parseAuthorizationEvaluatorInput, projectAuthorizationPolicy, realmScopeMatches };
1463
2151
 
1464
2152
  //# sourceMappingURL=index.mjs.map