@cullet/erp-core 1.4.0 → 2.0.0
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.
- package/KIT_CONTEXT.md +7 -1
- package/dist/abac/index.d.cts +2 -2
- package/dist/abac/index.d.ts +2 -2
- package/dist/aggregate-version.cjs +14 -0
- package/dist/aggregate-version.cjs.map +1 -0
- package/dist/aggregate-version.js +9 -0
- package/dist/aggregate-version.js.map +1 -0
- package/dist/app-error.cjs +21 -6
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +21 -6
- package/dist/app-error.js.map +1 -1
- package/dist/application/index.cjs +8 -4
- package/dist/application/index.d.cts +2 -2
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +1 -2
- package/dist/authorization-error.cjs +67 -17
- package/dist/authorization-error.cjs.map +1 -1
- package/dist/authorization-error.d.cts +6 -2
- package/dist/authorization-error.d.ts +6 -2
- package/dist/authorization-error.js +50 -18
- package/dist/authorization-error.js.map +1 -1
- package/dist/authorizer.port.d.cts +18 -3
- package/dist/authorizer.port.d.ts +18 -3
- package/dist/composite-authorizer.d.cts +63 -10
- package/dist/composite-authorizer.d.ts +63 -10
- package/dist/condition-evaluator.cjs +117 -172
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +118 -167
- package/dist/condition-evaluator.js.map +1 -1
- package/dist/core-config.d.cts +53 -6
- package/dist/core-config.d.ts +53 -6
- package/dist/decorate.cjs +14 -0
- package/dist/decorate.js +9 -0
- package/dist/domain/index.cjs +107 -2
- package/dist/domain/index.cjs.map +1 -0
- package/dist/domain/index.d.cts +25 -1
- package/dist/domain/index.d.ts +25 -1
- package/dist/domain/index.js +99 -3
- package/dist/domain/index.js.map +1 -0
- package/dist/domain-event-contracts.cjs +12 -8
- package/dist/domain-event-contracts.cjs.map +1 -1
- package/dist/domain-event-contracts.d.cts +29 -4
- package/dist/domain-event-contracts.d.ts +29 -4
- package/dist/domain-event-contracts.js +11 -7
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +2 -1
- package/dist/domain-exception.cjs.map +1 -1
- package/dist/domain-exception.js +2 -1
- package/dist/domain-exception.js.map +1 -1
- package/dist/errors/index.cjs +3 -2
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +3 -2
- package/dist/exceptions/index.cjs +2 -2
- package/dist/exceptions/index.d.cts +1 -2
- package/dist/exceptions/index.d.ts +1 -2
- package/dist/exceptions/index.js +2 -2
- package/dist/gate-engine-registry.cjs.map +1 -1
- package/dist/gate-engine-registry.d.cts +1 -1
- package/dist/gate-engine-registry.d.ts +1 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +11 -6
- package/dist/gate-v1-payload.schema.cjs.map +1 -1
- package/dist/gate-v1-payload.schema.js +11 -6
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +2 -44
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +3 -39
- package/dist/hashing.js.map +1 -1
- package/dist/immutable.cjs +25 -12
- package/dist/immutable.cjs.map +1 -1
- package/dist/immutable.js +24 -11
- package/dist/immutable.js.map +1 -1
- package/dist/index.cjs +15 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +6 -6
- package/dist/invalid-state-transition-exception.cjs.map +1 -1
- package/dist/invalid-state-transition-exception.js +6 -6
- package/dist/invalid-state-transition-exception.js.map +1 -1
- package/dist/invariant-violation-exception.cjs +2 -2
- package/dist/invariant-violation-exception.cjs.map +1 -1
- package/dist/invariant-violation-exception.js +2 -2
- package/dist/invariant-violation-exception.js.map +1 -1
- package/dist/not-found-error.cjs +6 -4
- package/dist/not-found-error.cjs.map +1 -1
- package/dist/not-found-error.js +6 -4
- package/dist/not-found-error.js.map +1 -1
- package/dist/outcome.cjs +5 -5
- package/dist/outcome.cjs.map +1 -1
- package/dist/outcome.js +5 -5
- package/dist/outcome.js.map +1 -1
- package/dist/parse-gate-payload.d.cts +1 -1
- package/dist/parse-gate-payload.d.ts +1 -1
- package/dist/path.d.cts +2 -2
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +23 -13
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +22 -8
- package/dist/plugin.d.ts +22 -8
- package/dist/plugin.js +23 -13
- package/dist/plugin.js.map +1 -1
- package/dist/policies/engines/index.d.cts +1 -1
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +3 -13
- package/dist/policies/engines/v1/gate/index.d.ts +3 -13
- package/dist/policies/index.d.cts +1 -1
- package/dist/policies/index.d.ts +1 -1
- package/dist/policy-bridge.cjs +30 -2
- package/dist/policy-bridge.cjs.map +1 -1
- package/dist/policy-bridge.d.cts +18 -0
- package/dist/policy-bridge.d.ts +18 -0
- package/dist/policy-bridge.js +30 -2
- package/dist/policy-bridge.js.map +1 -1
- package/dist/policy-service.cjs +41 -46
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.d.cts +72 -9
- package/dist/policy-service.d.ts +72 -9
- package/dist/policy-service.js +43 -48
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +4 -4
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +2 -2
- package/dist/requested-by.js.map +1 -1
- package/dist/result.cjs +29 -1
- package/dist/result.cjs.map +1 -1
- package/dist/result.d.cts +12 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.js +29 -1
- package/dist/result.js.map +1 -1
- package/dist/rule.cjs +94 -24
- package/dist/rule.cjs.map +1 -1
- package/dist/rule.js +94 -24
- package/dist/rule.js.map +1 -1
- package/dist/ruleset-registry.cjs +19 -0
- package/dist/ruleset-registry.cjs.map +1 -1
- package/dist/ruleset-registry.js +19 -0
- package/dist/ruleset-registry.js.map +1 -1
- package/dist/stable-stringify.cjs +87 -0
- package/dist/stable-stringify.cjs.map +1 -0
- package/dist/stable-stringify.js +76 -0
- package/dist/stable-stringify.js.map +1 -0
- package/dist/temporal-snapshot.d.cts +87 -0
- package/dist/temporal-snapshot.d.ts +87 -0
- package/dist/temporal-use-case.cjs +174 -16
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +82 -44
- package/dist/temporal-use-case.d.ts +82 -44
- package/dist/temporal-use-case.js +167 -15
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +4 -2
- package/dist/unexpected-error.cjs.map +1 -1
- package/dist/unexpected-error.js +4 -2
- package/dist/unexpected-error.js.map +1 -1
- package/dist/uuid-identifier.cjs +141 -8
- package/dist/uuid-identifier.cjs.map +1 -1
- package/dist/uuid-identifier.d.cts +26 -7
- package/dist/uuid-identifier.d.ts +26 -7
- package/dist/uuid-identifier.js +135 -8
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +23 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.js +18 -0
- package/dist/uuid.js.map +1 -0
- package/dist/validation-code.cjs +9 -0
- package/dist/validation-code.cjs.map +1 -1
- package/dist/validation-code.d.cts +3 -0
- package/dist/validation-code.d.ts +3 -0
- package/dist/validation-code.js +9 -0
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.cjs +42 -67
- package/dist/validation-error.cjs.map +1 -1
- package/dist/validation-error.d.cts +29 -8
- package/dist/validation-error.d.ts +29 -8
- package/dist/validation-error.js +41 -66
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +17 -6
- package/dist/validation-exception.cjs.map +1 -1
- package/dist/validation-exception.d.cts +33 -9
- package/dist/validation-exception.d.ts +33 -9
- package/dist/validation-exception.js +17 -6
- package/dist/validation-exception.js.map +1 -1
- package/dist/validation-field.cjs +3 -0
- package/dist/validation-field.cjs.map +1 -1
- package/dist/validation-field.d.cts +1 -0
- package/dist/validation-field.d.ts +1 -0
- package/dist/validation-field.js +3 -0
- package/dist/validation-field.js.map +1 -1
- package/dist/value-object-ruleset.contracts.d.cts +10 -0
- package/dist/value-object-ruleset.contracts.d.ts +10 -0
- package/dist/value-object.cjs +23 -4
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +25 -1
- package/dist/value-object.d.ts +25 -1
- package/dist/value-object.js +23 -4
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +27 -0
- package/dist/version.d.ts +27 -0
- package/dist/versioning/index.d.cts +2 -2
- package/dist/versioning/index.d.ts +2 -2
- package/meta.json +5 -2
- package/package.json +1 -1
- package/src/core/abac/authorizer.ts +60 -10
- package/src/core/abac/domain/policy-set.ts +52 -5
- package/src/core/abac/domain/rule.ts +28 -16
- package/src/core/abac/index.ts +7 -1
- package/src/core/application/commands/command.ts +14 -1
- package/src/core/application/commands/requested-by.ts +13 -5
- package/src/core/application/index.ts +2 -1
- package/src/core/application/policy-error-mapper.ts +6 -0
- package/src/core/application/ports/index.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +35 -2
- package/src/core/application/queries/index.ts +1 -1
- package/src/core/application/queries/query.ts +25 -3
- package/src/core/application/temporal/temporal-use-case.ts +31 -4
- package/src/core/application/use-case.ts +45 -8
- package/src/core/config/core-config.ts +46 -25
- package/src/core/config/index.ts +1 -0
- package/src/core/config/policy-reporter.ts +32 -1
- package/src/core/domain/entity.ts +51 -8
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
- package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
- package/src/core/domain/temporal/half-open-interval.ts +34 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
- package/src/core/domain/temporal/transaction-time.ts +19 -15
- package/src/core/domain/temporal/valid-time.ts +20 -15
- package/src/core/domain/uuid-identifier.ts +6 -11
- package/src/core/domain/value-object.ts +29 -3
- package/src/core/errors/authentication-error.ts +5 -31
- package/src/core/errors/authorization-error.ts +12 -20
- package/src/core/errors/business-rule-violation-error.ts +4 -1
- package/src/core/errors/idempotency-error.ts +5 -2
- package/src/core/errors/index.ts +4 -0
- package/src/core/errors/integration-error.ts +4 -24
- package/src/core/errors/legacy-incompatible-error.ts +1 -0
- package/src/core/errors/not-found-error.ts +4 -1
- package/src/core/errors/temporal-error.ts +22 -20
- package/src/core/errors/unexpected-error.ts +5 -1
- package/src/core/errors/utils/factory-helpers.ts +70 -0
- package/src/core/errors/utils/index.ts +5 -0
- package/src/core/errors/utils/json-safe.ts +35 -12
- package/src/core/errors/validation-error.ts +4 -1
- package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
- package/src/core/exceptions/domain-exception.ts +9 -1
- package/src/core/exceptions/entity-not-found-exception.ts +5 -1
- package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
- package/src/core/exceptions/invariant-violation-exception.ts +2 -2
- package/src/core/exceptions/validation-code.ts +14 -0
- package/src/core/exceptions/validation-exception.ts +44 -5
- package/src/core/exceptions/validation-field.ts +11 -1
- package/src/core/plugins/plugin.ts +25 -15
- package/src/core/plugins/types.ts +4 -2
- package/src/core/policies/asof/asof.ts +12 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
- package/src/core/policies/catalog/policy-catalog.ts +5 -1
- package/src/core/policies/context/context-builder.ts +20 -0
- package/src/core/policies/context/context-resolver.ts +5 -0
- package/src/core/policies/context/context-seed.ts +11 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
- package/src/core/policies/engines/parse-gate-payload.ts +9 -0
- package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +120 -291
- package/src/core/policies/engines/v1/condition-schema.ts +23 -19
- package/src/core/policies/engines/v1/condition-types.ts +18 -11
- package/src/core/policies/index.ts +4 -6
- package/src/core/policies/service/policy-service.ts +46 -35
- package/src/core/policies/utils/hash.ts +5 -69
- package/src/core/policies/utils/result.ts +1 -1
- package/src/core/rbac/access-request.ts +12 -2
- package/src/core/rbac/authorizer.ts +8 -1
- package/src/core/rbac/domain/role.ts +20 -0
- package/src/core/rbac/domain/scope.ts +6 -1
- package/src/core/result/index.ts +5 -0
- package/src/core/result/outcome.ts +5 -7
- package/src/core/result/result.ts +34 -1
- package/src/core/shared/hashing.ts +6 -57
- package/src/core/shared/immutable.ts +22 -4
- package/src/core/shared/stable-stringify.ts +144 -0
- package/src/core/shared/uuid.ts +16 -0
- package/src/core/versioning/domain-event-contracts.ts +43 -15
- package/src/core/versioning/index.ts +1 -0
- package/src/core/versioning/version.ts +30 -1
- package/src/domain/index.ts +5 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
- package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
- package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
- package/src/result/index.ts +7 -2
- package/src/version.ts +1 -1
- package/dist/domain-exception.d.cts +0 -7
- package/dist/domain-exception.d.ts +0 -7
- package/dist/entity.cjs +0 -126
- package/dist/entity.cjs.map +0 -1
- package/dist/entity.js +0 -115
- package/dist/entity.js.map +0 -1
- package/dist/use-case.cjs +0 -96
- package/dist/use-case.cjs.map +0 -1
- package/dist/use-case.js +0 -91
- package/dist/use-case.js.map +0 -1
package/dist/rule.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule.cjs","names":["coreConfig","ConditionEvaluatorV1","Result","AuthorizationError","ValidationField","ValueObject","InvalidValueException","ValidationCode"],"sources":["../src/core/abac/attributes.ts","../src/core/abac/combining.ts","../src/core/abac/authorizer.ts","../src/core/abac/composite-authorizer.ts","../src/core/abac/domain/policy-set.ts","../src/core/abac/domain/rule.ts"],"sourcesContent":["import type { PolicyContext } from \"../policies/engines/gate-types.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\n\n/**\n * Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the\n * condition evaluator reads (it resolves dotted fields like `resource.status` by\n * walking nested objects). The actor lands as `subject.id`; the four attribute\n * bags nest under `subject` / `resource` / `action` / `env`; a `resource`\n * reference adds `resource.type` / `resource.id`. The actor id and the resource\n * reference win over any same-named keys in the attribute bags.\n *\n * Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,\n * or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so\n * rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.\n */\nexport function abacContext(request: AbacRequest): PolicyContext {\n const attributes = request.attributes;\n\n const subject: Record<string, unknown> = {\n ...attributes.subject,\n id: request.actor.raw,\n };\n\n const resource: Record<string, unknown> = { ...attributes.resource };\n if (request.resource) {\n resource.type = request.resource.type;\n if (request.resource.id !== undefined) {\n resource.id = request.resource.id;\n }\n }\n\n return {\n subject,\n resource,\n action: { ...attributes.action },\n env: { ...attributes.environment },\n };\n}\n","import type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n/**\n * How an {@link AbacPolicySet} resolves several applicable rules into one effect.\n *\n * - `deny-overrides` (default, secure): any applicable DENY wins; otherwise the\n * first PERMIT; otherwise the set's default effect.\n * - `permit-overrides`: any applicable PERMIT wins; otherwise the first DENY;\n * otherwise the default effect.\n * - `first-applicable`: the effect of the first rule that matches; otherwise the\n * default effect.\n */\nexport type CombiningAlgorithm =\n | \"deny-overrides\"\n | \"permit-overrides\"\n | \"first-applicable\";\n\n/** The resolved decision plus the rule (if any) that produced it. */\nexport interface CombineResult {\n readonly effect: RuleEffect;\n readonly decidingRule?: AbacRule;\n}\n\nfunction firstWithEffect(\n rules: readonly AbacRule[],\n effect: RuleEffect,\n): AbacRule | undefined {\n return rules.find((rule) => rule.effect === effect);\n}\n\n/**\n * Applies `algorithm` to the already-matched `applicable` rules, falling back to\n * `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and\n * total; the returned `decidingRule` is absent exactly when `defaultEffect` was\n * used.\n */\nexport function combine(\n algorithm: CombiningAlgorithm,\n applicable: readonly AbacRule[],\n defaultEffect: RuleEffect,\n): CombineResult {\n switch (algorithm) {\n case \"deny-overrides\": {\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n return { effect: defaultEffect };\n }\n case \"permit-overrides\": {\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n return { effect: defaultEffect };\n }\n case \"first-applicable\": {\n const first = applicable[0];\n if (first) return { effect: first.effect, decidingRule: first };\n return { effect: defaultEffect };\n }\n }\n}\n","import { type CoreConfig, coreConfig } from \"../config/index.js\";\nimport { AuthorizationError } from \"../errors/authorization-error.js\";\nimport { ConditionEvaluatorV1 } from \"../policies/engines/v1/condition-evaluator.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\nimport { abacContext } from \"./attributes.js\";\nimport { combine, type CombiningAlgorithm } from \"./combining.js\";\nimport type { AbacPolicySet } from \"./domain/policy-set.js\";\nimport type { AbacRule } from \"./domain/rule.js\";\n\n// ABAC reuses the gate engine's v1 condition matcher; the version stamps the\n// evaluation reports the shared reporter emits.\nconst ENGINE_VERSION = 1;\n\n/** The successful decision returned by {@link AbacAuthorizer.authorize}. */\ninterface AbacDecision {\n /** The audited business action that was allowed. */\n readonly action: string;\n readonly effect: \"PERMIT\";\n /** The id of the rule that permitted, or `\"<default>\"` when the set's default did. */\n readonly matchedRule: string;\n readonly algorithm: CombiningAlgorithm;\n /** When the decision was made, ISO-8601. */\n readonly grantedAtIso: string;\n}\n\n/**\n * The pure ABAC decisor. Given an {@link AbacRequest} and an\n * {@link AbacPolicySet}, it flattens the request's attributes into a context,\n * matches each rule's condition with the gate engine's pure condition evaluator\n * (reused, not reimplemented), resolves the effects through the set's combining\n * algorithm, and returns a {@link Result} — never throwing, mirroring the\n * \"errors as values\" contract of `RbacAuthorizer`/`GateEngineV1`.\n *\n * A denial maps to {@link AuthorizationError.policyDenied}, attributed to the\n * deciding rule's `id`/`version`; a condition-evaluation failure (a missing\n * attribute, a wrong-typed operand) fails closed as\n * {@link AuthorizationError.forbidden}. Loading the dynamic attributes is the\n * consumer's job (behind an `AbacAuthorizerPort` adapter); this class only decides.\n */\nclass AbacAuthorizer {\n private readonly coreConfig: CoreConfig;\n\n constructor(params: { readonly coreConfig?: CoreConfig } = {}) {\n this.coreConfig = params.coreConfig ?? coreConfig;\n }\n\n authorize(\n request: AbacRequest,\n policies: AbacPolicySet,\n ): Result<AbacDecision, AuthorizationError> {\n const context = abacContext(request);\n const evaluator = new ConditionEvaluatorV1(\n context,\n this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION),\n );\n\n const applicable: AbacRule[] = [];\n for (const rule of policies.rules) {\n const matched = evaluator.evaluate(rule.condition);\n if (matched.isErr()) {\n // Fail closed: a technical evaluation error is never a silent PERMIT.\n return Result.err(\n AuthorizationError.forbidden({\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n details: matched.errorOrNull() ?? undefined,\n }),\n );\n }\n if (matched.getOrThrow()) {\n applicable.push(rule);\n }\n }\n\n const { effect, decidingRule } = combine(\n policies.algorithm,\n applicable,\n policies.defaultEffect,\n );\n\n if (effect === \"PERMIT\") {\n return Result.ok({\n action: request.action,\n effect: \"PERMIT\",\n matchedRule: decidingRule?.id ?? \"<default>\",\n algorithm: policies.algorithm,\n grantedAtIso: new Date().toISOString(),\n });\n }\n\n return Result.err(\n AuthorizationError.policyDenied({\n policyId: decidingRule?.id ?? \"<default-deny>\",\n policyVersion: decidingRule?.version ?? 0,\n evaluatedAtIso: new Date().toISOString(),\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n reasonCode: decidingRule?.id,\n }),\n );\n }\n}\n\nexport { AbacAuthorizer, type AbacDecision };\n","import { Result } from \"../result/result.js\";\nimport type { AbacAuthorizerPort } from \"../application/ports/abac-authorizer.port.js\";\nimport type { AuthorizerPort } from \"../application/ports/authorizer.port.js\";\nimport type { AuthorizationError } from \"../errors/authorization-error.js\";\nimport type { AccessRequest } from \"../rbac/access-request.js\";\n\nimport type { AbacAttributes } from \"./abac-request.js\";\n\n/**\n * The request a {@link CompositeAuthorizer} accepts — one object that satisfies\n * both the RBAC {@link AccessRequest} (its `required`/`scope`) and the ABAC\n * request (its `attributes`).\n */\ntype CompositeAccessRequest = AccessRequest & {\n readonly attributes: AbacAttributes;\n};\n\n/**\n * Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid\n * \"does the actor hold the capability, *and* do the attributes allow it here?\"\n * flow. Short-circuits with the RBAC denial when the actor is not even capable,\n * so the boundary sees the most specific 403.\n *\n * Only the two port interfaces are referenced (both erased at runtime), so this\n * pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the\n * consumer wires up.\n */\nclass CompositeAuthorizer {\n constructor(\n private readonly rbac: AuthorizerPort,\n private readonly abac: AbacAuthorizerPort,\n ) {}\n\n async authorize(\n request: CompositeAccessRequest,\n ): Promise<Result<void, AuthorizationError>> {\n const coarse = await this.rbac.authorize(request);\n if (coarse.isErr()) {\n return coarse;\n }\n return this.abac.authorize(request);\n }\n}\n\nexport { CompositeAuthorizer, type CompositeAccessRequest };\n","import type { CombiningAlgorithm } from \"../combining.js\";\n\nimport type { AbacRule, RuleEffect } from \"./rule.js\";\n\n/**\n * An ordered collection of {@link AbacRule}s plus how to combine them\n * ({@link CombiningAlgorithm}) and what to decide when no rule applies\n * ({@link defaultEffect}). This is where ABAC goes beyond a single gate\n * condition: several PERMIT/DENY rules resolved by an explicit algorithm.\n *\n * Closed by default — the combining algorithm defaults to `\"deny-overrides\"` and\n * the default effect to `\"DENY\"`, so a request matching nothing is denied. A\n * plain holder (not a value object); build through {@link of}.\n */\nclass AbacPolicySet {\n private constructor(\n private readonly _rules: readonly AbacRule[],\n private readonly _algorithm: CombiningAlgorithm,\n private readonly _defaultEffect: RuleEffect,\n ) {\n Object.freeze(this._rules);\n Object.freeze(this);\n }\n\n static of(\n rules: readonly AbacRule[],\n options: {\n readonly algorithm?: CombiningAlgorithm;\n readonly defaultEffect?: RuleEffect;\n } = {},\n ): AbacPolicySet {\n return new AbacPolicySet(\n [...rules],\n options.algorithm ?? \"deny-overrides\",\n options.defaultEffect ?? \"DENY\",\n );\n }\n\n get rules(): readonly AbacRule[] {\n return this._rules;\n }\n\n get algorithm(): CombiningAlgorithm {\n return this._algorithm;\n }\n\n get defaultEffect(): RuleEffect {\n return this._defaultEffect;\n }\n}\n\nexport { AbacPolicySet };\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\nimport type {\n ConditionNode,\n ConditionOp,\n} from \"../../policies/engines/v1/condition-types.js\";\n\n/** Whether a matching {@link AbacRule} permits or denies the action. */\ntype RuleEffect = \"PERMIT\" | \"DENY\";\n\n/**\n * The serializable shape of a rule: a stable `id`/`version` (surfaced on a\n * denial as `policyId`/`policyVersion`), the {@link RuleEffect}, and the\n * {@link ConditionNode} that decides whether the rule applies to a request's\n * flattened attributes.\n */\ntype AbacRuleProps = {\n readonly id: string;\n readonly version: number;\n readonly effect: RuleEffect;\n readonly condition: ConditionNode;\n readonly description?: string;\n};\n\nconst RULE_FIELD = ValidationField.of(\"abacRule\");\n\nconst CONDITION_OPS = new Set<ConditionOp>([\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"in\",\n \"notIn\",\n \"isNull\",\n \"isNotNull\",\n]);\n\nconst RULE_EFFECTS = new Set<RuleEffect>([\"PERMIT\", \"DENY\"]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A single attribute-based rule: an {@link RuleEffect} that takes hold when its\n * {@link ConditionNode} matches the request's flattened attributes. The\n * condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves\n * combined with `and`/`or`/`not`), so a consumer learns one condition language\n * across the whole kit.\n *\n * A zod-free value object: it validates its own shape on construction (throwing\n * {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are\n * authored in TypeScript — trusted data, not untrusted JSON — the structural\n * check is a hand-rolled walk of the condition tree rather than a schema parse.\n * Build one through {@link of}; the constructor is private.\n */\nclass AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {\n private constructor(props: AbacRuleProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Builds a rule, validating that `id` is non-blank, `version` is an integer\n * ≥ 1, `effect` is `\"PERMIT\"`/`\"DENY\"`, and `condition` is a well-formed\n * condition node. Throws {@link InvalidValueException} otherwise.\n */\n static of(props: AbacRuleProps): AbacRule {\n if (typeof props.id !== \"string\" || props.id.trim().length === 0) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"id\"),\n ValidationCode.BLANK,\n `abac rule id must be a non-empty string, got \"${props.id}\"`,\n );\n }\n\n if (!Number.isInteger(props.version) || props.version < 1) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"version\"),\n ValidationCode.OUT_OF_RANGE,\n `abac rule version must be an integer >= 1, got \"${props.version}\"`,\n );\n }\n\n if (!RULE_EFFECTS.has(props.effect)) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"effect\"),\n ValidationCode.INVALID_FORMAT,\n `abac rule effect must be \"PERMIT\" or \"DENY\", got \"${String(props.effect)}\"`,\n );\n }\n\n AbacRule.assertConditionShape(props.condition);\n\n return new AbacRule(props);\n }\n\n // Mirrors what the shared condition evaluator would reject at eval time\n // (empty and/or nodes, malformed leaves) but fails fast at construction.\n private static assertConditionShape(node: unknown): void {\n if (!isPlainObject(node)) {\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const key = \"and\" in node ? \"and\" : \"or\";\n const children = node[key];\n if (!Array.isArray(children) || children.length === 0) {\n AbacRule.rejectCondition(\n `abac rule \"${key}\" node must hold a non-empty array of child conditions`,\n );\n }\n for (const child of children) {\n AbacRule.assertConditionShape(child);\n }\n return;\n }\n\n if (\"not\" in node) {\n AbacRule.assertConditionShape(node.not);\n return;\n }\n\n if (\"field\" in node && \"op\" in node) {\n if (\n typeof node.field !== \"string\" ||\n node.field.trim().length === 0\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"field\" must be a non-empty string, got \"${String(node.field)}\"`,\n );\n }\n if (\n typeof node.op !== \"string\" ||\n !CONDITION_OPS.has(node.op as ConditionOp)\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"op\" is not a supported operator, got \"${String(node.op)}\"`,\n );\n }\n return;\n }\n\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n private static rejectCondition(message: string): never {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"condition\"),\n ValidationCode.INVALID_FORMAT,\n message,\n );\n }\n\n get id(): string {\n return this.value.id;\n }\n\n get version(): number {\n return this.value.version;\n }\n\n get effect(): RuleEffect {\n return this.value.effect;\n }\n\n /** The condition tree evaluated against a request's flattened attributes. */\n get condition(): ConditionNode {\n return this.value.condition as ConditionNode;\n }\n\n get description(): string | undefined {\n return this.value.description;\n }\n\n toPrimitive(): AbacRuleProps {\n return {\n id: this.value.id,\n version: this.value.version,\n effect: this.value.effect,\n condition: this.value.condition as ConditionNode,\n ...(this.value.description !== undefined\n ? { description: this.value.description }\n : {}),\n };\n }\n}\n\nexport { AbacRule, type AbacRuleProps, type RuleEffect };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rule.cjs","names":["coreConfig","ConditionEvaluatorV1","Result","AuthorizationError","ValidationField","InvalidValueException","ValidationCode","ValidationField","ValueObject","InvalidValueException","ValidationCode"],"sources":["../src/core/abac/attributes.ts","../src/core/abac/combining.ts","../src/core/abac/authorizer.ts","../src/core/abac/composite-authorizer.ts","../src/core/abac/domain/policy-set.ts","../src/core/policies/engines/v1/condition-types.ts","../src/core/abac/domain/rule.ts"],"sourcesContent":["import type { PolicyContext } from \"../policies/engines/gate-types.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\n\n/**\n * Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the\n * condition evaluator reads (it resolves dotted fields like `resource.status` by\n * walking nested objects). The actor lands as `subject.id`; the four attribute\n * bags nest under `subject` / `resource` / `action` / `env`; a `resource`\n * reference adds `resource.type` / `resource.id`. The actor id and the resource\n * reference win over any same-named keys in the attribute bags.\n *\n * Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,\n * or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so\n * rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.\n */\nexport function abacContext(request: AbacRequest): PolicyContext {\n const attributes = request.attributes;\n\n const subject: Record<string, unknown> = {\n ...attributes.subject,\n id: request.actor.raw,\n };\n\n const resource: Record<string, unknown> = { ...attributes.resource };\n if (request.resource) {\n resource.type = request.resource.type;\n if (request.resource.id !== undefined) {\n resource.id = request.resource.id;\n }\n }\n\n return {\n subject,\n resource,\n action: { ...attributes.action },\n env: { ...attributes.environment },\n };\n}\n","import type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n/**\n * How an {@link AbacPolicySet} resolves several applicable rules into one effect.\n *\n * - `deny-overrides` (default, secure): any applicable DENY wins; otherwise the\n * first PERMIT; otherwise the set's default effect.\n * - `permit-overrides`: any applicable PERMIT wins; otherwise the first DENY;\n * otherwise the default effect.\n * - `first-applicable`: the effect of the first rule that matches; otherwise the\n * default effect.\n */\nexport type CombiningAlgorithm =\n | \"deny-overrides\"\n | \"permit-overrides\"\n | \"first-applicable\";\n\n/** The resolved decision plus the rule (if any) that produced it. */\nexport interface CombineResult {\n readonly effect: RuleEffect;\n readonly decidingRule?: AbacRule;\n}\n\nfunction firstWithEffect(\n rules: readonly AbacRule[],\n effect: RuleEffect,\n): AbacRule | undefined {\n return rules.find((rule) => rule.effect === effect);\n}\n\n/**\n * Applies `algorithm` to the already-matched `applicable` rules, falling back to\n * `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and\n * total; the returned `decidingRule` is absent exactly when `defaultEffect` was\n * used.\n */\nexport function combine(\n algorithm: CombiningAlgorithm,\n applicable: readonly AbacRule[],\n defaultEffect: RuleEffect,\n): CombineResult {\n switch (algorithm) {\n case \"deny-overrides\": {\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n return { effect: defaultEffect };\n }\n case \"permit-overrides\": {\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n return { effect: defaultEffect };\n }\n case \"first-applicable\": {\n const first = applicable[0];\n if (first) return { effect: first.effect, decidingRule: first };\n return { effect: defaultEffect };\n }\n }\n}\n","import { type CoreConfig, coreConfig } from \"../config/index.js\";\nimport { AuthorizationError } from \"../errors/authorization-error.js\";\nimport { ConditionEvaluatorV1 } from \"../policies/engines/v1/condition-evaluator.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\nimport { abacContext } from \"./attributes.js\";\nimport { combine, type CombiningAlgorithm } from \"./combining.js\";\nimport type { AbacPolicySet } from \"./domain/policy-set.js\";\nimport type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n// ABAC reuses the gate engine's v1 condition matcher; the version stamps the\n// evaluation reports the shared reporter emits.\nconst ENGINE_VERSION = 1;\n\n/** The successful decision returned by {@link AbacAuthorizer.authorize}. */\ninterface AbacDecision {\n /** The audited business action that was allowed. */\n readonly action: string;\n readonly effect: \"PERMIT\";\n /** The id of the rule that permitted, or `\"<default>\"` when the set's default did. */\n readonly matchedRule: string;\n readonly algorithm: CombiningAlgorithm;\n /** When the decision was made, ISO-8601. */\n readonly grantedAtIso: string;\n}\n\n/**\n * The pure ABAC decisor. Given an {@link AbacRequest} and an\n * {@link AbacPolicySet}, it flattens the request's attributes into a context,\n * matches each rule's condition with the gate engine's pure condition evaluator\n * (reused, not reimplemented), resolves the effects through the set's combining\n * algorithm, and returns a {@link Result} — never throwing, mirroring the\n * \"errors as values\" contract of `RbacAuthorizer`/`GateEngineV1`.\n *\n * A denial maps to {@link AuthorizationError.policyDenied}, attributed to the\n * deciding rule's `id`/`version`. A condition-evaluation failure (a missing\n * attribute, a wrong-typed operand) fails closed as\n * {@link AuthorizationError.forbidden} — also attributed to the culprit rule's\n * `id`/`version` — unless the set opts into `onEvaluationError: \"skip-rule\"`, in\n * which case the unevaluable rule is skipped and the rest decide. Every resolved\n * decision (PERMIT and DENY) is emitted best-effort through the configured\n * reporter as an `abac-decision` event (silent by default). Timestamps come from\n * an injectable `clock` (default `() => new Date()`), so the decisor is pure\n * given one. Loading the dynamic attributes is the consumer's job (behind an\n * `AbacAuthorizerPort` adapter); this class only decides.\n */\nclass AbacAuthorizer {\n private readonly coreConfig: CoreConfig;\n private readonly clock: () => Date;\n\n constructor(\n params: {\n readonly coreConfig?: CoreConfig;\n /** Injectable clock for deterministic timestamps; defaults to `() => new Date()`. */\n readonly clock?: () => Date;\n } = {},\n ) {\n this.coreConfig = params.coreConfig ?? coreConfig;\n this.clock = params.clock ?? (() => new Date());\n }\n\n authorize(\n request: AbacRequest,\n policies: AbacPolicySet,\n ): Result<AbacDecision, AuthorizationError> {\n const context = abacContext(request);\n const evaluator = new ConditionEvaluatorV1(\n context,\n this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION),\n );\n\n const applicable: AbacRule[] = [];\n for (const rule of policies.rules) {\n const matched = evaluator.evaluate(rule.condition);\n if (matched.isErr()) {\n if (policies.onEvaluationError === \"skip-rule\") {\n // Escape valve for evolving rules: drop the unevaluable rule\n // (the evaluator already reported the anomaly) and let the\n // rest decide, instead of failing the whole set closed.\n continue;\n }\n // Fail closed: a technical evaluation error is never a silent\n // PERMIT. Attribute the deny to the culprit rule so the 403 is\n // triageable without turning on the reporter.\n return Result.err(\n AuthorizationError.forbidden({\n action: request.action,\n resource: request.resource,\n actor: { actorId: request.actor.raw },\n policyId: rule.id,\n policyVersion: rule.version,\n details: matched.errorOrNull() ?? undefined,\n }),\n );\n }\n if (matched.getOrThrow()) {\n applicable.push(rule);\n }\n }\n\n const { effect, decidingRule } = combine(\n policies.algorithm,\n applicable,\n policies.defaultEffect,\n );\n\n this.reportDecision(request, policies, effect, decidingRule);\n\n if (effect === \"PERMIT\") {\n return Result.ok({\n action: request.action,\n effect: \"PERMIT\",\n matchedRule: decidingRule?.id ?? \"<default>\",\n algorithm: policies.algorithm,\n grantedAtIso: this.clock().toISOString(),\n });\n }\n\n return Result.err(\n AuthorizationError.policyDenied({\n policyId: decidingRule?.id ?? \"<default-deny>\",\n policyVersion: decidingRule?.version ?? 0,\n evaluatedAtIso: this.clock().toISOString(),\n action: request.action,\n resource: request.resource,\n actor: { actorId: request.actor.raw },\n reasonCode: decidingRule?.id,\n }),\n );\n }\n\n // Best-effort audit trail: emits the resolved decision (PERMIT and DENY)\n // through the configured reporter, silent by default.\n private reportDecision(\n request: AbacRequest,\n policies: AbacPolicySet,\n effect: RuleEffect,\n decidingRule: AbacRule | undefined,\n ): void {\n this.coreConfig.reportSafely({\n kind: \"abac-decision\",\n level: \"info\",\n action: request.action,\n resource: request.resource,\n actorId: request.actor.raw,\n effect,\n decidingRuleId:\n decidingRule?.id ??\n (effect === \"PERMIT\" ? \"<default>\" : \"<default-deny>\"),\n decidingRuleVersion: decidingRule?.version,\n algorithm: policies.algorithm,\n occurredAt: this.clock(),\n });\n }\n}\n\nexport { AbacAuthorizer, type AbacDecision };\n","import { Result } from \"../result/result.js\";\nimport type { AbacAuthorizerPort } from \"../application/ports/abac-authorizer.port.js\";\nimport type { AuthorizerPort } from \"../application/ports/authorizer.port.js\";\nimport type { AuthorizationError } from \"../errors/authorization-error.js\";\nimport type { AccessRequest } from \"../rbac/access-request.js\";\n\nimport type { AbacAttributes } from \"./abac-request.js\";\n\n/**\n * The request a {@link CompositeAuthorizer} accepts — one object that satisfies\n * both the RBAC {@link AccessRequest} (its `required`/`scope`) and the ABAC\n * request (its `attributes`).\n */\ntype CompositeAccessRequest = AccessRequest & {\n readonly attributes: AbacAttributes;\n};\n\n/**\n * Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid\n * \"does the actor hold the capability, *and* do the attributes allow it here?\"\n * flow. Short-circuits with the RBAC denial when the actor is not even capable,\n * so the boundary sees the most specific 403.\n *\n * Only the two port interfaces are referenced (both erased at runtime), so this\n * pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the\n * consumer wires up.\n */\nclass CompositeAuthorizer {\n constructor(\n private readonly rbac: AuthorizerPort,\n private readonly abac: AbacAuthorizerPort,\n ) {}\n\n async authorize(\n request: CompositeAccessRequest,\n ): Promise<Result<void, AuthorizationError>> {\n const coarse = await this.rbac.authorize(request);\n if (coarse.isErr()) {\n return coarse;\n }\n return this.abac.authorize(request);\n }\n}\n\nexport { CompositeAuthorizer, type CompositeAccessRequest };\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport type { CombiningAlgorithm } from \"../combining.js\";\n\nimport type { AbacRule, RuleEffect } from \"./rule.js\";\n\n/**\n * How the {@link AbacAuthorizer} reacts when a rule's condition cannot be\n * evaluated (a referenced attribute is absent, an operand is wrong-typed).\n *\n * - `fail-closed` (default, secure): the whole decision fails closed as\n * `forbidden`. Safe, but it means adding a rule that reads an attribute a call\n * site does not populate makes that call site deny everything until it does.\n * - `skip-rule`: the unevaluable rule is dropped from the applicable set and the\n * remaining rules decide — the escape valve for evolving rules without\n * trailing every call site. The evaluator still reports the anomaly.\n */\nexport type OnEvaluationError = \"fail-closed\" | \"skip-rule\";\n\nconst POLICY_SET_FIELD = ValidationField.of(\"abacPolicySet\");\n\n/**\n * An ordered collection of {@link AbacRule}s plus how to combine them\n * ({@link CombiningAlgorithm}), what to decide when no rule applies\n * ({@link defaultEffect}), and how to react to an unevaluable rule\n * ({@link onEvaluationError}). This is where ABAC goes beyond a single gate\n * condition: several PERMIT/DENY rules resolved by an explicit algorithm.\n *\n * Closed by default — the combining algorithm defaults to `\"deny-overrides\"`,\n * the default effect to `\"DENY\"`, and evaluation errors to `\"fail-closed\"`, so a\n * request matching nothing (or hitting a technical error) is denied. A plain\n * holder (not a value object); build through {@link of}.\n */\nclass AbacPolicySet {\n private constructor(\n private readonly _rules: readonly AbacRule[],\n private readonly _algorithm: CombiningAlgorithm,\n private readonly _defaultEffect: RuleEffect,\n private readonly _onEvaluationError: OnEvaluationError,\n ) {\n Object.freeze(this._rules);\n Object.freeze(this);\n }\n\n /**\n * Builds a policy set. Rejects duplicate rule ids with\n * {@link InvalidValueException} so a denial's `policyId` attribution is\n * unambiguous.\n */\n static of(\n rules: readonly AbacRule[],\n options: {\n readonly algorithm?: CombiningAlgorithm;\n readonly defaultEffect?: RuleEffect;\n readonly onEvaluationError?: OnEvaluationError;\n } = {},\n ): AbacPolicySet {\n AbacPolicySet.assertUniqueIds(rules);\n return new AbacPolicySet(\n [...rules],\n options.algorithm ?? \"deny-overrides\",\n options.defaultEffect ?? \"DENY\",\n options.onEvaluationError ?? \"fail-closed\",\n );\n }\n\n private static assertUniqueIds(rules: readonly AbacRule[]): void {\n const seen = new Set<string>();\n for (const rule of rules) {\n if (seen.has(rule.id)) {\n throw new InvalidValueException(\n POLICY_SET_FIELD.nested(\"rules\"),\n ValidationCode.ALREADY_EXISTS,\n `abac policy set has duplicate rule id \"${rule.id}\"; ids must be unique for unambiguous audit attribution`,\n );\n }\n seen.add(rule.id);\n }\n }\n\n get rules(): readonly AbacRule[] {\n return this._rules;\n }\n\n get algorithm(): CombiningAlgorithm {\n return this._algorithm;\n }\n\n get defaultEffect(): RuleEffect {\n return this._defaultEffect;\n }\n\n get onEvaluationError(): OnEvaluationError {\n return this._onEvaluationError;\n }\n}\n\nexport { AbacPolicySet };\n","// ─── Shared v1 condition DSL types ──────────────────────────────────────────\n// These types belong to the v1 engine family: both gate/v1 and compute/v1\n// share the same condition-tree DSL. Keeping them here avoids making compute/v1\n// structurally dependent on gate/v1.\n\n// Single source of truth for the operator vocabulary: the union is derived from\n// this array so a new operator is added in exactly one place, and consumers that\n// need the runtime list (e.g. AbacRule's structural check) reuse it instead of\n// hand-maintaining a parallel set that can drift out of sync.\nexport const CONDITION_OPS = [\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"in\",\n \"notIn\",\n \"isNull\",\n \"isNotNull\",\n] as const;\n\nexport type ConditionOp = (typeof CONDITION_OPS)[number];\n\nexport interface ConditionLeafNode {\n readonly field: string;\n readonly op: ConditionOp;\n readonly value: unknown;\n readonly allowNull?: true;\n}\n\nexport interface ConditionAndNode {\n readonly and: readonly ConditionNode[];\n}\n\nexport interface ConditionOrNode {\n readonly or: readonly ConditionNode[];\n}\n\nexport interface ConditionNotNode {\n readonly not: ConditionNode;\n}\n\nexport type ConditionNode =\n | ConditionLeafNode\n | ConditionAndNode\n | ConditionOrNode\n | ConditionNotNode;\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\nimport {\n CONDITION_OPS,\n type ConditionNode,\n type ConditionOp,\n} from \"../../policies/engines/v1/condition-types.js\";\n\n/** Whether a matching {@link AbacRule} permits or denies the action. */\ntype RuleEffect = \"PERMIT\" | \"DENY\";\n\n/**\n * The serializable shape of a rule: a stable `id`/`version` (surfaced on a\n * denial as `policyId`/`policyVersion`), the {@link RuleEffect}, and the\n * {@link ConditionNode} that decides whether the rule applies to a request's\n * flattened attributes.\n */\ntype AbacRuleProps = {\n readonly id: string;\n readonly version: number;\n readonly effect: RuleEffect;\n readonly condition: ConditionNode;\n readonly description?: string;\n};\n\nconst RULE_FIELD = ValidationField.of(\"abacRule\");\n\n// Derived from the evaluator's own operator list (single source of truth) so a\n// new operator becomes usable in ABAC rules without a parallel edit here.\nconst SUPPORTED_OPS = new Set<ConditionOp>(CONDITION_OPS);\n\nconst RULE_EFFECTS = new Set<RuleEffect>([\"PERMIT\", \"DENY\"]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A single attribute-based rule: an {@link RuleEffect} that takes hold when its\n * {@link ConditionNode} matches the request's flattened attributes. The\n * condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves\n * combined with `and`/`or`/`not`), so a consumer learns one condition language\n * across the whole kit.\n *\n * A zod-free value object: it validates its own shape on construction (throwing\n * {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are\n * authored in TypeScript — trusted data, not untrusted JSON — the structural\n * check is a hand-rolled walk of the condition tree rather than a schema parse.\n * Build one through {@link of}; the constructor is private.\n *\n * **Attribute semantics a rule author must know (the condition is evaluated by\n * the shared gate/compute engine, whose runtime rules apply here too):**\n * - **A referenced attribute that is *absent* from the request is a technical\n * evaluation error, not a non-match.** By default (`onEvaluationError:\n * \"fail-closed\"` on the {@link AbacPolicySet}) that error fails the *entire*\n * decision closed — so adding a rule that reads an attribute a given call site\n * does not yet populate makes that call site start returning `forbidden`,\n * *even for requests the older rules used to permit*. Choose\n * `onEvaluationError: \"skip-rule\"` on the set to skip an unevaluable rule\n * instead of failing the whole set.\n * - **`isNull` / `isNotNull` test for an explicit `null`/`undefined` *value*, not\n * for a missing key.** A key that is absent from the bag never reaches the\n * operator — it short-circuits to the missing-attribute error above. To match\n * \"no deletedAt\", the consumer must put `deletedAt: null` in the bag, not omit\n * it. Pass `allowNull: true` on a relational leaf to treat a present `null` as\n * a non-match instead of an error.\n * - **Date comparison operands must be strict ISO-8601 UTC**\n * (`YYYY-MM-DDTHH:mm:ss.sssZ`); a bare `\"2026-07-09\"` or an offset like\n * `+00:00` is rejected as an invalid operand and fails closed.\n */\nclass AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {\n private constructor(props: AbacRuleProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Builds a rule, validating that `id` is non-blank, `version` is an integer\n * ≥ 1, `effect` is `\"PERMIT\"`/`\"DENY\"`, and `condition` is a well-formed\n * condition node. Throws {@link InvalidValueException} otherwise.\n */\n static of(props: AbacRuleProps): AbacRule {\n if (typeof props.id !== \"string\" || props.id.trim().length === 0) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"id\"),\n ValidationCode.BLANK,\n `abac rule id must be a non-empty string, got \"${props.id}\"`,\n );\n }\n\n if (!Number.isInteger(props.version) || props.version < 1) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"version\"),\n ValidationCode.OUT_OF_RANGE,\n `abac rule version must be an integer >= 1, got \"${props.version}\"`,\n );\n }\n\n if (!RULE_EFFECTS.has(props.effect)) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"effect\"),\n ValidationCode.INVALID_FORMAT,\n `abac rule effect must be \"PERMIT\" or \"DENY\", got \"${String(props.effect)}\"`,\n );\n }\n\n AbacRule.assertConditionShape(props.condition);\n\n return new AbacRule(props);\n }\n\n // Mirrors what the shared condition evaluator would reject at eval time\n // (empty and/or nodes, malformed leaves) but fails fast at construction.\n private static assertConditionShape(node: unknown): void {\n if (!isPlainObject(node)) {\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const key = \"and\" in node ? \"and\" : \"or\";\n const children = node[key];\n if (!Array.isArray(children) || children.length === 0) {\n AbacRule.rejectCondition(\n `abac rule \"${key}\" node must hold a non-empty array of child conditions`,\n );\n }\n for (const child of children) {\n AbacRule.assertConditionShape(child);\n }\n return;\n }\n\n if (\"not\" in node) {\n AbacRule.assertConditionShape(node.not);\n return;\n }\n\n if (\"field\" in node && \"op\" in node) {\n if (\n typeof node.field !== \"string\" ||\n node.field.trim().length === 0\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"field\" must be a non-empty string, got \"${String(node.field)}\"`,\n );\n }\n if (\n typeof node.op !== \"string\" ||\n !SUPPORTED_OPS.has(node.op as ConditionOp)\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"op\" is not a supported operator, got \"${String(node.op)}\"`,\n );\n }\n return;\n }\n\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n private static rejectCondition(message: string): never {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"condition\"),\n ValidationCode.INVALID_FORMAT,\n message,\n );\n }\n\n get id(): string {\n return this.value.id;\n }\n\n get version(): number {\n return this.value.version;\n }\n\n get effect(): RuleEffect {\n return this.value.effect;\n }\n\n /** The condition tree evaluated against a request's flattened attributes. */\n get condition(): ConditionNode {\n return this.value.condition as ConditionNode;\n }\n\n get description(): string | undefined {\n return this.value.description;\n }\n\n toPrimitive(): AbacRuleProps {\n return {\n id: this.value.id,\n version: this.value.version,\n effect: this.value.effect,\n condition: this.value.condition as ConditionNode,\n ...(this.value.description !== undefined\n ? { description: this.value.description }\n : {}),\n };\n }\n}\n\nexport { AbacRule, type AbacRuleProps, type RuleEffect };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgBA,SAAgB,YAAY,SAAqC;CAC7D,MAAM,aAAa,QAAQ;CAE3B,MAAM,UAAmC;EACrC,GAAG,WAAW;EACd,IAAI,QAAQ,MAAM;CACtB;CAEA,MAAM,WAAoC,EAAE,GAAG,WAAW,SAAS;CACnE,IAAI,QAAQ,UAAU;EAClB,SAAS,OAAO,QAAQ,SAAS;EACjC,IAAI,QAAQ,SAAS,OAAO,KAAA,GACxB,SAAS,KAAK,QAAQ,SAAS;CAEvC;CAEA,OAAO;EACH;EACA;EACA,QAAQ,EAAE,GAAG,WAAW,OAAO;EAC/B,KAAK,EAAE,GAAG,WAAW,YAAY;CACrC;AACJ;;;ACfA,SAAS,gBACL,OACA,QACoB;CACpB,OAAO,MAAM,MAAM,SAAS,KAAK,WAAW,MAAM;AACtD;;;;;;;AAQA,SAAgB,QACZ,WACA,YACA,eACa;CACb,QAAQ,WAAR;EACI,KAAK,kBAAkB;GACnB,MAAM,OAAO,gBAAgB,YAAY,MAAM;GAC/C,IAAI,MAAM,OAAO;IAAE,QAAQ;IAAQ,cAAc;GAAK;GACtD,MAAM,SAAS,gBAAgB,YAAY,QAAQ;GACnD,IAAI,QAAQ,OAAO;IAAE,QAAQ;IAAU,cAAc;GAAO;GAC5D,OAAO,EAAE,QAAQ,cAAc;EACnC;EACA,KAAK,oBAAoB;GACrB,MAAM,SAAS,gBAAgB,YAAY,QAAQ;GACnD,IAAI,QAAQ,OAAO;IAAE,QAAQ;IAAU,cAAc;GAAO;GAC5D,MAAM,OAAO,gBAAgB,YAAY,MAAM;GAC/C,IAAI,MAAM,OAAO;IAAE,QAAQ;IAAQ,cAAc;GAAK;GACtD,OAAO,EAAE,QAAQ,cAAc;EACnC;EACA,KAAK,oBAAoB;GACrB,MAAM,QAAQ,WAAW;GACzB,IAAI,OAAO,OAAO;IAAE,QAAQ,MAAM;IAAQ,cAAc;GAAM;GAC9D,OAAO,EAAE,QAAQ,cAAc;EACnC;CACJ;AACJ;;;ACjDA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;AAkCvB,IAAM,iBAAN,MAAqB;CAIjB,YACI,SAII,CAAC,GACP;EACE,KAAK,aAAa,OAAO,cAAcA,4BAAAA;EACvC,KAAK,QAAQ,OAAO,gCAAgB,IAAI,KAAK;CACjD;CAEA,UACI,SACA,UACwC;EAExC,MAAM,YAAY,IAAIC,4BAAAA,qBADN,YAAY,OAElB,GACN,KAAK,WAAW,8BAA8B,cAAc,CAChE;EAEA,MAAM,aAAyB,CAAC;EAChC,KAAK,MAAM,QAAQ,SAAS,OAAO;GAC/B,MAAM,UAAU,UAAU,SAAS,KAAK,SAAS;GACjD,IAAI,QAAQ,MAAM,GAAG;IACjB,IAAI,SAAS,sBAAsB,aAI/B;IAKJ,OAAOC,eAAAA,OAAO,IACVC,4BAAAA,mBAAmB,UAAU;KACzB,QAAQ,QAAQ;KAChB,UAAU,QAAQ;KAClB,OAAO,EAAE,SAAS,QAAQ,MAAM,IAAI;KACpC,UAAU,KAAK;KACf,eAAe,KAAK;KACpB,SAAS,QAAQ,YAAY,KAAK,KAAA;IACtC,CAAC,CACL;GACJ;GACA,IAAI,QAAQ,WAAW,GACnB,WAAW,KAAK,IAAI;EAE5B;EAEA,MAAM,EAAE,QAAQ,iBAAiB,QAC7B,SAAS,WACT,YACA,SAAS,aACb;EAEA,KAAK,eAAe,SAAS,UAAU,QAAQ,YAAY;EAE3D,IAAI,WAAW,UACX,OAAOD,eAAAA,OAAO,GAAG;GACb,QAAQ,QAAQ;GAChB,QAAQ;GACR,aAAa,cAAc,MAAM;GACjC,WAAW,SAAS;GACpB,cAAc,KAAK,MAAM,EAAE,YAAY;EAC3C,CAAC;EAGL,OAAOA,eAAAA,OAAO,IACVC,4BAAAA,mBAAmB,aAAa;GAC5B,UAAU,cAAc,MAAM;GAC9B,eAAe,cAAc,WAAW;GACxC,gBAAgB,KAAK,MAAM,EAAE,YAAY;GACzC,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,OAAO,EAAE,SAAS,QAAQ,MAAM,IAAI;GACpC,YAAY,cAAc;EAC9B,CAAC,CACL;CACJ;CAIA,eACI,SACA,UACA,QACA,cACI;EACJ,KAAK,WAAW,aAAa;GACzB,MAAM;GACN,OAAO;GACP,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,SAAS,QAAQ,MAAM;GACvB;GACA,gBACI,cAAc,OACb,WAAW,WAAW,cAAc;GACzC,qBAAqB,cAAc;GACnC,WAAW,SAAS;GACpB,YAAY,KAAK,MAAM;EAC3B,CAAC;CACL;AACJ;;;;;;;;;;;;;AChIA,IAAM,sBAAN,MAA0B;CACtB,YACI,MACA,MACF;EAFmB,KAAA,OAAA;EACA,KAAA,OAAA;CAClB;CAEH,MAAM,UACF,SACyC;EACzC,MAAM,SAAS,MAAM,KAAK,KAAK,UAAU,OAAO;EAChD,IAAI,OAAO,MAAM,GACb,OAAO;EAEX,OAAO,KAAK,KAAK,UAAU,OAAO;CACtC;AACJ;;;ACtBA,MAAM,mBAAmBC,yBAAAA,gBAAgB,GAAG,eAAe;;;;;;;;;;;;;AAc3D,IAAM,gBAAN,MAAM,cAAc;CAChB,YACI,QACA,YACA,gBACA,oBACF;EAJmB,KAAA,SAAA;EACA,KAAA,aAAA;EACA,KAAA,iBAAA;EACA,KAAA,qBAAA;EAEjB,OAAO,OAAO,KAAK,MAAM;EACzB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,OAAO,GACH,OACA,UAII,CAAC,GACQ;EACb,cAAc,gBAAgB,KAAK;EACnC,OAAO,IAAI,cACP,CAAC,GAAG,KAAK,GACT,QAAQ,aAAa,kBACrB,QAAQ,iBAAiB,QACzB,QAAQ,qBAAqB,aACjC;CACJ;CAEA,OAAe,gBAAgB,OAAkC;EAC7D,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,QAAQ,OAAO;GACtB,IAAI,KAAK,IAAI,KAAK,EAAE,GAChB,MAAM,IAAIC,6BAAAA,sBACN,iBAAiB,OAAO,OAAO,GAC/BC,wBAAAA,eAAe,gBACf,0CAA0C,KAAK,GAAG,wDACtD;GAEJ,KAAK,IAAI,KAAK,EAAE;EACpB;CACJ;CAEA,IAAI,QAA6B;EAC7B,OAAO,KAAK;CAChB;CAEA,IAAI,YAAgC;EAChC,OAAO,KAAK;CAChB;CAEA,IAAI,gBAA4B;EAC5B,OAAO,KAAK;CAChB;CAEA,IAAI,oBAAuC;EACvC,OAAO,KAAK;CAChB;AACJ;;;ACvFA,MAAa,gBAAgB;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;ACOA,MAAM,aAAaC,yBAAAA,gBAAgB,GAAG,UAAU;AAIhD,MAAM,gBAAgB,IAAI,IAAiB,aAAa;AAExD,MAAM,eAAe,IAAI,IAAgB,CAAC,UAAU,MAAM,CAAC;AAE3D,SAAS,cAAc,OAAkD;CACrE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,IAAM,WAAN,MAAM,iBAAiBC,qBAAAA,YAA0C;CAC7D,YAAoB,OAAsB;EACtC,MAAM,KAAK;EACX,KAAK,SAAS;CAClB;;;;;;CAOA,OAAO,GAAG,OAAgC;EACtC,IAAI,OAAO,MAAM,OAAO,YAAY,MAAM,GAAG,KAAK,EAAE,WAAW,GAC3D,MAAM,IAAIC,6BAAAA,sBACN,WAAW,OAAO,IAAI,GACtBC,wBAAAA,eAAe,OACf,iDAAiD,MAAM,GAAG,EAC9D;EAGJ,IAAI,CAAC,OAAO,UAAU,MAAM,OAAO,KAAK,MAAM,UAAU,GACpD,MAAM,IAAID,6BAAAA,sBACN,WAAW,OAAO,SAAS,GAC3BC,wBAAAA,eAAe,cACf,mDAAmD,MAAM,QAAQ,EACrE;EAGJ,IAAI,CAAC,aAAa,IAAI,MAAM,MAAM,GAC9B,MAAM,IAAID,6BAAAA,sBACN,WAAW,OAAO,QAAQ,GAC1BC,wBAAAA,eAAe,gBACf,qDAAqD,OAAO,MAAM,MAAM,EAAE,EAC9E;EAGJ,SAAS,qBAAqB,MAAM,SAAS;EAE7C,OAAO,IAAI,SAAS,KAAK;CAC7B;CAIA,OAAe,qBAAqB,MAAqB;EACrD,IAAI,CAAC,cAAc,IAAI,GACnB,SAAS,gBACL,gFACJ;EAGJ,IAAI,SAAS,QAAQ,QAAQ,MAAM;GAC/B,MAAM,MAAM,SAAS,OAAO,QAAQ;GACpC,MAAM,WAAW,KAAK;GACtB,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,GAChD,SAAS,gBACL,cAAc,IAAI,uDACtB;GAEJ,KAAK,MAAM,SAAS,UAChB,SAAS,qBAAqB,KAAK;GAEvC;EACJ;EAEA,IAAI,SAAS,MAAM;GACf,SAAS,qBAAqB,KAAK,GAAG;GACtC;EACJ;EAEA,IAAI,WAAW,QAAQ,QAAQ,MAAM;GACjC,IACI,OAAO,KAAK,UAAU,YACtB,KAAK,MAAM,KAAK,EAAE,WAAW,GAE7B,SAAS,gBACL,qEAAqE,OAAO,KAAK,KAAK,EAAE,EAC5F;GAEJ,IACI,OAAO,KAAK,OAAO,YACnB,CAAC,cAAc,IAAI,KAAK,EAAiB,GAEzC,SAAS,gBACL,mEAAmE,OAAO,KAAK,EAAE,EAAE,EACvF;GAEJ;EACJ;EAEA,SAAS,gBACL,gFACJ;CACJ;CAEA,OAAe,gBAAgB,SAAwB;EACnD,MAAM,IAAID,6BAAAA,sBACN,WAAW,OAAO,WAAW,GAC7BC,wBAAAA,eAAe,gBACf,OACJ;CACJ;CAEA,IAAI,KAAa;EACb,OAAO,KAAK,MAAM;CACtB;CAEA,IAAI,UAAkB;EAClB,OAAO,KAAK,MAAM;CACtB;CAEA,IAAI,SAAqB;EACrB,OAAO,KAAK,MAAM;CACtB;;CAGA,IAAI,YAA2B;EAC3B,OAAO,KAAK,MAAM;CACtB;CAEA,IAAI,cAAkC;EAClC,OAAO,KAAK,MAAM;CACtB;CAEA,cAA6B;EACzB,OAAO;GACH,IAAI,KAAK,MAAM;GACf,SAAS,KAAK,MAAM;GACpB,QAAQ,KAAK,MAAM;GACnB,WAAW,KAAK,MAAM;GACtB,GAAI,KAAK,MAAM,gBAAgB,KAAA,IACzB,EAAE,aAAa,KAAK,MAAM,YAAY,IACtC,CAAC;EACX;CACJ;AACJ"}
|
package/dist/rule.js
CHANGED
|
@@ -97,46 +97,73 @@ const ENGINE_VERSION = 1;
|
|
|
97
97
|
* "errors as values" contract of `RbacAuthorizer`/`GateEngineV1`.
|
|
98
98
|
*
|
|
99
99
|
* A denial maps to {@link AuthorizationError.policyDenied}, attributed to the
|
|
100
|
-
* deciding rule's `id`/`version
|
|
100
|
+
* deciding rule's `id`/`version`. A condition-evaluation failure (a missing
|
|
101
101
|
* attribute, a wrong-typed operand) fails closed as
|
|
102
|
-
* {@link AuthorizationError.forbidden}
|
|
103
|
-
*
|
|
102
|
+
* {@link AuthorizationError.forbidden} — also attributed to the culprit rule's
|
|
103
|
+
* `id`/`version` — unless the set opts into `onEvaluationError: "skip-rule"`, in
|
|
104
|
+
* which case the unevaluable rule is skipped and the rest decide. Every resolved
|
|
105
|
+
* decision (PERMIT and DENY) is emitted best-effort through the configured
|
|
106
|
+
* reporter as an `abac-decision` event (silent by default). Timestamps come from
|
|
107
|
+
* an injectable `clock` (default `() => new Date()`), so the decisor is pure
|
|
108
|
+
* given one. Loading the dynamic attributes is the consumer's job (behind an
|
|
109
|
+
* `AbacAuthorizerPort` adapter); this class only decides.
|
|
104
110
|
*/
|
|
105
111
|
var AbacAuthorizer = class {
|
|
106
112
|
constructor(params = {}) {
|
|
107
113
|
this.coreConfig = params.coreConfig ?? coreConfig;
|
|
114
|
+
this.clock = params.clock ?? (() => /* @__PURE__ */ new Date());
|
|
108
115
|
}
|
|
109
116
|
authorize(request, policies) {
|
|
110
117
|
const evaluator = new ConditionEvaluatorV1(abacContext(request), this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION));
|
|
111
118
|
const applicable = [];
|
|
112
119
|
for (const rule of policies.rules) {
|
|
113
120
|
const matched = evaluator.evaluate(rule.condition);
|
|
114
|
-
if (matched.isErr())
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
if (matched.isErr()) {
|
|
122
|
+
if (policies.onEvaluationError === "skip-rule") continue;
|
|
123
|
+
return Result.err(AuthorizationError.forbidden({
|
|
124
|
+
action: request.action,
|
|
125
|
+
resource: request.resource,
|
|
126
|
+
actor: { actorId: request.actor.raw },
|
|
127
|
+
policyId: rule.id,
|
|
128
|
+
policyVersion: rule.version,
|
|
129
|
+
details: matched.errorOrNull() ?? void 0
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
120
132
|
if (matched.getOrThrow()) applicable.push(rule);
|
|
121
133
|
}
|
|
122
134
|
const { effect, decidingRule } = combine(policies.algorithm, applicable, policies.defaultEffect);
|
|
135
|
+
this.reportDecision(request, policies, effect, decidingRule);
|
|
123
136
|
if (effect === "PERMIT") return Result.ok({
|
|
124
137
|
action: request.action,
|
|
125
138
|
effect: "PERMIT",
|
|
126
139
|
matchedRule: decidingRule?.id ?? "<default>",
|
|
127
140
|
algorithm: policies.algorithm,
|
|
128
|
-
grantedAtIso: (
|
|
141
|
+
grantedAtIso: this.clock().toISOString()
|
|
129
142
|
});
|
|
130
143
|
return Result.err(AuthorizationError.policyDenied({
|
|
131
144
|
policyId: decidingRule?.id ?? "<default-deny>",
|
|
132
145
|
policyVersion: decidingRule?.version ?? 0,
|
|
133
|
-
evaluatedAtIso: (
|
|
146
|
+
evaluatedAtIso: this.clock().toISOString(),
|
|
134
147
|
action: request.action,
|
|
135
148
|
resource: request.resource,
|
|
136
|
-
actor: {
|
|
149
|
+
actor: { actorId: request.actor.raw },
|
|
137
150
|
reasonCode: decidingRule?.id
|
|
138
151
|
}));
|
|
139
152
|
}
|
|
153
|
+
reportDecision(request, policies, effect, decidingRule) {
|
|
154
|
+
this.coreConfig.reportSafely({
|
|
155
|
+
kind: "abac-decision",
|
|
156
|
+
level: "info",
|
|
157
|
+
action: request.action,
|
|
158
|
+
resource: request.resource,
|
|
159
|
+
actorId: request.actor.raw,
|
|
160
|
+
effect,
|
|
161
|
+
decidingRuleId: decidingRule?.id ?? (effect === "PERMIT" ? "<default>" : "<default-deny>"),
|
|
162
|
+
decidingRuleVersion: decidingRule?.version,
|
|
163
|
+
algorithm: policies.algorithm,
|
|
164
|
+
occurredAt: this.clock()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
140
167
|
};
|
|
141
168
|
//#endregion
|
|
142
169
|
//#region src/core/abac/composite-authorizer.ts
|
|
@@ -163,26 +190,43 @@ var CompositeAuthorizer = class {
|
|
|
163
190
|
};
|
|
164
191
|
//#endregion
|
|
165
192
|
//#region src/core/abac/domain/policy-set.ts
|
|
193
|
+
const POLICY_SET_FIELD = ValidationField.of("abacPolicySet");
|
|
166
194
|
/**
|
|
167
195
|
* An ordered collection of {@link AbacRule}s plus how to combine them
|
|
168
|
-
* ({@link CombiningAlgorithm})
|
|
169
|
-
* ({@link defaultEffect})
|
|
196
|
+
* ({@link CombiningAlgorithm}), what to decide when no rule applies
|
|
197
|
+
* ({@link defaultEffect}), and how to react to an unevaluable rule
|
|
198
|
+
* ({@link onEvaluationError}). This is where ABAC goes beyond a single gate
|
|
170
199
|
* condition: several PERMIT/DENY rules resolved by an explicit algorithm.
|
|
171
200
|
*
|
|
172
|
-
* Closed by default — the combining algorithm defaults to `"deny-overrides"
|
|
173
|
-
* the default effect to `"DENY"`,
|
|
174
|
-
*
|
|
201
|
+
* Closed by default — the combining algorithm defaults to `"deny-overrides"`,
|
|
202
|
+
* the default effect to `"DENY"`, and evaluation errors to `"fail-closed"`, so a
|
|
203
|
+
* request matching nothing (or hitting a technical error) is denied. A plain
|
|
204
|
+
* holder (not a value object); build through {@link of}.
|
|
175
205
|
*/
|
|
176
206
|
var AbacPolicySet = class AbacPolicySet {
|
|
177
|
-
constructor(_rules, _algorithm, _defaultEffect) {
|
|
207
|
+
constructor(_rules, _algorithm, _defaultEffect, _onEvaluationError) {
|
|
178
208
|
this._rules = _rules;
|
|
179
209
|
this._algorithm = _algorithm;
|
|
180
210
|
this._defaultEffect = _defaultEffect;
|
|
211
|
+
this._onEvaluationError = _onEvaluationError;
|
|
181
212
|
Object.freeze(this._rules);
|
|
182
213
|
Object.freeze(this);
|
|
183
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Builds a policy set. Rejects duplicate rule ids with
|
|
217
|
+
* {@link InvalidValueException} so a denial's `policyId` attribution is
|
|
218
|
+
* unambiguous.
|
|
219
|
+
*/
|
|
184
220
|
static of(rules, options = {}) {
|
|
185
|
-
|
|
221
|
+
AbacPolicySet.assertUniqueIds(rules);
|
|
222
|
+
return new AbacPolicySet([...rules], options.algorithm ?? "deny-overrides", options.defaultEffect ?? "DENY", options.onEvaluationError ?? "fail-closed");
|
|
223
|
+
}
|
|
224
|
+
static assertUniqueIds(rules) {
|
|
225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226
|
+
for (const rule of rules) {
|
|
227
|
+
if (seen.has(rule.id)) throw new InvalidValueException(POLICY_SET_FIELD.nested("rules"), ValidationCode.ALREADY_EXISTS, `abac policy set has duplicate rule id "${rule.id}"; ids must be unique for unambiguous audit attribution`);
|
|
228
|
+
seen.add(rule.id);
|
|
229
|
+
}
|
|
186
230
|
}
|
|
187
231
|
get rules() {
|
|
188
232
|
return this._rules;
|
|
@@ -193,11 +237,13 @@ var AbacPolicySet = class AbacPolicySet {
|
|
|
193
237
|
get defaultEffect() {
|
|
194
238
|
return this._defaultEffect;
|
|
195
239
|
}
|
|
240
|
+
get onEvaluationError() {
|
|
241
|
+
return this._onEvaluationError;
|
|
242
|
+
}
|
|
196
243
|
};
|
|
197
244
|
//#endregion
|
|
198
|
-
//#region src/core/
|
|
199
|
-
const
|
|
200
|
-
const CONDITION_OPS = new Set([
|
|
245
|
+
//#region src/core/policies/engines/v1/condition-types.ts
|
|
246
|
+
const CONDITION_OPS = [
|
|
201
247
|
"eq",
|
|
202
248
|
"neq",
|
|
203
249
|
"gt",
|
|
@@ -208,7 +254,11 @@ const CONDITION_OPS = new Set([
|
|
|
208
254
|
"notIn",
|
|
209
255
|
"isNull",
|
|
210
256
|
"isNotNull"
|
|
211
|
-
]
|
|
257
|
+
];
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/core/abac/domain/rule.ts
|
|
260
|
+
const RULE_FIELD = ValidationField.of("abacRule");
|
|
261
|
+
const SUPPORTED_OPS = new Set(CONDITION_OPS);
|
|
212
262
|
const RULE_EFFECTS = new Set(["PERMIT", "DENY"]);
|
|
213
263
|
function isPlainObject(value) {
|
|
214
264
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -225,6 +275,26 @@ function isPlainObject(value) {
|
|
|
225
275
|
* authored in TypeScript — trusted data, not untrusted JSON — the structural
|
|
226
276
|
* check is a hand-rolled walk of the condition tree rather than a schema parse.
|
|
227
277
|
* Build one through {@link of}; the constructor is private.
|
|
278
|
+
*
|
|
279
|
+
* **Attribute semantics a rule author must know (the condition is evaluated by
|
|
280
|
+
* the shared gate/compute engine, whose runtime rules apply here too):**
|
|
281
|
+
* - **A referenced attribute that is *absent* from the request is a technical
|
|
282
|
+
* evaluation error, not a non-match.** By default (`onEvaluationError:
|
|
283
|
+
* "fail-closed"` on the {@link AbacPolicySet}) that error fails the *entire*
|
|
284
|
+
* decision closed — so adding a rule that reads an attribute a given call site
|
|
285
|
+
* does not yet populate makes that call site start returning `forbidden`,
|
|
286
|
+
* *even for requests the older rules used to permit*. Choose
|
|
287
|
+
* `onEvaluationError: "skip-rule"` on the set to skip an unevaluable rule
|
|
288
|
+
* instead of failing the whole set.
|
|
289
|
+
* - **`isNull` / `isNotNull` test for an explicit `null`/`undefined` *value*, not
|
|
290
|
+
* for a missing key.** A key that is absent from the bag never reaches the
|
|
291
|
+
* operator — it short-circuits to the missing-attribute error above. To match
|
|
292
|
+
* "no deletedAt", the consumer must put `deletedAt: null` in the bag, not omit
|
|
293
|
+
* it. Pass `allowNull: true` on a relational leaf to treat a present `null` as
|
|
294
|
+
* a non-match instead of an error.
|
|
295
|
+
* - **Date comparison operands must be strict ISO-8601 UTC**
|
|
296
|
+
* (`YYYY-MM-DDTHH:mm:ss.sssZ`); a bare `"2026-07-09"` or an offset like
|
|
297
|
+
* `+00:00` is rejected as an invalid operand and fails closed.
|
|
228
298
|
*/
|
|
229
299
|
var AbacRule = class AbacRule extends ValueObject {
|
|
230
300
|
constructor(props) {
|
|
@@ -258,7 +328,7 @@ var AbacRule = class AbacRule extends ValueObject {
|
|
|
258
328
|
}
|
|
259
329
|
if ("field" in node && "op" in node) {
|
|
260
330
|
if (typeof node.field !== "string" || node.field.trim().length === 0) AbacRule.rejectCondition(`abac rule condition leaf "field" must be a non-empty string, got "${String(node.field)}"`);
|
|
261
|
-
if (typeof node.op !== "string" || !
|
|
331
|
+
if (typeof node.op !== "string" || !SUPPORTED_OPS.has(node.op)) AbacRule.rejectCondition(`abac rule condition leaf "op" is not a supported operator, got "${String(node.op)}"`);
|
|
262
332
|
return;
|
|
263
333
|
}
|
|
264
334
|
AbacRule.rejectCondition("abac rule condition must be a leaf { field, op } or an { and | or | not } node");
|
package/dist/rule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule.js","names":[],"sources":["../src/core/abac/attributes.ts","../src/core/abac/combining.ts","../src/core/abac/authorizer.ts","../src/core/abac/composite-authorizer.ts","../src/core/abac/domain/policy-set.ts","../src/core/abac/domain/rule.ts"],"sourcesContent":["import type { PolicyContext } from \"../policies/engines/gate-types.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\n\n/**\n * Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the\n * condition evaluator reads (it resolves dotted fields like `resource.status` by\n * walking nested objects). The actor lands as `subject.id`; the four attribute\n * bags nest under `subject` / `resource` / `action` / `env`; a `resource`\n * reference adds `resource.type` / `resource.id`. The actor id and the resource\n * reference win over any same-named keys in the attribute bags.\n *\n * Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,\n * or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so\n * rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.\n */\nexport function abacContext(request: AbacRequest): PolicyContext {\n const attributes = request.attributes;\n\n const subject: Record<string, unknown> = {\n ...attributes.subject,\n id: request.actor.raw,\n };\n\n const resource: Record<string, unknown> = { ...attributes.resource };\n if (request.resource) {\n resource.type = request.resource.type;\n if (request.resource.id !== undefined) {\n resource.id = request.resource.id;\n }\n }\n\n return {\n subject,\n resource,\n action: { ...attributes.action },\n env: { ...attributes.environment },\n };\n}\n","import type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n/**\n * How an {@link AbacPolicySet} resolves several applicable rules into one effect.\n *\n * - `deny-overrides` (default, secure): any applicable DENY wins; otherwise the\n * first PERMIT; otherwise the set's default effect.\n * - `permit-overrides`: any applicable PERMIT wins; otherwise the first DENY;\n * otherwise the default effect.\n * - `first-applicable`: the effect of the first rule that matches; otherwise the\n * default effect.\n */\nexport type CombiningAlgorithm =\n | \"deny-overrides\"\n | \"permit-overrides\"\n | \"first-applicable\";\n\n/** The resolved decision plus the rule (if any) that produced it. */\nexport interface CombineResult {\n readonly effect: RuleEffect;\n readonly decidingRule?: AbacRule;\n}\n\nfunction firstWithEffect(\n rules: readonly AbacRule[],\n effect: RuleEffect,\n): AbacRule | undefined {\n return rules.find((rule) => rule.effect === effect);\n}\n\n/**\n * Applies `algorithm` to the already-matched `applicable` rules, falling back to\n * `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and\n * total; the returned `decidingRule` is absent exactly when `defaultEffect` was\n * used.\n */\nexport function combine(\n algorithm: CombiningAlgorithm,\n applicable: readonly AbacRule[],\n defaultEffect: RuleEffect,\n): CombineResult {\n switch (algorithm) {\n case \"deny-overrides\": {\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n return { effect: defaultEffect };\n }\n case \"permit-overrides\": {\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n return { effect: defaultEffect };\n }\n case \"first-applicable\": {\n const first = applicable[0];\n if (first) return { effect: first.effect, decidingRule: first };\n return { effect: defaultEffect };\n }\n }\n}\n","import { type CoreConfig, coreConfig } from \"../config/index.js\";\nimport { AuthorizationError } from \"../errors/authorization-error.js\";\nimport { ConditionEvaluatorV1 } from \"../policies/engines/v1/condition-evaluator.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\nimport { abacContext } from \"./attributes.js\";\nimport { combine, type CombiningAlgorithm } from \"./combining.js\";\nimport type { AbacPolicySet } from \"./domain/policy-set.js\";\nimport type { AbacRule } from \"./domain/rule.js\";\n\n// ABAC reuses the gate engine's v1 condition matcher; the version stamps the\n// evaluation reports the shared reporter emits.\nconst ENGINE_VERSION = 1;\n\n/** The successful decision returned by {@link AbacAuthorizer.authorize}. */\ninterface AbacDecision {\n /** The audited business action that was allowed. */\n readonly action: string;\n readonly effect: \"PERMIT\";\n /** The id of the rule that permitted, or `\"<default>\"` when the set's default did. */\n readonly matchedRule: string;\n readonly algorithm: CombiningAlgorithm;\n /** When the decision was made, ISO-8601. */\n readonly grantedAtIso: string;\n}\n\n/**\n * The pure ABAC decisor. Given an {@link AbacRequest} and an\n * {@link AbacPolicySet}, it flattens the request's attributes into a context,\n * matches each rule's condition with the gate engine's pure condition evaluator\n * (reused, not reimplemented), resolves the effects through the set's combining\n * algorithm, and returns a {@link Result} — never throwing, mirroring the\n * \"errors as values\" contract of `RbacAuthorizer`/`GateEngineV1`.\n *\n * A denial maps to {@link AuthorizationError.policyDenied}, attributed to the\n * deciding rule's `id`/`version`; a condition-evaluation failure (a missing\n * attribute, a wrong-typed operand) fails closed as\n * {@link AuthorizationError.forbidden}. Loading the dynamic attributes is the\n * consumer's job (behind an `AbacAuthorizerPort` adapter); this class only decides.\n */\nclass AbacAuthorizer {\n private readonly coreConfig: CoreConfig;\n\n constructor(params: { readonly coreConfig?: CoreConfig } = {}) {\n this.coreConfig = params.coreConfig ?? coreConfig;\n }\n\n authorize(\n request: AbacRequest,\n policies: AbacPolicySet,\n ): Result<AbacDecision, AuthorizationError> {\n const context = abacContext(request);\n const evaluator = new ConditionEvaluatorV1(\n context,\n this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION),\n );\n\n const applicable: AbacRule[] = [];\n for (const rule of policies.rules) {\n const matched = evaluator.evaluate(rule.condition);\n if (matched.isErr()) {\n // Fail closed: a technical evaluation error is never a silent PERMIT.\n return Result.err(\n AuthorizationError.forbidden({\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n details: matched.errorOrNull() ?? undefined,\n }),\n );\n }\n if (matched.getOrThrow()) {\n applicable.push(rule);\n }\n }\n\n const { effect, decidingRule } = combine(\n policies.algorithm,\n applicable,\n policies.defaultEffect,\n );\n\n if (effect === \"PERMIT\") {\n return Result.ok({\n action: request.action,\n effect: \"PERMIT\",\n matchedRule: decidingRule?.id ?? \"<default>\",\n algorithm: policies.algorithm,\n grantedAtIso: new Date().toISOString(),\n });\n }\n\n return Result.err(\n AuthorizationError.policyDenied({\n policyId: decidingRule?.id ?? \"<default-deny>\",\n policyVersion: decidingRule?.version ?? 0,\n evaluatedAtIso: new Date().toISOString(),\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n reasonCode: decidingRule?.id,\n }),\n );\n }\n}\n\nexport { AbacAuthorizer, type AbacDecision };\n","import { Result } from \"../result/result.js\";\nimport type { AbacAuthorizerPort } from \"../application/ports/abac-authorizer.port.js\";\nimport type { AuthorizerPort } from \"../application/ports/authorizer.port.js\";\nimport type { AuthorizationError } from \"../errors/authorization-error.js\";\nimport type { AccessRequest } from \"../rbac/access-request.js\";\n\nimport type { AbacAttributes } from \"./abac-request.js\";\n\n/**\n * The request a {@link CompositeAuthorizer} accepts — one object that satisfies\n * both the RBAC {@link AccessRequest} (its `required`/`scope`) and the ABAC\n * request (its `attributes`).\n */\ntype CompositeAccessRequest = AccessRequest & {\n readonly attributes: AbacAttributes;\n};\n\n/**\n * Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid\n * \"does the actor hold the capability, *and* do the attributes allow it here?\"\n * flow. Short-circuits with the RBAC denial when the actor is not even capable,\n * so the boundary sees the most specific 403.\n *\n * Only the two port interfaces are referenced (both erased at runtime), so this\n * pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the\n * consumer wires up.\n */\nclass CompositeAuthorizer {\n constructor(\n private readonly rbac: AuthorizerPort,\n private readonly abac: AbacAuthorizerPort,\n ) {}\n\n async authorize(\n request: CompositeAccessRequest,\n ): Promise<Result<void, AuthorizationError>> {\n const coarse = await this.rbac.authorize(request);\n if (coarse.isErr()) {\n return coarse;\n }\n return this.abac.authorize(request);\n }\n}\n\nexport { CompositeAuthorizer, type CompositeAccessRequest };\n","import type { CombiningAlgorithm } from \"../combining.js\";\n\nimport type { AbacRule, RuleEffect } from \"./rule.js\";\n\n/**\n * An ordered collection of {@link AbacRule}s plus how to combine them\n * ({@link CombiningAlgorithm}) and what to decide when no rule applies\n * ({@link defaultEffect}). This is where ABAC goes beyond a single gate\n * condition: several PERMIT/DENY rules resolved by an explicit algorithm.\n *\n * Closed by default — the combining algorithm defaults to `\"deny-overrides\"` and\n * the default effect to `\"DENY\"`, so a request matching nothing is denied. A\n * plain holder (not a value object); build through {@link of}.\n */\nclass AbacPolicySet {\n private constructor(\n private readonly _rules: readonly AbacRule[],\n private readonly _algorithm: CombiningAlgorithm,\n private readonly _defaultEffect: RuleEffect,\n ) {\n Object.freeze(this._rules);\n Object.freeze(this);\n }\n\n static of(\n rules: readonly AbacRule[],\n options: {\n readonly algorithm?: CombiningAlgorithm;\n readonly defaultEffect?: RuleEffect;\n } = {},\n ): AbacPolicySet {\n return new AbacPolicySet(\n [...rules],\n options.algorithm ?? \"deny-overrides\",\n options.defaultEffect ?? \"DENY\",\n );\n }\n\n get rules(): readonly AbacRule[] {\n return this._rules;\n }\n\n get algorithm(): CombiningAlgorithm {\n return this._algorithm;\n }\n\n get defaultEffect(): RuleEffect {\n return this._defaultEffect;\n }\n}\n\nexport { AbacPolicySet };\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\nimport type {\n ConditionNode,\n ConditionOp,\n} from \"../../policies/engines/v1/condition-types.js\";\n\n/** Whether a matching {@link AbacRule} permits or denies the action. */\ntype RuleEffect = \"PERMIT\" | \"DENY\";\n\n/**\n * The serializable shape of a rule: a stable `id`/`version` (surfaced on a\n * denial as `policyId`/`policyVersion`), the {@link RuleEffect}, and the\n * {@link ConditionNode} that decides whether the rule applies to a request's\n * flattened attributes.\n */\ntype AbacRuleProps = {\n readonly id: string;\n readonly version: number;\n readonly effect: RuleEffect;\n readonly condition: ConditionNode;\n readonly description?: string;\n};\n\nconst RULE_FIELD = ValidationField.of(\"abacRule\");\n\nconst CONDITION_OPS = new Set<ConditionOp>([\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"in\",\n \"notIn\",\n \"isNull\",\n \"isNotNull\",\n]);\n\nconst RULE_EFFECTS = new Set<RuleEffect>([\"PERMIT\", \"DENY\"]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A single attribute-based rule: an {@link RuleEffect} that takes hold when its\n * {@link ConditionNode} matches the request's flattened attributes. The\n * condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves\n * combined with `and`/`or`/`not`), so a consumer learns one condition language\n * across the whole kit.\n *\n * A zod-free value object: it validates its own shape on construction (throwing\n * {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are\n * authored in TypeScript — trusted data, not untrusted JSON — the structural\n * check is a hand-rolled walk of the condition tree rather than a schema parse.\n * Build one through {@link of}; the constructor is private.\n */\nclass AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {\n private constructor(props: AbacRuleProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Builds a rule, validating that `id` is non-blank, `version` is an integer\n * ≥ 1, `effect` is `\"PERMIT\"`/`\"DENY\"`, and `condition` is a well-formed\n * condition node. Throws {@link InvalidValueException} otherwise.\n */\n static of(props: AbacRuleProps): AbacRule {\n if (typeof props.id !== \"string\" || props.id.trim().length === 0) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"id\"),\n ValidationCode.BLANK,\n `abac rule id must be a non-empty string, got \"${props.id}\"`,\n );\n }\n\n if (!Number.isInteger(props.version) || props.version < 1) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"version\"),\n ValidationCode.OUT_OF_RANGE,\n `abac rule version must be an integer >= 1, got \"${props.version}\"`,\n );\n }\n\n if (!RULE_EFFECTS.has(props.effect)) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"effect\"),\n ValidationCode.INVALID_FORMAT,\n `abac rule effect must be \"PERMIT\" or \"DENY\", got \"${String(props.effect)}\"`,\n );\n }\n\n AbacRule.assertConditionShape(props.condition);\n\n return new AbacRule(props);\n }\n\n // Mirrors what the shared condition evaluator would reject at eval time\n // (empty and/or nodes, malformed leaves) but fails fast at construction.\n private static assertConditionShape(node: unknown): void {\n if (!isPlainObject(node)) {\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const key = \"and\" in node ? \"and\" : \"or\";\n const children = node[key];\n if (!Array.isArray(children) || children.length === 0) {\n AbacRule.rejectCondition(\n `abac rule \"${key}\" node must hold a non-empty array of child conditions`,\n );\n }\n for (const child of children) {\n AbacRule.assertConditionShape(child);\n }\n return;\n }\n\n if (\"not\" in node) {\n AbacRule.assertConditionShape(node.not);\n return;\n }\n\n if (\"field\" in node && \"op\" in node) {\n if (\n typeof node.field !== \"string\" ||\n node.field.trim().length === 0\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"field\" must be a non-empty string, got \"${String(node.field)}\"`,\n );\n }\n if (\n typeof node.op !== \"string\" ||\n !CONDITION_OPS.has(node.op as ConditionOp)\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"op\" is not a supported operator, got \"${String(node.op)}\"`,\n );\n }\n return;\n }\n\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n private static rejectCondition(message: string): never {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"condition\"),\n ValidationCode.INVALID_FORMAT,\n message,\n );\n }\n\n get id(): string {\n return this.value.id;\n }\n\n get version(): number {\n return this.value.version;\n }\n\n get effect(): RuleEffect {\n return this.value.effect;\n }\n\n /** The condition tree evaluated against a request's flattened attributes. */\n get condition(): ConditionNode {\n return this.value.condition as ConditionNode;\n }\n\n get description(): string | undefined {\n return this.value.description;\n }\n\n toPrimitive(): AbacRuleProps {\n return {\n id: this.value.id,\n version: this.value.version,\n effect: this.value.effect,\n condition: this.value.condition as ConditionNode,\n ...(this.value.description !== undefined\n ? { description: this.value.description }\n : {}),\n };\n }\n}\n\nexport { AbacRule, type AbacRuleProps, type RuleEffect };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rule.js","names":[],"sources":["../src/core/abac/attributes.ts","../src/core/abac/combining.ts","../src/core/abac/authorizer.ts","../src/core/abac/composite-authorizer.ts","../src/core/abac/domain/policy-set.ts","../src/core/policies/engines/v1/condition-types.ts","../src/core/abac/domain/rule.ts"],"sourcesContent":["import type { PolicyContext } from \"../policies/engines/gate-types.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\n\n/**\n * Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the\n * condition evaluator reads (it resolves dotted fields like `resource.status` by\n * walking nested objects). The actor lands as `subject.id`; the four attribute\n * bags nest under `subject` / `resource` / `action` / `env`; a `resource`\n * reference adds `resource.type` / `resource.id`. The actor id and the resource\n * reference win over any same-named keys in the attribute bags.\n *\n * Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,\n * or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so\n * rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.\n */\nexport function abacContext(request: AbacRequest): PolicyContext {\n const attributes = request.attributes;\n\n const subject: Record<string, unknown> = {\n ...attributes.subject,\n id: request.actor.raw,\n };\n\n const resource: Record<string, unknown> = { ...attributes.resource };\n if (request.resource) {\n resource.type = request.resource.type;\n if (request.resource.id !== undefined) {\n resource.id = request.resource.id;\n }\n }\n\n return {\n subject,\n resource,\n action: { ...attributes.action },\n env: { ...attributes.environment },\n };\n}\n","import type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n/**\n * How an {@link AbacPolicySet} resolves several applicable rules into one effect.\n *\n * - `deny-overrides` (default, secure): any applicable DENY wins; otherwise the\n * first PERMIT; otherwise the set's default effect.\n * - `permit-overrides`: any applicable PERMIT wins; otherwise the first DENY;\n * otherwise the default effect.\n * - `first-applicable`: the effect of the first rule that matches; otherwise the\n * default effect.\n */\nexport type CombiningAlgorithm =\n | \"deny-overrides\"\n | \"permit-overrides\"\n | \"first-applicable\";\n\n/** The resolved decision plus the rule (if any) that produced it. */\nexport interface CombineResult {\n readonly effect: RuleEffect;\n readonly decidingRule?: AbacRule;\n}\n\nfunction firstWithEffect(\n rules: readonly AbacRule[],\n effect: RuleEffect,\n): AbacRule | undefined {\n return rules.find((rule) => rule.effect === effect);\n}\n\n/**\n * Applies `algorithm` to the already-matched `applicable` rules, falling back to\n * `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and\n * total; the returned `decidingRule` is absent exactly when `defaultEffect` was\n * used.\n */\nexport function combine(\n algorithm: CombiningAlgorithm,\n applicable: readonly AbacRule[],\n defaultEffect: RuleEffect,\n): CombineResult {\n switch (algorithm) {\n case \"deny-overrides\": {\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n return { effect: defaultEffect };\n }\n case \"permit-overrides\": {\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n return { effect: defaultEffect };\n }\n case \"first-applicable\": {\n const first = applicable[0];\n if (first) return { effect: first.effect, decidingRule: first };\n return { effect: defaultEffect };\n }\n }\n}\n","import { type CoreConfig, coreConfig } from \"../config/index.js\";\nimport { AuthorizationError } from \"../errors/authorization-error.js\";\nimport { ConditionEvaluatorV1 } from \"../policies/engines/v1/condition-evaluator.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\nimport { abacContext } from \"./attributes.js\";\nimport { combine, type CombiningAlgorithm } from \"./combining.js\";\nimport type { AbacPolicySet } from \"./domain/policy-set.js\";\nimport type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n// ABAC reuses the gate engine's v1 condition matcher; the version stamps the\n// evaluation reports the shared reporter emits.\nconst ENGINE_VERSION = 1;\n\n/** The successful decision returned by {@link AbacAuthorizer.authorize}. */\ninterface AbacDecision {\n /** The audited business action that was allowed. */\n readonly action: string;\n readonly effect: \"PERMIT\";\n /** The id of the rule that permitted, or `\"<default>\"` when the set's default did. */\n readonly matchedRule: string;\n readonly algorithm: CombiningAlgorithm;\n /** When the decision was made, ISO-8601. */\n readonly grantedAtIso: string;\n}\n\n/**\n * The pure ABAC decisor. Given an {@link AbacRequest} and an\n * {@link AbacPolicySet}, it flattens the request's attributes into a context,\n * matches each rule's condition with the gate engine's pure condition evaluator\n * (reused, not reimplemented), resolves the effects through the set's combining\n * algorithm, and returns a {@link Result} — never throwing, mirroring the\n * \"errors as values\" contract of `RbacAuthorizer`/`GateEngineV1`.\n *\n * A denial maps to {@link AuthorizationError.policyDenied}, attributed to the\n * deciding rule's `id`/`version`. A condition-evaluation failure (a missing\n * attribute, a wrong-typed operand) fails closed as\n * {@link AuthorizationError.forbidden} — also attributed to the culprit rule's\n * `id`/`version` — unless the set opts into `onEvaluationError: \"skip-rule\"`, in\n * which case the unevaluable rule is skipped and the rest decide. Every resolved\n * decision (PERMIT and DENY) is emitted best-effort through the configured\n * reporter as an `abac-decision` event (silent by default). Timestamps come from\n * an injectable `clock` (default `() => new Date()`), so the decisor is pure\n * given one. Loading the dynamic attributes is the consumer's job (behind an\n * `AbacAuthorizerPort` adapter); this class only decides.\n */\nclass AbacAuthorizer {\n private readonly coreConfig: CoreConfig;\n private readonly clock: () => Date;\n\n constructor(\n params: {\n readonly coreConfig?: CoreConfig;\n /** Injectable clock for deterministic timestamps; defaults to `() => new Date()`. */\n readonly clock?: () => Date;\n } = {},\n ) {\n this.coreConfig = params.coreConfig ?? coreConfig;\n this.clock = params.clock ?? (() => new Date());\n }\n\n authorize(\n request: AbacRequest,\n policies: AbacPolicySet,\n ): Result<AbacDecision, AuthorizationError> {\n const context = abacContext(request);\n const evaluator = new ConditionEvaluatorV1(\n context,\n this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION),\n );\n\n const applicable: AbacRule[] = [];\n for (const rule of policies.rules) {\n const matched = evaluator.evaluate(rule.condition);\n if (matched.isErr()) {\n if (policies.onEvaluationError === \"skip-rule\") {\n // Escape valve for evolving rules: drop the unevaluable rule\n // (the evaluator already reported the anomaly) and let the\n // rest decide, instead of failing the whole set closed.\n continue;\n }\n // Fail closed: a technical evaluation error is never a silent\n // PERMIT. Attribute the deny to the culprit rule so the 403 is\n // triageable without turning on the reporter.\n return Result.err(\n AuthorizationError.forbidden({\n action: request.action,\n resource: request.resource,\n actor: { actorId: request.actor.raw },\n policyId: rule.id,\n policyVersion: rule.version,\n details: matched.errorOrNull() ?? undefined,\n }),\n );\n }\n if (matched.getOrThrow()) {\n applicable.push(rule);\n }\n }\n\n const { effect, decidingRule } = combine(\n policies.algorithm,\n applicable,\n policies.defaultEffect,\n );\n\n this.reportDecision(request, policies, effect, decidingRule);\n\n if (effect === \"PERMIT\") {\n return Result.ok({\n action: request.action,\n effect: \"PERMIT\",\n matchedRule: decidingRule?.id ?? \"<default>\",\n algorithm: policies.algorithm,\n grantedAtIso: this.clock().toISOString(),\n });\n }\n\n return Result.err(\n AuthorizationError.policyDenied({\n policyId: decidingRule?.id ?? \"<default-deny>\",\n policyVersion: decidingRule?.version ?? 0,\n evaluatedAtIso: this.clock().toISOString(),\n action: request.action,\n resource: request.resource,\n actor: { actorId: request.actor.raw },\n reasonCode: decidingRule?.id,\n }),\n );\n }\n\n // Best-effort audit trail: emits the resolved decision (PERMIT and DENY)\n // through the configured reporter, silent by default.\n private reportDecision(\n request: AbacRequest,\n policies: AbacPolicySet,\n effect: RuleEffect,\n decidingRule: AbacRule | undefined,\n ): void {\n this.coreConfig.reportSafely({\n kind: \"abac-decision\",\n level: \"info\",\n action: request.action,\n resource: request.resource,\n actorId: request.actor.raw,\n effect,\n decidingRuleId:\n decidingRule?.id ??\n (effect === \"PERMIT\" ? \"<default>\" : \"<default-deny>\"),\n decidingRuleVersion: decidingRule?.version,\n algorithm: policies.algorithm,\n occurredAt: this.clock(),\n });\n }\n}\n\nexport { AbacAuthorizer, type AbacDecision };\n","import { Result } from \"../result/result.js\";\nimport type { AbacAuthorizerPort } from \"../application/ports/abac-authorizer.port.js\";\nimport type { AuthorizerPort } from \"../application/ports/authorizer.port.js\";\nimport type { AuthorizationError } from \"../errors/authorization-error.js\";\nimport type { AccessRequest } from \"../rbac/access-request.js\";\n\nimport type { AbacAttributes } from \"./abac-request.js\";\n\n/**\n * The request a {@link CompositeAuthorizer} accepts — one object that satisfies\n * both the RBAC {@link AccessRequest} (its `required`/`scope`) and the ABAC\n * request (its `attributes`).\n */\ntype CompositeAccessRequest = AccessRequest & {\n readonly attributes: AbacAttributes;\n};\n\n/**\n * Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid\n * \"does the actor hold the capability, *and* do the attributes allow it here?\"\n * flow. Short-circuits with the RBAC denial when the actor is not even capable,\n * so the boundary sees the most specific 403.\n *\n * Only the two port interfaces are referenced (both erased at runtime), so this\n * pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the\n * consumer wires up.\n */\nclass CompositeAuthorizer {\n constructor(\n private readonly rbac: AuthorizerPort,\n private readonly abac: AbacAuthorizerPort,\n ) {}\n\n async authorize(\n request: CompositeAccessRequest,\n ): Promise<Result<void, AuthorizationError>> {\n const coarse = await this.rbac.authorize(request);\n if (coarse.isErr()) {\n return coarse;\n }\n return this.abac.authorize(request);\n }\n}\n\nexport { CompositeAuthorizer, type CompositeAccessRequest };\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport type { CombiningAlgorithm } from \"../combining.js\";\n\nimport type { AbacRule, RuleEffect } from \"./rule.js\";\n\n/**\n * How the {@link AbacAuthorizer} reacts when a rule's condition cannot be\n * evaluated (a referenced attribute is absent, an operand is wrong-typed).\n *\n * - `fail-closed` (default, secure): the whole decision fails closed as\n * `forbidden`. Safe, but it means adding a rule that reads an attribute a call\n * site does not populate makes that call site deny everything until it does.\n * - `skip-rule`: the unevaluable rule is dropped from the applicable set and the\n * remaining rules decide — the escape valve for evolving rules without\n * trailing every call site. The evaluator still reports the anomaly.\n */\nexport type OnEvaluationError = \"fail-closed\" | \"skip-rule\";\n\nconst POLICY_SET_FIELD = ValidationField.of(\"abacPolicySet\");\n\n/**\n * An ordered collection of {@link AbacRule}s plus how to combine them\n * ({@link CombiningAlgorithm}), what to decide when no rule applies\n * ({@link defaultEffect}), and how to react to an unevaluable rule\n * ({@link onEvaluationError}). This is where ABAC goes beyond a single gate\n * condition: several PERMIT/DENY rules resolved by an explicit algorithm.\n *\n * Closed by default — the combining algorithm defaults to `\"deny-overrides\"`,\n * the default effect to `\"DENY\"`, and evaluation errors to `\"fail-closed\"`, so a\n * request matching nothing (or hitting a technical error) is denied. A plain\n * holder (not a value object); build through {@link of}.\n */\nclass AbacPolicySet {\n private constructor(\n private readonly _rules: readonly AbacRule[],\n private readonly _algorithm: CombiningAlgorithm,\n private readonly _defaultEffect: RuleEffect,\n private readonly _onEvaluationError: OnEvaluationError,\n ) {\n Object.freeze(this._rules);\n Object.freeze(this);\n }\n\n /**\n * Builds a policy set. Rejects duplicate rule ids with\n * {@link InvalidValueException} so a denial's `policyId` attribution is\n * unambiguous.\n */\n static of(\n rules: readonly AbacRule[],\n options: {\n readonly algorithm?: CombiningAlgorithm;\n readonly defaultEffect?: RuleEffect;\n readonly onEvaluationError?: OnEvaluationError;\n } = {},\n ): AbacPolicySet {\n AbacPolicySet.assertUniqueIds(rules);\n return new AbacPolicySet(\n [...rules],\n options.algorithm ?? \"deny-overrides\",\n options.defaultEffect ?? \"DENY\",\n options.onEvaluationError ?? \"fail-closed\",\n );\n }\n\n private static assertUniqueIds(rules: readonly AbacRule[]): void {\n const seen = new Set<string>();\n for (const rule of rules) {\n if (seen.has(rule.id)) {\n throw new InvalidValueException(\n POLICY_SET_FIELD.nested(\"rules\"),\n ValidationCode.ALREADY_EXISTS,\n `abac policy set has duplicate rule id \"${rule.id}\"; ids must be unique for unambiguous audit attribution`,\n );\n }\n seen.add(rule.id);\n }\n }\n\n get rules(): readonly AbacRule[] {\n return this._rules;\n }\n\n get algorithm(): CombiningAlgorithm {\n return this._algorithm;\n }\n\n get defaultEffect(): RuleEffect {\n return this._defaultEffect;\n }\n\n get onEvaluationError(): OnEvaluationError {\n return this._onEvaluationError;\n }\n}\n\nexport { AbacPolicySet };\n","// ─── Shared v1 condition DSL types ──────────────────────────────────────────\n// These types belong to the v1 engine family: both gate/v1 and compute/v1\n// share the same condition-tree DSL. Keeping them here avoids making compute/v1\n// structurally dependent on gate/v1.\n\n// Single source of truth for the operator vocabulary: the union is derived from\n// this array so a new operator is added in exactly one place, and consumers that\n// need the runtime list (e.g. AbacRule's structural check) reuse it instead of\n// hand-maintaining a parallel set that can drift out of sync.\nexport const CONDITION_OPS = [\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"in\",\n \"notIn\",\n \"isNull\",\n \"isNotNull\",\n] as const;\n\nexport type ConditionOp = (typeof CONDITION_OPS)[number];\n\nexport interface ConditionLeafNode {\n readonly field: string;\n readonly op: ConditionOp;\n readonly value: unknown;\n readonly allowNull?: true;\n}\n\nexport interface ConditionAndNode {\n readonly and: readonly ConditionNode[];\n}\n\nexport interface ConditionOrNode {\n readonly or: readonly ConditionNode[];\n}\n\nexport interface ConditionNotNode {\n readonly not: ConditionNode;\n}\n\nexport type ConditionNode =\n | ConditionLeafNode\n | ConditionAndNode\n | ConditionOrNode\n | ConditionNotNode;\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\nimport {\n CONDITION_OPS,\n type ConditionNode,\n type ConditionOp,\n} from \"../../policies/engines/v1/condition-types.js\";\n\n/** Whether a matching {@link AbacRule} permits or denies the action. */\ntype RuleEffect = \"PERMIT\" | \"DENY\";\n\n/**\n * The serializable shape of a rule: a stable `id`/`version` (surfaced on a\n * denial as `policyId`/`policyVersion`), the {@link RuleEffect}, and the\n * {@link ConditionNode} that decides whether the rule applies to a request's\n * flattened attributes.\n */\ntype AbacRuleProps = {\n readonly id: string;\n readonly version: number;\n readonly effect: RuleEffect;\n readonly condition: ConditionNode;\n readonly description?: string;\n};\n\nconst RULE_FIELD = ValidationField.of(\"abacRule\");\n\n// Derived from the evaluator's own operator list (single source of truth) so a\n// new operator becomes usable in ABAC rules without a parallel edit here.\nconst SUPPORTED_OPS = new Set<ConditionOp>(CONDITION_OPS);\n\nconst RULE_EFFECTS = new Set<RuleEffect>([\"PERMIT\", \"DENY\"]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A single attribute-based rule: an {@link RuleEffect} that takes hold when its\n * {@link ConditionNode} matches the request's flattened attributes. The\n * condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves\n * combined with `and`/`or`/`not`), so a consumer learns one condition language\n * across the whole kit.\n *\n * A zod-free value object: it validates its own shape on construction (throwing\n * {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are\n * authored in TypeScript — trusted data, not untrusted JSON — the structural\n * check is a hand-rolled walk of the condition tree rather than a schema parse.\n * Build one through {@link of}; the constructor is private.\n *\n * **Attribute semantics a rule author must know (the condition is evaluated by\n * the shared gate/compute engine, whose runtime rules apply here too):**\n * - **A referenced attribute that is *absent* from the request is a technical\n * evaluation error, not a non-match.** By default (`onEvaluationError:\n * \"fail-closed\"` on the {@link AbacPolicySet}) that error fails the *entire*\n * decision closed — so adding a rule that reads an attribute a given call site\n * does not yet populate makes that call site start returning `forbidden`,\n * *even for requests the older rules used to permit*. Choose\n * `onEvaluationError: \"skip-rule\"` on the set to skip an unevaluable rule\n * instead of failing the whole set.\n * - **`isNull` / `isNotNull` test for an explicit `null`/`undefined` *value*, not\n * for a missing key.** A key that is absent from the bag never reaches the\n * operator — it short-circuits to the missing-attribute error above. To match\n * \"no deletedAt\", the consumer must put `deletedAt: null` in the bag, not omit\n * it. Pass `allowNull: true` on a relational leaf to treat a present `null` as\n * a non-match instead of an error.\n * - **Date comparison operands must be strict ISO-8601 UTC**\n * (`YYYY-MM-DDTHH:mm:ss.sssZ`); a bare `\"2026-07-09\"` or an offset like\n * `+00:00` is rejected as an invalid operand and fails closed.\n */\nclass AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {\n private constructor(props: AbacRuleProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Builds a rule, validating that `id` is non-blank, `version` is an integer\n * ≥ 1, `effect` is `\"PERMIT\"`/`\"DENY\"`, and `condition` is a well-formed\n * condition node. Throws {@link InvalidValueException} otherwise.\n */\n static of(props: AbacRuleProps): AbacRule {\n if (typeof props.id !== \"string\" || props.id.trim().length === 0) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"id\"),\n ValidationCode.BLANK,\n `abac rule id must be a non-empty string, got \"${props.id}\"`,\n );\n }\n\n if (!Number.isInteger(props.version) || props.version < 1) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"version\"),\n ValidationCode.OUT_OF_RANGE,\n `abac rule version must be an integer >= 1, got \"${props.version}\"`,\n );\n }\n\n if (!RULE_EFFECTS.has(props.effect)) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"effect\"),\n ValidationCode.INVALID_FORMAT,\n `abac rule effect must be \"PERMIT\" or \"DENY\", got \"${String(props.effect)}\"`,\n );\n }\n\n AbacRule.assertConditionShape(props.condition);\n\n return new AbacRule(props);\n }\n\n // Mirrors what the shared condition evaluator would reject at eval time\n // (empty and/or nodes, malformed leaves) but fails fast at construction.\n private static assertConditionShape(node: unknown): void {\n if (!isPlainObject(node)) {\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const key = \"and\" in node ? \"and\" : \"or\";\n const children = node[key];\n if (!Array.isArray(children) || children.length === 0) {\n AbacRule.rejectCondition(\n `abac rule \"${key}\" node must hold a non-empty array of child conditions`,\n );\n }\n for (const child of children) {\n AbacRule.assertConditionShape(child);\n }\n return;\n }\n\n if (\"not\" in node) {\n AbacRule.assertConditionShape(node.not);\n return;\n }\n\n if (\"field\" in node && \"op\" in node) {\n if (\n typeof node.field !== \"string\" ||\n node.field.trim().length === 0\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"field\" must be a non-empty string, got \"${String(node.field)}\"`,\n );\n }\n if (\n typeof node.op !== \"string\" ||\n !SUPPORTED_OPS.has(node.op as ConditionOp)\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"op\" is not a supported operator, got \"${String(node.op)}\"`,\n );\n }\n return;\n }\n\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n private static rejectCondition(message: string): never {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"condition\"),\n ValidationCode.INVALID_FORMAT,\n message,\n );\n }\n\n get id(): string {\n return this.value.id;\n }\n\n get version(): number {\n return this.value.version;\n }\n\n get effect(): RuleEffect {\n return this.value.effect;\n }\n\n /** The condition tree evaluated against a request's flattened attributes. */\n get condition(): ConditionNode {\n return this.value.condition as ConditionNode;\n }\n\n get description(): string | undefined {\n return this.value.description;\n }\n\n toPrimitive(): AbacRuleProps {\n return {\n id: this.value.id,\n version: this.value.version,\n effect: this.value.effect,\n condition: this.value.condition as ConditionNode,\n ...(this.value.description !== undefined\n ? { description: this.value.description }\n : {}),\n };\n }\n}\n\nexport { AbacRule, type AbacRuleProps, type RuleEffect };\n"],"mappings":"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"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
const require_domain_exception = require("./domain-exception.cjs");
|
|
2
2
|
//#region src/core/domain/rulesets/ruleset-registry.ts
|
|
3
3
|
var RulesetRegistryError = class extends require_domain_exception.DomainException {};
|
|
4
|
+
/**
|
|
5
|
+
* Runtime guard for the `RulesetId` shape (`name@major.minor`). The template
|
|
6
|
+
* type only protects TypeScript callers — a JS consumer (or a cast) could
|
|
7
|
+
* register `"foo"` or `"foo@1.5.2"`, and either would silently corrupt
|
|
8
|
+
* {@link RulesetRegistry.getCurrent}'s version ordering via `NaN`/ignored
|
|
9
|
+
* segments. Same idea as `CONTRACT_VERSION_PATTERN` in `versioning/version.ts`.
|
|
10
|
+
*/
|
|
11
|
+
const RULESET_ID_PATTERN = /^.+@\d+\.\d+$/;
|
|
4
12
|
function parseVersion(id) {
|
|
5
13
|
const atIdx = id.lastIndexOf("@");
|
|
6
14
|
const [major, minor] = id.slice(atIdx + 1).split(".").map(Number);
|
|
@@ -13,17 +21,28 @@ var RulesetRegistry = class {
|
|
|
13
21
|
}
|
|
14
22
|
register(ruleset) {
|
|
15
23
|
if (this._sealed) throw new RulesetRegistryError("Registry is sealed. No new rulesets can be registered.");
|
|
24
|
+
if (!RULESET_ID_PATTERN.test(ruleset.id)) throw new RulesetRegistryError(`Invalid ruleset id "${ruleset.id}". Expected "<name>@<major>.<minor>", e.g. "order-creation@1.0".`);
|
|
16
25
|
if (this._store.has(ruleset.id)) throw new RulesetRegistryError(`Ruleset with id "${ruleset.id}" is already registered.`);
|
|
17
26
|
this._store.set(ruleset.id, ruleset);
|
|
18
27
|
}
|
|
19
28
|
seal() {
|
|
20
29
|
this._sealed = true;
|
|
21
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Looks up a ruleset by exact id. The `T` parameter is a caller-asserted
|
|
33
|
+
* cast, not a runtime check: asking for the wrong `T` compiles and returns
|
|
34
|
+
* the object mistyped — the standard registry trade-off. Keep the id and
|
|
35
|
+
* the expected type paired at the call site.
|
|
36
|
+
*/
|
|
22
37
|
get(id) {
|
|
23
38
|
const ruleset = this._store.get(id);
|
|
24
39
|
if (!ruleset) throw new RulesetRegistryError(`Ruleset "${id}" not found. Available: [${Array.from(this._store.keys()).join(", ")}]`);
|
|
25
40
|
return ruleset;
|
|
26
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns the highest `major.minor` version registered under `prefix`.
|
|
44
|
+
* The same caller-asserted `T` cast as {@link get} applies.
|
|
45
|
+
*/
|
|
27
46
|
getCurrent(prefix) {
|
|
28
47
|
const matchingEntries = Array.from(this._store.entries()).filter(([key]) => key.startsWith(prefix + "@"));
|
|
29
48
|
if (matchingEntries.length === 0) throw new RulesetRegistryError(`No rulesets found with prefix "${prefix}".`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ruleset-registry.cjs","names":["DomainException"],"sources":["../src/core/domain/rulesets/ruleset-registry.ts"],"sourcesContent":["import { DomainException } from \"../../exceptions/domain-exception.js\";\nimport {\n type Ruleset,\n type RulesetRegistry as RulesetRegistryContract,\n} from \"./ruleset.contracts.js\";\n\nclass RulesetRegistryError extends DomainException {}\n\nfunction parseVersion(id: string): [number, number] {\n const atIdx = id.lastIndexOf(\"@\");\n const versionStr = id.slice(atIdx + 1);\n const [major, minor] = versionStr.split(\".\").map(Number);\n return [major, minor];\n}\n\nclass RulesetRegistry implements RulesetRegistryContract {\n private readonly _store = new Map<string, Ruleset>();\n private _sealed = false;\n\n register(ruleset: Ruleset): void {\n if (this._sealed) {\n throw new RulesetRegistryError(\n \"Registry is sealed. No new rulesets can be registered.\",\n );\n }\n if (this._store.has(ruleset.id)) {\n throw new RulesetRegistryError(\n `Ruleset with id \"${ruleset.id}\" is already registered.`,\n );\n }\n this._store.set(ruleset.id, ruleset);\n }\n\n seal(): void {\n this._sealed = true;\n }\n\n get<T extends Ruleset>(id: string): T {\n const ruleset = this._store.get(id);\n if (!ruleset) {\n const available = Array.from(this._store.keys()).join(\", \");\n throw new RulesetRegistryError(\n `Ruleset \"${id}\" not found. Available: [${available}]`,\n );\n }\n return ruleset as T;\n }\n\n getCurrent<T extends Ruleset>(prefix: string): T {\n const matchingEntries = Array.from(this._store.entries()).filter(\n ([key]) => key.startsWith(prefix + \"@\"),\n );\n\n if (matchingEntries.length === 0) {\n throw new RulesetRegistryError(\n `No rulesets found with prefix \"${prefix}\".`,\n );\n }\n\n const sorted = matchingEntries.sort(([a], [b]) => {\n const [aMajor, aMinor] = parseVersion(a);\n const [bMajor, bMinor] = parseVersion(b);\n if (aMajor !== bMajor) return aMajor - bMajor;\n return aMinor - bMinor;\n });\n\n return sorted[sorted.length - 1][1] as T;\n }\n}\n\nexport { RulesetRegistry };\n"],"mappings":";;AAMA,IAAM,uBAAN,cAAmCA,yBAAAA,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"ruleset-registry.cjs","names":["DomainException"],"sources":["../src/core/domain/rulesets/ruleset-registry.ts"],"sourcesContent":["import { DomainException } from \"../../exceptions/domain-exception.js\";\nimport {\n type Ruleset,\n type RulesetRegistry as RulesetRegistryContract,\n} from \"./ruleset.contracts.js\";\n\nclass RulesetRegistryError extends DomainException {}\n\n/**\n * Runtime guard for the `RulesetId` shape (`name@major.minor`). The template\n * type only protects TypeScript callers — a JS consumer (or a cast) could\n * register `\"foo\"` or `\"foo@1.5.2\"`, and either would silently corrupt\n * {@link RulesetRegistry.getCurrent}'s version ordering via `NaN`/ignored\n * segments. Same idea as `CONTRACT_VERSION_PATTERN` in `versioning/version.ts`.\n */\nconst RULESET_ID_PATTERN = /^.+@\\d+\\.\\d+$/;\n\nfunction parseVersion(id: string): [number, number] {\n const atIdx = id.lastIndexOf(\"@\");\n const versionStr = id.slice(atIdx + 1);\n const [major, minor] = versionStr.split(\".\").map(Number);\n return [major, minor];\n}\n\nclass RulesetRegistry implements RulesetRegistryContract {\n private readonly _store = new Map<string, Ruleset>();\n private _sealed = false;\n\n register(ruleset: Ruleset): void {\n if (this._sealed) {\n throw new RulesetRegistryError(\n \"Registry is sealed. No new rulesets can be registered.\",\n );\n }\n if (!RULESET_ID_PATTERN.test(ruleset.id)) {\n throw new RulesetRegistryError(\n `Invalid ruleset id \"${ruleset.id}\". Expected \"<name>@<major>.<minor>\", e.g. \"order-creation@1.0\".`,\n );\n }\n if (this._store.has(ruleset.id)) {\n throw new RulesetRegistryError(\n `Ruleset with id \"${ruleset.id}\" is already registered.`,\n );\n }\n this._store.set(ruleset.id, ruleset);\n }\n\n seal(): void {\n this._sealed = true;\n }\n\n /**\n * Looks up a ruleset by exact id. The `T` parameter is a caller-asserted\n * cast, not a runtime check: asking for the wrong `T` compiles and returns\n * the object mistyped — the standard registry trade-off. Keep the id and\n * the expected type paired at the call site.\n */\n get<T extends Ruleset>(id: string): T {\n const ruleset = this._store.get(id);\n if (!ruleset) {\n const available = Array.from(this._store.keys()).join(\", \");\n throw new RulesetRegistryError(\n `Ruleset \"${id}\" not found. Available: [${available}]`,\n );\n }\n return ruleset as T;\n }\n\n /**\n * Returns the highest `major.minor` version registered under `prefix`.\n * The same caller-asserted `T` cast as {@link get} applies.\n */\n getCurrent<T extends Ruleset>(prefix: string): T {\n const matchingEntries = Array.from(this._store.entries()).filter(\n ([key]) => key.startsWith(prefix + \"@\"),\n );\n\n if (matchingEntries.length === 0) {\n throw new RulesetRegistryError(\n `No rulesets found with prefix \"${prefix}\".`,\n );\n }\n\n const sorted = matchingEntries.sort(([a], [b]) => {\n const [aMajor, aMinor] = parseVersion(a);\n const [bMajor, bMinor] = parseVersion(b);\n if (aMajor !== bMajor) return aMajor - bMajor;\n return aMinor - bMinor;\n });\n\n return sorted[sorted.length - 1][1] as T;\n }\n}\n\nexport { RulesetRegistry };\n"],"mappings":";;AAMA,IAAM,uBAAN,cAAmCA,yBAAAA,gBAAgB,CAAC;;;;;;;;AASpD,MAAM,qBAAqB;AAE3B,SAAS,aAAa,IAA8B;CAChD,MAAM,QAAQ,GAAG,YAAY,GAAG;CAEhC,MAAM,CAAC,OAAO,SADK,GAAG,MAAM,QAAQ,CACJ,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;CACvD,OAAO,CAAC,OAAO,KAAK;AACxB;AAEA,IAAM,kBAAN,MAAyD;;gCAC3B,IAAI,IAAqB;iBACjC;;CAElB,SAAS,SAAwB;EAC7B,IAAI,KAAK,SACL,MAAM,IAAI,qBACN,wDACJ;EAEJ,IAAI,CAAC,mBAAmB,KAAK,QAAQ,EAAE,GACnC,MAAM,IAAI,qBACN,uBAAuB,QAAQ,GAAG,iEACtC;EAEJ,IAAI,KAAK,OAAO,IAAI,QAAQ,EAAE,GAC1B,MAAM,IAAI,qBACN,oBAAoB,QAAQ,GAAG,yBACnC;EAEJ,KAAK,OAAO,IAAI,QAAQ,IAAI,OAAO;CACvC;CAEA,OAAa;EACT,KAAK,UAAU;CACnB;;;;;;;CAQA,IAAuB,IAAe;EAClC,MAAM,UAAU,KAAK,OAAO,IAAI,EAAE;EAClC,IAAI,CAAC,SAED,MAAM,IAAI,qBACN,YAAY,GAAG,2BAFD,MAAM,KAAK,KAAK,OAAO,KAAK,CAAC,EAAE,KAAK,IAEA,EAAE,EACxD;EAEJ,OAAO;CACX;;;;;CAMA,WAA8B,QAAmB;EAC7C,MAAM,kBAAkB,MAAM,KAAK,KAAK,OAAO,QAAQ,CAAC,EAAE,QACrD,CAAC,SAAS,IAAI,WAAW,SAAS,GAAG,CAC1C;EAEA,IAAI,gBAAgB,WAAW,GAC3B,MAAM,IAAI,qBACN,kCAAkC,OAAO,GAC7C;EAGJ,MAAM,SAAS,gBAAgB,MAAM,CAAC,IAAI,CAAC,OAAO;GAC9C,MAAM,CAAC,QAAQ,UAAU,aAAa,CAAC;GACvC,MAAM,CAAC,QAAQ,UAAU,aAAa,CAAC;GACvC,IAAI,WAAW,QAAQ,OAAO,SAAS;GACvC,OAAO,SAAS;EACpB,CAAC;EAED,OAAO,OAAO,OAAO,SAAS,GAAG;CACrC;AACJ"}
|
package/dist/ruleset-registry.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { t as DomainException } from "./domain-exception.js";
|
|
2
2
|
//#region src/core/domain/rulesets/ruleset-registry.ts
|
|
3
3
|
var RulesetRegistryError = class extends DomainException {};
|
|
4
|
+
/**
|
|
5
|
+
* Runtime guard for the `RulesetId` shape (`name@major.minor`). The template
|
|
6
|
+
* type only protects TypeScript callers — a JS consumer (or a cast) could
|
|
7
|
+
* register `"foo"` or `"foo@1.5.2"`, and either would silently corrupt
|
|
8
|
+
* {@link RulesetRegistry.getCurrent}'s version ordering via `NaN`/ignored
|
|
9
|
+
* segments. Same idea as `CONTRACT_VERSION_PATTERN` in `versioning/version.ts`.
|
|
10
|
+
*/
|
|
11
|
+
const RULESET_ID_PATTERN = /^.+@\d+\.\d+$/;
|
|
4
12
|
function parseVersion(id) {
|
|
5
13
|
const atIdx = id.lastIndexOf("@");
|
|
6
14
|
const [major, minor] = id.slice(atIdx + 1).split(".").map(Number);
|
|
@@ -13,17 +21,28 @@ var RulesetRegistry = class {
|
|
|
13
21
|
}
|
|
14
22
|
register(ruleset) {
|
|
15
23
|
if (this._sealed) throw new RulesetRegistryError("Registry is sealed. No new rulesets can be registered.");
|
|
24
|
+
if (!RULESET_ID_PATTERN.test(ruleset.id)) throw new RulesetRegistryError(`Invalid ruleset id "${ruleset.id}". Expected "<name>@<major>.<minor>", e.g. "order-creation@1.0".`);
|
|
16
25
|
if (this._store.has(ruleset.id)) throw new RulesetRegistryError(`Ruleset with id "${ruleset.id}" is already registered.`);
|
|
17
26
|
this._store.set(ruleset.id, ruleset);
|
|
18
27
|
}
|
|
19
28
|
seal() {
|
|
20
29
|
this._sealed = true;
|
|
21
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Looks up a ruleset by exact id. The `T` parameter is a caller-asserted
|
|
33
|
+
* cast, not a runtime check: asking for the wrong `T` compiles and returns
|
|
34
|
+
* the object mistyped — the standard registry trade-off. Keep the id and
|
|
35
|
+
* the expected type paired at the call site.
|
|
36
|
+
*/
|
|
22
37
|
get(id) {
|
|
23
38
|
const ruleset = this._store.get(id);
|
|
24
39
|
if (!ruleset) throw new RulesetRegistryError(`Ruleset "${id}" not found. Available: [${Array.from(this._store.keys()).join(", ")}]`);
|
|
25
40
|
return ruleset;
|
|
26
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns the highest `major.minor` version registered under `prefix`.
|
|
44
|
+
* The same caller-asserted `T` cast as {@link get} applies.
|
|
45
|
+
*/
|
|
27
46
|
getCurrent(prefix) {
|
|
28
47
|
const matchingEntries = Array.from(this._store.entries()).filter(([key]) => key.startsWith(prefix + "@"));
|
|
29
48
|
if (matchingEntries.length === 0) throw new RulesetRegistryError(`No rulesets found with prefix "${prefix}".`);
|