@authup/access 1.0.0-beta.52 → 1.0.0-beta.54

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 (44) hide show
  1. package/dist/index.d.mts +150 -24
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +309 -89
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/permission/evaluator/module.d.ts +12 -1
  6. package/dist/permission/evaluator/module.d.ts.map +1 -1
  7. package/dist/permission/evaluator/types.d.ts +48 -0
  8. package/dist/permission/evaluator/types.d.ts.map +1 -1
  9. package/dist/permission/helpers/aggregate.d.ts +1 -1
  10. package/dist/permission/realm-scope/constants.d.ts +1 -1
  11. package/dist/permission/realm-scope/constants.d.ts.map +1 -1
  12. package/dist/permission/types.d.ts +7 -7
  13. package/dist/permission/types.d.ts.map +1 -1
  14. package/dist/policy/built-in/attribute-names/evaluator.d.ts +1 -0
  15. package/dist/policy/built-in/attribute-names/evaluator.d.ts.map +1 -1
  16. package/dist/policy/built-in/attributes/evaluator.d.ts +5 -0
  17. package/dist/policy/built-in/attributes/evaluator.d.ts.map +1 -1
  18. package/dist/policy/built-in/attributes/types.d.ts +2 -2
  19. package/dist/policy/built-in/attributes/types.d.ts.map +1 -1
  20. package/dist/policy/built-in/composite/evaluator.d.ts.map +1 -1
  21. package/dist/policy/built-in/composite/types.d.ts +1 -1
  22. package/dist/policy/built-in/composite/types.d.ts.map +1 -1
  23. package/dist/policy/built-in/constants.d.ts +0 -1
  24. package/dist/policy/built-in/constants.d.ts.map +1 -1
  25. package/dist/policy/built-in/identity/evaluator.d.ts +1 -0
  26. package/dist/policy/built-in/identity/evaluator.d.ts.map +1 -1
  27. package/dist/policy/built-in/identity/types.d.ts +1 -1
  28. package/dist/policy/built-in/permission-binding/module.d.ts +1 -0
  29. package/dist/policy/built-in/permission-binding/module.d.ts.map +1 -1
  30. package/dist/policy/built-in/realm-match/evaluator.d.ts +14 -0
  31. package/dist/policy/built-in/realm-match/evaluator.d.ts.map +1 -1
  32. package/dist/policy/built-in/realm-match/types.d.ts +6 -6
  33. package/dist/policy/built-in/realm-match/types.d.ts.map +1 -1
  34. package/dist/policy/built-in/time/types.d.ts +3 -3
  35. package/dist/policy/built-in/time/types.d.ts.map +1 -1
  36. package/dist/policy/built-in/types.d.ts +2 -0
  37. package/dist/policy/built-in/types.d.ts.map +1 -1
  38. package/dist/policy/engine/module.d.ts.map +1 -1
  39. package/dist/policy/evaluation/context/define.d.ts.map +1 -1
  40. package/dist/policy/evaluation/context/types.d.ts +7 -0
  41. package/dist/policy/evaluation/context/types.d.ts.map +1 -1
  42. package/dist/policy/evaluation/types.d.ts +42 -0
  43. package/dist/policy/evaluation/types.d.ts.map +1 -1
  44. package/package.json +6 -4
package/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  import { AuthupError, ErrorCode, hasInstanceof, isAuthupError, markInstanceof } from "@authup/errors";
2
2
  import { DecisionStrategy, flattenObject, hasOwnProperty, isObject } from "@authup/kit";
3
3
  import { isEqual } from "smob";
4
+ import { and, eq, inArray, nin, not, or } from "@rapiq/core";
4
5
  import { Container, defineIssueGroup, defineIssueItem } from "validup";
5
6
  import { createValidator } from "@validup/zod";
6
7
  import { z } from "zod";
7
- import { guard } from "@ucast/mongo2js";
8
+ import { compileFilters } from "@rapiq/memory";
9
+ import { MongoFiltersParser } from "@rapiq/parser-mongo";
8
10
  //#region src/permission/error/module.ts
9
11
  const PERMISSION_ERROR_INSTANCE = Symbol.for("@authup/access/PermissionError");
10
12
  var PermissionError = class PermissionError extends AuthupError {
@@ -72,7 +74,7 @@ let RealmScope = /* @__PURE__ */ function(RealmScope) {
72
74
  RealmScope["NONE"] = "none";
73
75
  /** Own realm only — resource realm must equal the actor's realm. */
74
76
  RealmScope["OWN"] = "own";
75
- /** Own realm or null/global resources (realm_id === null). */
77
+ /** Own realm or null/global resources (realmId === null). */
76
78
  RealmScope["OWN_OR_NULL"] = "ownOrNull";
77
79
  /** Any realm, including null/global. */
78
80
  RealmScope["ANY"] = "any";
@@ -162,7 +164,7 @@ function formatKeySegment(value) {
162
164
  return value || "*";
163
165
  }
164
166
  function buildPermissionKey(input) {
165
- return `${formatKeySegment(input.realm_id)}/${formatKeySegment(input.client_id)}/${input.name}`;
167
+ return `${formatKeySegment(input.realmId)}/${formatKeySegment(input.clientId)}/${input.name}`;
166
168
  }
167
169
  //#endregion
168
170
  //#region src/permission/helpers/aggregate.ts
@@ -170,24 +172,24 @@ function buildPermissionKey(input) {
170
172
  * One disjunction term per raw binding: the grant's realm reach (fail-closed `own` default)
171
173
  * paired with its policy — the raw single policy (its `id` preserved for junction propagation),
172
174
  * a composite over multiple policies (Layer-1 permission policies, combined under the
173
- * permission's decision_strategy), or `undefined` when the grant is unrestricted.
175
+ * permission's decisionStrategy), or `undefined` when the grant is unrestricted.
174
176
  */
175
177
  function buildGrant(binding) {
176
178
  let policy;
177
179
  if (binding.policies && binding.policies.length === 1) policy = binding.policies[0];
178
180
  else if (binding.policies && binding.policies.length > 1) policy = {
179
181
  type: "composite",
180
- decision_strategy: binding.permission.decision_strategy || DecisionStrategy.UNANIMOUS,
182
+ decisionStrategy: binding.permission.decisionStrategy || DecisionStrategy.UNANIMOUS,
181
183
  children: binding.policies
182
184
  };
183
185
  return {
184
- realm_scope: normalizeRealmScope(binding.realm_scope),
186
+ realmScope: normalizeRealmScope(binding.realmScope),
185
187
  policy
186
188
  };
187
189
  }
188
190
  /**
189
191
  * Group raw permission-policy bindings by permission key into the actor's disjunction of
190
- * `(realm_scope, policy)` grants per permission — with NO lossy collapse. Every consumer
192
+ * `(realmScope, policy)` grants per permission — with NO lossy collapse. Every consumer
191
193
  * (the binding evaluator, isSuperset, junction propagation, the memory provider) evaluates the
192
194
  * grant disjunction directly, so each grant's realm reach stays paired with the policy that
193
195
  * gates it.
@@ -213,8 +215,8 @@ function aggregatePermissionPolicyBindings(input) {
213
215
  //#region src/permission/helpers/equal.ts
214
216
  function isPermissionPolicyBindingEqual(a, b) {
215
217
  if (a.permission.name !== b.permission.name) return false;
216
- if ((a.permission.realm_id ?? null) !== (b.permission.realm_id ?? null)) return false;
217
- if ((a.permission.client_id ?? null) !== (b.permission.client_id ?? null)) return false;
218
+ if ((a.permission.realmId ?? null) !== (b.permission.realmId ?? null)) return false;
219
+ if ((a.permission.clientId ?? null) !== (b.permission.clientId ?? null)) return false;
218
220
  return true;
219
221
  }
220
222
  //#endregion
@@ -223,9 +225,9 @@ function isPermissionPolicyBindingEqual(a, b) {
223
225
  * Policy fields that do NOT affect evaluation — persisted identity, audit and label columns plus
224
226
  * the loaded `parent` / `realm` relation objects. They are stripped before the structural compare
225
227
  * in isPolicyEquivalent so two DISTINCT rows with identical configuration still match. Verified
226
- * against every built-in evaluator, which read only `type`, `invert`, `decision_strategy`,
228
+ * against every built-in evaluator, which read only `type`, `invert`, `decisionStrategy`,
227
229
  * `children` and the type-specific config (query / names / types / start / end / interval /
228
- * day_of_* / attribute_name* / scope). `parent` is additionally a circular back-reference and
230
+ * dayOf* / attributeName* / scope). `parent` is additionally a circular back-reference and
229
231
  * `realm` a nested object with its own id, so leaving either in would break the deep compare.
230
232
  *
231
233
  * SECURITY INVARIANT: every key here MUST stay non-evaluation-affecting. Adding an
@@ -234,16 +236,16 @@ function isPermissionPolicyBindingEqual(a, b) {
234
236
  */
235
237
  const NON_SEMANTIC_POLICY_KEYS = /* @__PURE__ */ new Set([
236
238
  "id",
237
- "built_in",
239
+ "builtIn",
238
240
  "name",
239
- "display_name",
241
+ "displayName",
240
242
  "description",
241
- "parent_id",
243
+ "parentId",
242
244
  "parent",
243
- "realm_id",
245
+ "realmId",
244
246
  "realm",
245
- "created_at",
246
- "updated_at"
247
+ "createdAt",
248
+ "updatedAt"
247
249
  ]);
248
250
  /**
249
251
  * Project a policy node onto its evaluation-relevant configuration: drop the non-semantic keys
@@ -305,7 +307,7 @@ function policyDominates(parent, child) {
305
307
  * every child grant must be dominated by SOME parent grant.
306
308
  */
307
309
  function grantDominates(parent, child) {
308
- if (compareRealmScope(parent.realm_scope, child.realm_scope) < 0) return false;
310
+ if (compareRealmScope(parent.realmScope, child.realmScope) < 0) return false;
309
311
  return policyDominates(parent.policy, child.policy);
310
312
  }
311
313
  //#endregion
@@ -318,8 +320,8 @@ var PermissionMemoryProvider = class {
318
320
  async findOne(options) {
319
321
  const key = buildPermissionKey({
320
322
  name: options.name,
321
- client_id: options.clientId,
322
- realm_id: options.realmId
323
+ clientId: options.clientId,
324
+ realmId: options.realmId
323
325
  });
324
326
  const entry = this.items[key];
325
327
  if (entry) return entry;
@@ -334,6 +336,38 @@ var PermissionMemoryProvider = class {
334
336
  }
335
337
  };
336
338
  //#endregion
339
+ //#region src/policy/issue/constants.ts
340
+ let PolicyIssueSeverity = /* @__PURE__ */ function(PolicyIssueSeverity) {
341
+ PolicyIssueSeverity["WARNING"] = "warning";
342
+ PolicyIssueSeverity["ERROR"] = "error";
343
+ return PolicyIssueSeverity;
344
+ }({});
345
+ let PolicyIssueCode = /* @__PURE__ */ function(PolicyIssueCode) {
346
+ PolicyIssueCode["DATA_MISSING"] = "DATA_MISSING";
347
+ PolicyIssueCode["DATA_INVALID"] = "DATA_INVALID";
348
+ PolicyIssueCode["INVALID"] = "INVALID";
349
+ PolicyIssueCode["TYPE_UNKNOWN"] = "TYPE_UNKNOWN";
350
+ PolicyIssueCode["FIELD_MISSING"] = "FIELD_MISSING";
351
+ PolicyIssueCode["FIELD_INVALID"] = "FIELD_INVALID";
352
+ PolicyIssueCode["CHILD_INVALID"] = "CHILD_INVALID";
353
+ PolicyIssueCode["CHILD_EVALUATION_FAILED"] = "CHILD_EVALUATION_FAILED";
354
+ PolicyIssueCode["EVALUATION_DENIED"] = "EVALUATION_DENIED";
355
+ PolicyIssueCode["EVALUATOR_NOT_FOUND"] = "EVALUATOR_NOT_FOUND";
356
+ PolicyIssueCode["EVALUATOR_ERROR"] = "EVALUATOR_ERROR";
357
+ PolicyIssueCode["SKIPPED_EXCLUDED"] = "SKIPPED_EXCLUDED";
358
+ PolicyIssueCode["SKIPPED_NOT_INCLUDED"] = "SKIPPED_NOT_INCLUDED";
359
+ PolicyIssueCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
360
+ return PolicyIssueCode;
361
+ }({});
362
+ //#endregion
363
+ //#region src/policy/issue/define.ts
364
+ function definePolicyIssueItem(input) {
365
+ return defineIssueItem(input);
366
+ }
367
+ function definePolicyIssueGroup(input) {
368
+ return defineIssueGroup(input);
369
+ }
370
+ //#endregion
337
371
  //#region src/policy/error/module.ts
338
372
  const POLICY_ERROR_INSTANCE = Symbol.for("@authup/access/PolicyError");
339
373
  var PolicyError = class PolicyError extends AuthupError {
@@ -418,6 +452,25 @@ var PolicyEngine = class {
418
452
  };
419
453
  }
420
454
  try {
455
+ if (evaluator.requires) {
456
+ const missing = evaluator.requires(policy).filter((key) => !ctx.data || !ctx.data.has(key));
457
+ if (missing.length > 0) {
458
+ const result = {
459
+ success: false,
460
+ pending: true,
461
+ issues: [definePolicyIssueItem({
462
+ code: "DATA_MISSING",
463
+ message: `The data propert${missing.length > 1 ? "ies" : "y"} ${missing.join(", ")} ${missing.length > 1 ? "are" : "is"} missing`,
464
+ path: ctx.path
465
+ })]
466
+ };
467
+ if (ctx.withConditions && evaluator.toCondition) try {
468
+ const condition = await evaluator.toCondition(policy, ctx);
469
+ if (condition) result.condition = condition;
470
+ } catch {}
471
+ return result;
472
+ }
473
+ }
421
474
  return await evaluator.evaluate(policy, {
422
475
  ...ctx,
423
476
  evaluators: {
@@ -494,42 +547,11 @@ function definePolicyEvaluationContext(input = {}) {
494
547
  path: input.path || [],
495
548
  data: input.data || new PolicyData(),
496
549
  include: input.include || [],
497
- exclude: input.exclude || []
550
+ exclude: input.exclude || [],
551
+ withConditions: input.withConditions ?? false
498
552
  };
499
553
  }
500
554
  //#endregion
501
- //#region src/policy/issue/constants.ts
502
- let PolicyIssueSeverity = /* @__PURE__ */ function(PolicyIssueSeverity) {
503
- PolicyIssueSeverity["WARNING"] = "warning";
504
- PolicyIssueSeverity["ERROR"] = "error";
505
- return PolicyIssueSeverity;
506
- }({});
507
- let PolicyIssueCode = /* @__PURE__ */ function(PolicyIssueCode) {
508
- PolicyIssueCode["DATA_MISSING"] = "DATA_MISSING";
509
- PolicyIssueCode["DATA_INVALID"] = "DATA_INVALID";
510
- PolicyIssueCode["INVALID"] = "INVALID";
511
- PolicyIssueCode["TYPE_UNKNOWN"] = "TYPE_UNKNOWN";
512
- PolicyIssueCode["FIELD_MISSING"] = "FIELD_MISSING";
513
- PolicyIssueCode["FIELD_INVALID"] = "FIELD_INVALID";
514
- PolicyIssueCode["CHILD_INVALID"] = "CHILD_INVALID";
515
- PolicyIssueCode["CHILD_EVALUATION_FAILED"] = "CHILD_EVALUATION_FAILED";
516
- PolicyIssueCode["EVALUATION_DENIED"] = "EVALUATION_DENIED";
517
- PolicyIssueCode["EVALUATOR_NOT_FOUND"] = "EVALUATOR_NOT_FOUND";
518
- PolicyIssueCode["EVALUATOR_ERROR"] = "EVALUATOR_ERROR";
519
- PolicyIssueCode["SKIPPED_EXCLUDED"] = "SKIPPED_EXCLUDED";
520
- PolicyIssueCode["SKIPPED_NOT_INCLUDED"] = "SKIPPED_NOT_INCLUDED";
521
- PolicyIssueCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
522
- return PolicyIssueCode;
523
- }({});
524
- //#endregion
525
- //#region src/policy/issue/define.ts
526
- function definePolicyIssueItem(input) {
527
- return defineIssueItem(input);
528
- }
529
- function definePolicyIssueGroup(input) {
530
- return defineIssueGroup(input);
531
- }
532
- //#endregion
533
555
  //#region src/policy/helpers/invert.ts
534
556
  function maybeInvertPolicyOutcome(outcome, invert) {
535
557
  if (typeof invert === "undefined") return outcome;
@@ -594,7 +616,6 @@ const SystemPolicyName = {
594
616
  PERMISSION_BINDING: "system.permission-binding",
595
617
  REALM_MATCH: "system.realm-match",
596
618
  CLIENT_NAMES_SELF_MANAGE: "system.client-names-self-manage",
597
- ROBOT_NAMES_SELF_MANAGE: "system.robot-names-self-manage",
598
619
  USER_NAMES_SELF_MANAGE: "system.user-names-self-manage"
599
620
  };
600
621
  //#endregion
@@ -610,8 +631,25 @@ var AttributesPolicyValidator = class extends Container {
610
631
  //#region src/policy/built-in/attributes/evaluator.ts
611
632
  var AttributesPolicyEvaluator = class {
612
633
  validator;
634
+ parser;
613
635
  constructor() {
614
636
  this.validator = new AttributesPolicyValidator();
637
+ this.parser = new MongoFiltersParser();
638
+ }
639
+ requires() {
640
+ return ["attributes"];
641
+ }
642
+ async toCondition(value) {
643
+ let policy;
644
+ let condition;
645
+ try {
646
+ policy = await this.validator.run(value);
647
+ this.fixQuery(policy.query);
648
+ condition = this.parser.parse(policy.query);
649
+ } catch {
650
+ return null;
651
+ }
652
+ return policy.invert ? not(condition) : condition;
615
653
  }
616
654
  async accessData(ctx) {
617
655
  if (!ctx.data.has("attributes")) return null;
@@ -633,7 +671,20 @@ var AttributesPolicyEvaluator = class {
633
671
  })]
634
672
  };
635
673
  this.fixQuery(policy.query);
636
- return { success: maybeInvertPolicyOutcome(guard(policy.query)(data), policy.invert) };
674
+ let testIt;
675
+ try {
676
+ testIt = compileFilters(this.parser.parse(policy.query), { caseSensitive: true });
677
+ } catch (e) {
678
+ return {
679
+ success: false,
680
+ issues: [definePolicyIssueItem({
681
+ code: "FIELD_INVALID",
682
+ message: e instanceof Error ? e.message : "The query could not be parsed.",
683
+ path: ctx.path
684
+ })]
685
+ };
686
+ }
687
+ return { success: maybeInvertPolicyOutcome(testIt(data), policy.invert) };
637
688
  }
638
689
  fixQuery(query) {
639
690
  if (Array.isArray(query)) {
@@ -665,6 +716,9 @@ var AttributeNamesPolicyEvaluator = class {
665
716
  this.validator = new AttributeNamesPolicyValidator();
666
717
  this.attributesEvaluator = new AttributesPolicyEvaluator();
667
718
  }
719
+ requires() {
720
+ return ["attributes"];
721
+ }
668
722
  async evaluate(value, ctx) {
669
723
  const policy = await this.validator.run(value);
670
724
  const data = await this.attributesEvaluator.accessData(ctx);
@@ -698,7 +752,7 @@ var AttributeNamesPolicyEvaluator = class {
698
752
  var CompositePolicyValidator = class extends Container {
699
753
  initialize() {
700
754
  super.initialize();
701
- this.mount("decision_strategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
755
+ this.mount("decisionStrategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
702
756
  this.mount("children", createValidator(z.array(z.looseObject({ type: z.string().min(3) }))));
703
757
  this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
704
758
  }
@@ -712,18 +766,41 @@ var CompositePolicyEvaluator = class {
712
766
  }
713
767
  async evaluate(value, ctx) {
714
768
  const policy = await this.validator.run(value);
769
+ if (policy.children.length === 0) return {
770
+ success: false,
771
+ issues: [definePolicyIssueItem({
772
+ code: "INVALID",
773
+ message: "A composite policy must define at least one child policy.",
774
+ path: ctx.path
775
+ })]
776
+ };
715
777
  let count = 0;
716
- const decision_strategy = policy.decision_strategy ?? DecisionStrategy.UNANIMOUS;
778
+ let pending = 0;
779
+ const decisionStrategy = policy.decisionStrategy ?? DecisionStrategy.UNANIMOUS;
717
780
  const engine = new PolicyEngine(ctx.evaluators);
718
781
  const issues = [];
782
+ const pendingIssues = [];
783
+ const pendingConditions = [];
784
+ let pendingWithoutCondition = 0;
719
785
  for (const childPolicy of policy.children) {
720
786
  const path = [...ctx.path || [], ...childPolicy.type ? [childPolicy.type] : []];
721
787
  const outcome = await engine.evaluate(childPolicy, {
722
788
  ...ctx,
723
789
  path
724
790
  });
791
+ if (outcome.pending) {
792
+ pending++;
793
+ if (outcome.condition) pendingConditions.push(outcome.condition);
794
+ else pendingWithoutCondition++;
795
+ if (outcome.issues && outcome.issues.length > 0) pendingIssues.push(defineIssueGroup({
796
+ message: `The evaluation of child policy ${childPolicy.type} is pending`,
797
+ issues: outcome.issues,
798
+ path
799
+ }));
800
+ continue;
801
+ }
725
802
  if (outcome.success) {
726
- if (decision_strategy === DecisionStrategy.AFFIRMATIVE) return {
803
+ if (decisionStrategy === DecisionStrategy.AFFIRMATIVE) return {
727
804
  success: maybeInvertPolicyOutcome(true, policy.invert),
728
805
  issues: []
729
806
  };
@@ -734,7 +811,7 @@ var CompositePolicyEvaluator = class {
734
811
  issues: outcome.issues || [],
735
812
  path
736
813
  }));
737
- if (decision_strategy === DecisionStrategy.UNANIMOUS) {
814
+ if (decisionStrategy === DecisionStrategy.UNANIMOUS) {
738
815
  const success = maybeInvertPolicyOutcome(false, policy.invert);
739
816
  return {
740
817
  success,
@@ -748,12 +825,41 @@ var CompositePolicyEvaluator = class {
748
825
  count--;
749
826
  }
750
827
  }
751
- if (count > 0) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
828
+ let settled;
829
+ switch (decisionStrategy) {
830
+ case DecisionStrategy.AFFIRMATIVE:
831
+ settled = pending > 0 ? null : false;
832
+ break;
833
+ case DecisionStrategy.UNANIMOUS:
834
+ settled = pending > 0 ? null : count > 0;
835
+ break;
836
+ default: if (count - pending > 0) settled = true;
837
+ else if (count + pending <= 0) settled = false;
838
+ else settled = null;
839
+ }
840
+ if (settled === null) {
841
+ let condition = null;
842
+ if (ctx.withConditions && pendingWithoutCondition === 0 && pendingConditions.length > 0 && decisionStrategy !== DecisionStrategy.CONSENSUS) {
843
+ if (pendingConditions.length === 1) {
844
+ const [first] = pendingConditions;
845
+ condition = first ?? null;
846
+ } else if (decisionStrategy === DecisionStrategy.AFFIRMATIVE) condition = or(...pendingConditions);
847
+ else condition = and(...pendingConditions);
848
+ if (condition && policy.invert) condition = not(condition);
849
+ }
850
+ return {
851
+ success: false,
852
+ pending: true,
853
+ ...condition ? { condition } : {},
854
+ issues: pendingIssues
855
+ };
856
+ }
857
+ if (settled) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
752
858
  const success = maybeInvertPolicyOutcome(false, policy.invert);
753
859
  return {
754
860
  success,
755
861
  issues: success ? [] : [defineIssueGroup({
756
- message: `The evaluation of composite policy failed (${DecisionStrategy.CONSENSUS})`,
862
+ message: `The evaluation of composite policy failed (${decisionStrategy})`,
757
863
  issues: issues || [],
758
864
  path: ctx.path
759
865
  })]
@@ -840,6 +946,9 @@ var IdentityPolicyEvaluator = class {
840
946
  this.validator = new IdentityPolicyValidator();
841
947
  this.dataValidator = new PolicyIdentityDataValidator();
842
948
  }
949
+ requires() {
950
+ return ["identity"];
951
+ }
843
952
  async accessData(ctx) {
844
953
  if (!ctx.data.has("identity")) return null;
845
954
  if (ctx.data.isValidated("identity")) return ctx.data.get("identity");
@@ -884,6 +993,9 @@ var PermissionBindingPolicyEvaluator = class {
884
993
  constructor() {
885
994
  this.validator = new PermissionBindingPolicyValidator();
886
995
  }
996
+ requires() {
997
+ return ["permissionBinding"];
998
+ }
887
999
  async evaluate(value) {
888
1000
  return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
889
1001
  }
@@ -893,10 +1005,10 @@ var PermissionBindingPolicyEvaluator = class {
893
1005
  var RealmMatchPolicyValidator = class extends Container {
894
1006
  initialize() {
895
1007
  super.initialize();
896
- this.mount("decision_strategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
897
- this.mount("attribute_name", createValidator(z.string().min(3).or(z.array(z.string().min(3))).or(z.null()).or(z.undefined()).optional()));
898
- this.mount("attribute_name_strict", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
899
- this.mount("attribute_null_match_all", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
1008
+ this.mount("decisionStrategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
1009
+ this.mount("attributeName", createValidator(z.string().min(3).or(z.array(z.string().min(3))).or(z.null()).or(z.undefined()).optional()));
1010
+ this.mount("attributeNameStrict", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
1011
+ this.mount("attributeNullMatchAll", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
900
1012
  this.mount("scope", createValidator(z.enum(RealmScope).or(z.null()).or(z.undefined()).optional()));
901
1013
  this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
902
1014
  }
@@ -912,6 +1024,56 @@ var RealmMatchPolicyEvaluator = class {
912
1024
  this.identityEvaluator = new IdentityPolicyEvaluator();
913
1025
  this.attributesEvaluator = new AttributesPolicyEvaluator();
914
1026
  }
1027
+ requires(value) {
1028
+ if (value.scope) return ["identity"];
1029
+ return ["identity", "attributes"];
1030
+ }
1031
+ /**
1032
+ * Condition form over the row's realm column, with the actor realm baked in
1033
+ * (mirrors `realmScopeMatches` / the attribute-mode match). Exactness holds for
1034
+ * row-shaped data where the referenced field exists (DB rows) — the object-bag
1035
+ * "key absent → neutral-pass" case has no SQL counterpart. Lowerable configs:
1036
+ * scope mode (field = single-string `attributeName`, default `realmId` — scope-mode
1037
+ * EVALUATION ignores `attributeName`, lowering reuses it as the column override),
1038
+ * and strict attribute mode with a single explicit `attributeName`. Everything
1039
+ * else (multi-key / default-key CONSENSUS, non-strict key expansion) is
1040
+ * row-shape-dependent and stays a post-check.
1041
+ */
1042
+ async toCondition(value, ctx) {
1043
+ let policy;
1044
+ try {
1045
+ policy = await this.validator.run(value);
1046
+ } catch {
1047
+ return null;
1048
+ }
1049
+ const identity = await this.identityEvaluator.accessData(ctx);
1050
+ if (!identity) return null;
1051
+ const realmId = identity.realmId ?? null;
1052
+ if (policy.scope) {
1053
+ const scope = normalizeRealmScope(policy.scope);
1054
+ const field = typeof policy.attributeName === "string" ? policy.attributeName : "realmId";
1055
+ let condition;
1056
+ if (scope === "any") condition = nin(field, []);
1057
+ else {
1058
+ const terms = [];
1059
+ if (scope !== "none" && realmId !== null) terms.push(eq(field, realmId));
1060
+ if (scope === "ownOrNull") terms.push(eq(field, null));
1061
+ if (terms.length === 0) condition = inArray(field, []);
1062
+ else if (terms.length === 1) condition = terms[0];
1063
+ else condition = or(...terms);
1064
+ }
1065
+ return policy.invert ? not(condition) : condition;
1066
+ }
1067
+ if (typeof policy.attributeName !== "string" || (policy.attributeNameStrict ?? true) === false) return null;
1068
+ const key = policy.attributeName;
1069
+ const terms = [];
1070
+ if (policy.attributeNullMatchAll) terms.push(eq(key, null));
1071
+ if (identity.realmId !== void 0) terms.push(eq(key, identity.realmId));
1072
+ if (identity.realmName !== void 0) terms.push(eq(key, identity.realmName));
1073
+ if (terms.length === 0) return policy.invert ? nin(key, []) : inArray(key, []);
1074
+ const condition = terms.length === 1 ? terms[0] : or(...terms);
1075
+ return policy.invert ? not(condition) : condition;
1076
+ }
915
1077
  async evaluate(value, ctx) {
916
1078
  const policy = await this.validator.run(value);
917
1079
  const identity = await this.identityEvaluator.accessData(ctx);
@@ -924,7 +1086,17 @@ var RealmMatchPolicyEvaluator = class {
924
1086
  })]
925
1087
  };
926
1088
  if (policy.scope) {
927
- if (!ctx.data || !ctx.data.has("realmMatch")) return { success: true };
1089
+ if (!ctx.data || !ctx.data.has("realmMatch")) {
1090
+ if (ctx.withConditions) {
1091
+ const condition = await this.toCondition(value, ctx);
1092
+ return {
1093
+ success: false,
1094
+ pending: true,
1095
+ ...condition ? { condition } : {}
1096
+ };
1097
+ }
1098
+ return { success: true };
1099
+ }
928
1100
  const resourceRealm = ctx.data.get("realmMatch");
929
1101
  return { success: maybeInvertPolicyOutcome(realmScopeMatches(policy.scope, resourceRealm ?? null, identity.realmId, identity.realmName), policy.invert) };
930
1102
  }
@@ -938,8 +1110,8 @@ var RealmMatchPolicyEvaluator = class {
938
1110
  })]
939
1111
  };
940
1112
  let keys;
941
- if (policy.attribute_name) if (Array.isArray(policy.attribute_name)) keys = policy.attribute_name;
942
- else keys = [policy.attribute_name];
1113
+ if (policy.attributeName) if (Array.isArray(policy.attributeName)) keys = policy.attributeName;
1114
+ else keys = [policy.attributeName];
943
1115
  else {
944
1116
  keys = [
945
1117
  "realm_id",
@@ -947,9 +1119,9 @@ var RealmMatchPolicyEvaluator = class {
947
1119
  "realmId",
948
1120
  "realmName"
949
1121
  ];
950
- policy.decision_strategy = DecisionStrategy.CONSENSUS;
1122
+ policy.decisionStrategy = DecisionStrategy.CONSENSUS;
951
1123
  }
952
- if (!(policy.attribute_name_strict ?? true)) {
1124
+ if (!(policy.attributeNameStrict ?? true)) {
953
1125
  const resourceKeys = Object.keys(attributes);
954
1126
  const keysToAdd = [];
955
1127
  for (const resourceKey of resourceKeys) {
@@ -969,13 +1141,13 @@ var RealmMatchPolicyEvaluator = class {
969
1141
  evaluated = true;
970
1142
  let outcome = false;
971
1143
  const attributeValue = attributes[key];
972
- if (attributeValue === null && policy.attribute_null_match_all) outcome = true;
1144
+ if (attributeValue === null && policy.attributeNullMatchAll) outcome = true;
973
1145
  else if (attributeValue === identity.realmId || attributeValue === identity.realmName) outcome = true;
974
1146
  if (outcome) {
975
- if (policy.decision_strategy === DecisionStrategy.AFFIRMATIVE) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
1147
+ if (policy.decisionStrategy === DecisionStrategy.AFFIRMATIVE) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
976
1148
  count++;
977
1149
  } else {
978
- if (policy.decision_strategy === DecisionStrategy.UNANIMOUS) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1150
+ if (policy.decisionStrategy === DecisionStrategy.UNANIMOUS) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
979
1151
  count--;
980
1152
  }
981
1153
  }
@@ -1008,9 +1180,9 @@ function isIntervalForDayOfYear(interval) {
1008
1180
  var TimePolicyValidator = class extends Container {
1009
1181
  initialize() {
1010
1182
  super.initialize();
1011
- this.mount("day_of_week", createValidator(z.number().min(0).max(6).or(z.null()).or(z.undefined()).optional()));
1012
- this.mount("day_of_month", createValidator(z.number().min(1).max(31).or(z.null()).or(z.undefined()).optional()));
1013
- this.mount("day_of_year", createValidator(z.number().min(1).max(365).or(z.null()).or(z.undefined()).optional()));
1183
+ this.mount("dayOfWeek", createValidator(z.number().min(0).max(6).or(z.null()).or(z.undefined()).optional()));
1184
+ this.mount("dayOfMonth", createValidator(z.number().min(1).max(31).or(z.null()).or(z.undefined()).optional()));
1185
+ this.mount("dayOfYear", createValidator(z.number().min(1).max(365).or(z.null()).or(z.undefined()).optional()));
1014
1186
  this.mount("start", createValidator(z.date().or(z.iso.datetime()).or(z.iso.time()).or(z.number()).or(z.null()).or(z.undefined()).optional()));
1015
1187
  this.mount("end", createValidator(z.date().or(z.iso.datetime()).or(z.iso.time()).or(z.number()).or(z.null()).or(z.undefined()).optional()));
1016
1188
  this.mount("interval", createValidator(z.enum(TimePolicyInterval).or(z.null()).or(z.undefined()).optional()));
@@ -1067,16 +1239,16 @@ var TimePolicyEvaluator = class {
1067
1239
  if (now > end) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1068
1240
  }
1069
1241
  if (policy.interval) {
1070
- if (isIntervalForDayOfWeek(policy.interval) && policy.day_of_week) {
1071
- if (now.getDay() !== policy.day_of_week) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1242
+ if (isIntervalForDayOfWeek(policy.interval) && policy.dayOfWeek) {
1243
+ if (now.getDay() !== policy.dayOfWeek) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1072
1244
  }
1073
- if (isIntervalForDayOfMonth(policy.interval) && policy.day_of_month) {
1074
- if (now.getDate() !== policy.day_of_month) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1245
+ if (isIntervalForDayOfMonth(policy.interval) && policy.dayOfMonth) {
1246
+ if (now.getDate() !== policy.dayOfMonth) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1075
1247
  }
1076
- if (isIntervalForDayOfYear(policy.interval) && policy.day_of_year) {
1248
+ if (isIntervalForDayOfYear(policy.interval) && policy.dayOfYear) {
1077
1249
  const start = new Date(now.getFullYear(), 0, 0);
1078
1250
  const diff = now.getTime() - start.getTime() + (start.getTimezoneOffset() - now.getTimezoneOffset()) * 60 * 1e3;
1079
- if (Math.floor(diff / (1e3 * 60 * 60 * 24)) !== policy.day_of_year) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1251
+ if (Math.floor(diff / (1e3 * 60 * 60 * 24)) !== policy.dayOfYear) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
1080
1252
  }
1081
1253
  }
1082
1254
  return { success: maybeInvertPolicyOutcome(true, policy.invert) };
@@ -1176,7 +1348,7 @@ var PermissionEvaluator = class {
1176
1348
  data.set("permissionBinding", binding);
1177
1349
  const compositePolicy = {
1178
1350
  type: "composite",
1179
- decision_strategy: DecisionStrategy.AFFIRMATIVE,
1351
+ decisionStrategy: DecisionStrategy.AFFIRMATIVE,
1180
1352
  children: grantPolicies
1181
1353
  };
1182
1354
  const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
@@ -1184,7 +1356,7 @@ var PermissionEvaluator = class {
1184
1356
  exclude: options.policiesExcluded,
1185
1357
  data
1186
1358
  }));
1187
- if (evaluationResult.success) {
1359
+ if (evaluationResult.success || options.pendingPolicies === "permit" && !!evaluationResult.pending) {
1188
1360
  if (decisionStrategy === DecisionStrategy.AFFIRMATIVE) return;
1189
1361
  count++;
1190
1362
  } else {
@@ -1224,11 +1396,7 @@ var PermissionEvaluator = class {
1224
1396
  ...ctx,
1225
1397
  options: {
1226
1398
  ...ctx.options || {},
1227
- policiesExcluded: [
1228
- "attributes",
1229
- "attributeNames",
1230
- "realmMatch"
1231
- ]
1399
+ pendingPolicies: "permit"
1232
1400
  }
1233
1401
  });
1234
1402
  }
@@ -1241,6 +1409,58 @@ var PermissionEvaluator = class {
1241
1409
  }
1242
1410
  });
1243
1411
  }
1412
+ /**
1413
+ * Compile the permission's policy trees against the knowns bag into a
1414
+ * {@link PermissionCompileResult} — the query-build counterpart of
1415
+ * {@link evaluate} (issue #3286 phase 3). Runs the SAME evaluation walk with
1416
+ * `withConditions` enabled: policies that settle with the knowns settle here,
1417
+ * pending subtrees contribute their exact condition form. Multiple names are
1418
+ * a disjunction (any-of), so a single non-expressible name degrades the whole
1419
+ * compilation to `post` — pushing only some disjuncts would wrongly exclude
1420
+ * rows another name would admit.
1421
+ */
1422
+ async compile(ctx) {
1423
+ const names = Array.isArray(ctx.name) ? ctx.name : [ctx.name];
1424
+ const dataBase = ctx.data || new PolicyData();
1425
+ const conditions = [];
1426
+ let post = false;
1427
+ for (const name of names) {
1428
+ const binding = await this.findOne(name, {
1429
+ realmId: ctx.realmId,
1430
+ clientId: ctx.clientId
1431
+ });
1432
+ if (!binding) continue;
1433
+ const grantPolicies = binding.grants.map((grant) => grant.policy).filter((grantPolicy) => !!grantPolicy);
1434
+ if (grantPolicies.length < binding.grants.length) return { verdict: "allow" };
1435
+ const data = dataBase.clone();
1436
+ data.set("permissionBinding", binding);
1437
+ const compositePolicy = {
1438
+ type: "composite",
1439
+ decisionStrategy: DecisionStrategy.AFFIRMATIVE,
1440
+ children: grantPolicies
1441
+ };
1442
+ const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
1443
+ data,
1444
+ withConditions: true
1445
+ }));
1446
+ if (evaluationResult.success) return { verdict: "allow" };
1447
+ if (evaluationResult.pending) if (evaluationResult.condition) conditions.push(evaluationResult.condition);
1448
+ else post = true;
1449
+ }
1450
+ if (post) return { verdict: "post" };
1451
+ if (conditions.length === 0) return { verdict: "deny" };
1452
+ if (conditions.length === 1) {
1453
+ const [condition] = conditions;
1454
+ return {
1455
+ verdict: "conditional",
1456
+ condition
1457
+ };
1458
+ }
1459
+ return {
1460
+ verdict: "conditional",
1461
+ condition: or(...conditions)
1462
+ };
1463
+ }
1244
1464
  };
1245
1465
  //#endregion
1246
1466
  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 };