@cullet/erp-core 1.5.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 +2 -2
- 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 +8 -1
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +8 -1
- 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 +115 -184
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +116 -179
- 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 +1 -1
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +2 -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 +1 -1
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +2 -2
- 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 +13 -10
- 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 +7 -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 -15
- package/dist/policies/engines/v1/gate/index.d.ts +3 -15
- 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 +40 -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 +42 -48
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +1 -1
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +1 -1
- 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 +43 -3
- package/dist/stable-stringify.cjs.map +1 -1
- package/dist/stable-stringify.js +38 -4
- package/dist/stable-stringify.js.map +1 -1
- package/dist/temporal-snapshot.d.cts +87 -0
- package/dist/temporal-snapshot.d.ts +87 -0
- package/dist/temporal-use-case.cjs +169 -16
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +76 -43
- package/dist/temporal-use-case.d.ts +76 -43
- package/dist/temporal-use-case.js +162 -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 +140 -2
- 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 -3
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +11 -6
- package/dist/uuid.cjs.map +1 -1
- package/dist/uuid.js +11 -6
- package/dist/uuid.js.map +1 -1
- 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 +24 -8
- package/dist/validation-error.d.ts +24 -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 +12 -2
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +16 -0
- package/dist/value-object.d.ts +16 -0
- package/dist/value-object.js +13 -3
- 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 +3 -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 +12 -3
- 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 +19 -2
- package/src/core/application/temporal/temporal-use-case.ts +31 -4
- package/src/core/application/use-case.ts +44 -7
- 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 +50 -7
- 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 +5 -3
- package/src/core/domain/value-object.ts +17 -0
- 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 +15 -1
- 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 +99 -325
- 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 -3
- package/src/core/shared/immutable.ts +22 -4
- package/src/core/shared/stable-stringify.ts +91 -4
- package/src/core/shared/uuid.ts +11 -6
- 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 -122
- package/dist/entity.cjs.map +0 -1
- package/dist/entity.js +0 -111
- package/dist/entity.js.map +0 -1
- package/dist/use-case.cjs +0 -92
- package/dist/use-case.cjs.map +0 -1
- package/dist/use-case.js +0 -87
- package/dist/use-case.js.map +0 -1
package/dist/policies/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as Ok, r as Result, t as Err } from "../result.js";
|
|
2
2
|
import { n as Outcome, t as CommonOutcomeStatus } from "../outcome.js";
|
|
3
|
-
import { C as
|
|
3
|
+
import { C as GateViolationTrace, D as GatePayload, E as VersionedGateEngine, S as GateTraceNodeSnapshot, T as PolicyViolation, _ as ConditionEvaluatorReporter, a as PolicyDefinitionId, b as GateStatus, c as asPolicyDecisionId, d as asTenantId, f as ConditionEvaluationCause, g as ConditionEvaluationReportTag, h as ConditionEvaluationReportLevel, i as PolicyDecisionId, l as asPolicyDefinitionId, m as ConditionEvaluationReport, n as CoreConfigOptions, o as SchoolId, p as ConditionEvaluationOptions, r as CoreObservabilityConfig, s as TenantId, t as CoreConfig, u as asSchoolId, v as GateOutcome, w as PolicyContext, x as GateTraceLeafSnapshot, y as GateOutcomeData } from "../core-config.js";
|
|
4
4
|
import { a as PolicyPackage, c as coreConfig, i as PolicyCatalogFactory, n as contextResolverRegistry, o as InMemoryPolicyDefinitionRepository, r as registerNamespacedContextResolvers, s as PolicyHashing, t as PolicyContextPath } from "../path.js";
|
|
5
5
|
import { A as PolicyScope, C as BasePolicyDefinitionProps, D as PolicyDefinition, E as GatePolicyDefinitionProps, F as PolicyKey, I as AsOfSource, L as PolicyKind, M as ScopeChain, N as PolicyCatalog, O as PolicyDefinitionProps, P as PolicyCatalogEntryProps, R as PolicyOwner, T as FindCandidatesParams, _ as ContextResolverResilienceOptions, a as PolicyServiceOptions, b as ContextSeed, c as PolicyEvaluationErrors, d as PolicyAsOfResolver, f as PolicyContextBuilder, g as ContextResolverCircuitBreakerOptions, h as registerNamespacedContextResolversIn, i as PolicyService, j as PolicyScopeMatcher, k as PolicyDefinitionStatus, l as PolicyResolver, m as ContextResolverRegistry, n as PolicyDecision, o as PolicyServiceParams, p as PolicyContextBuilderOptions, r as PolicyEvaluationResult, s as PolicyEvaluationError, t as EvaluateInput, u as DeriveAsOfOptions, v as ContextResolverRetryOptions, w as ComputePolicyDefinitionProps, x as ContextSeedValidator, y as ContextValueResolver, z as PolicyScopeLevel } from "../policy-service.js";
|
|
6
6
|
import { a as ComputeOutcome, c as VersionedComputeEngine, i as ComputeEvaluatorRegistration, n as ComputeRegistry, o as ComputeOutcomeData, r as ComputeEvaluator, s as ComputeStatus, t as GateEngineRegistry } from "../gate-engine-registry.js";
|
package/dist/policy-bridge.cjs
CHANGED
|
@@ -31,6 +31,13 @@ var RbacAuthorizer = class {
|
|
|
31
31
|
* 3. grants the permission but not in scope → `out_of_scope`;
|
|
32
32
|
* 4. otherwise → ALLOW (`Result.ok`).
|
|
33
33
|
*
|
|
34
|
+
* ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of
|
|
35
|
+
* *which* grant or role decided it. Denials are rich (see the metadata
|
|
36
|
+
* below) because they need to explain themselves; an allow does not. When an
|
|
37
|
+
* ERP needs a "who allowed this, and why" trail, that is the adapter's job —
|
|
38
|
+
* reconstruct it from the grants it loaded, or gate on `PermissionSet` before
|
|
39
|
+
* calling. Keeping the decisor's allow-path pure is the deliberate trade.
|
|
40
|
+
*
|
|
34
41
|
* @throws {@link InvalidValueException} when `request.required` is a wildcard
|
|
35
42
|
* permission (only *granted* permissions may wildcard, never the *required*
|
|
36
43
|
* one).
|
|
@@ -44,7 +51,7 @@ var RbacAuthorizer = class {
|
|
|
44
51
|
const metadata = {
|
|
45
52
|
action: request.action,
|
|
46
53
|
resource: request.resource,
|
|
47
|
-
actor: {
|
|
54
|
+
actor: { actorId: request.actor.raw },
|
|
48
55
|
required
|
|
49
56
|
};
|
|
50
57
|
const actorGrants = grants.filter((grant) => grant.appliesTo(request.actor));
|
|
@@ -180,6 +187,22 @@ var Role = class Role extends require_value_object.ValueObject {
|
|
|
180
187
|
get permissions() {
|
|
181
188
|
return this._permissions;
|
|
182
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Two roles are equal when they share a name and the same *set* of
|
|
192
|
+
* permissions. Order-insensitive on purpose: a role is a bundle, not a
|
|
193
|
+
* sequence, so `Role.of("cashier", [read, cancel])` equals
|
|
194
|
+
* `Role.of("cashier", [cancel, read])`. Overrides the base
|
|
195
|
+
* {@link ValueObject.equals}, whose stable-stringify keeps array order and
|
|
196
|
+
* would call those two roles different. The stored order (and thus
|
|
197
|
+
* {@link permissions}/{@link toPrimitive}) is still first-seen; only the
|
|
198
|
+
* comparison sorts.
|
|
199
|
+
*/
|
|
200
|
+
equals(other) {
|
|
201
|
+
if (this.value.name !== other.value.name) return false;
|
|
202
|
+
const mine = [...this.value.permissions].sort();
|
|
203
|
+
const theirs = [...other.value.permissions].sort();
|
|
204
|
+
return mine.length === theirs.length && mine.every((permission, index) => permission === theirs[index]);
|
|
205
|
+
}
|
|
183
206
|
/** Whether any permission in this role {@link Permission.implies | implies} `required`. */
|
|
184
207
|
grants(required) {
|
|
185
208
|
return this._permissions.some((permission) => permission.implies(required));
|
|
@@ -210,7 +233,12 @@ var Scope = class Scope extends require_value_object.ValueObject {
|
|
|
210
233
|
super(props);
|
|
211
234
|
this.finalize();
|
|
212
235
|
}
|
|
213
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* Validates `"type:id"` or the global `"*"`, throwing on anything else.
|
|
238
|
+
* `Scope.of("*")` is accepted and is equivalent to {@link Scope.global} —
|
|
239
|
+
* prefer `global()` at call-sites for intent, `of("*")` when rehydrating a
|
|
240
|
+
* stored string.
|
|
241
|
+
*/
|
|
214
242
|
static of(raw) {
|
|
215
243
|
if (typeof raw !== "string" || raw.trim().length === 0) throw new require_validation_exception.InvalidValueException(SCOPE_FIELD, require_validation_code.ValidationCode.BLANK, `scope must be a non-empty "type:id" string or "*", got "${raw}"`);
|
|
216
244
|
if (raw !== "*" && !SCOPE_PATTERN.test(raw)) throw new require_validation_exception.InvalidValueException(SCOPE_FIELD, require_validation_code.ValidationCode.INVALID_FORMAT, `scope must be "type:id" (e.g. "school:42") or the global "*", got "${raw}"`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-bridge.cjs","names":["ValidationField","InvalidValueException","ValidationCode","Result","AuthorizationError","ValidationField","ValueObject","InvalidValueException","ValidationCode","ValidationField","ValueObject","InvalidValueException","ValidationCode","ValidationField","ValueObject","InvalidValueException","ValidationCode","ValueObject","RequestedBy"],"sources":["../src/core/rbac/authorizer.ts","../src/core/rbac/domain/permission.ts","../src/core/rbac/domain/role.ts","../src/core/rbac/domain/scope.ts","../src/core/rbac/domain/grant.ts","../src/core/rbac/domain/permission-set.ts","../src/core/rbac/policy-bridge.ts"],"sourcesContent":["import {\n AuthorizationError,\n type AuthorizationRequirement,\n} from \"../errors/authorization-error.js\";\nimport { ValidationCode } from \"../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../exceptions/validation-field.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AccessRequest } from \"./access-request.js\";\nimport type { Grant } from \"./domain/grant.js\";\n\nconst REQUIRED_FIELD = ValidationField.of(\"required\");\n\n/**\n * The pure RBAC decisor. Given an {@link AccessRequest} and the {@link Grant}s\n * already loaded for the actor, it answers \"may this actor do this?\" with no\n * I/O — no storage, no network, no clock — returning a {@link Result}. An\n * authorization *decision* is never thrown, always a value, mirroring the\n * \"errors as values\" contract of `UseCase`/`PolicyService`. Loading the grants\n * is the consumer's job (behind an `AuthorizerPort` adapter); this class only\n * decides.\n *\n * The one thing it *does* throw is {@link InvalidValueException} when the caller\n * misuses the API by passing a wildcard `required` permission — that is a\n * programming error surfaced loudly, not an authorization outcome.\n */\nclass RbacAuthorizer {\n /**\n * Decides the request against the supplied grants. The checks run\n * role → capability → scope so the denial reason is the most informative\n * one available:\n *\n * 1. no grant for the actor → `missing_role`;\n * 2. has grants but none grants the permission → `missing_capability`;\n * 3. grants the permission but not in scope → `out_of_scope`;\n * 4. otherwise → ALLOW (`Result.ok`).\n *\n * @throws {@link InvalidValueException} when `request.required` is a wildcard\n * permission (only *granted* permissions may wildcard, never the *required*\n * one).\n */\n authorize(\n request: AccessRequest,\n grants: readonly Grant[],\n ): Result<void, AuthorizationError> {\n if (request.required.hasWildcard()) {\n throw new InvalidValueException(\n REQUIRED_FIELD,\n ValidationCode.INVALID_FORMAT,\n `AccessRequest.required must be a concrete permission, not a wildcard, got \"${request.required.toPrimitive()}\"`,\n );\n }\n\n const required: AuthorizationRequirement = {\n capability: request.required.toPrimitive(),\n scope: request.scope.toPrimitive(),\n };\n const metadata = {\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n required,\n };\n\n const actorGrants = grants.filter((grant) =>\n grant.appliesTo(request.actor),\n );\n if (actorGrants.length === 0) {\n return Result.err(AuthorizationError.missingRole(metadata));\n }\n\n const withPermission = actorGrants.filter((grant) =>\n grant.role.grants(request.required),\n );\n if (withPermission.length === 0) {\n return Result.err(AuthorizationError.missingCapability(metadata));\n }\n\n const inScope = withPermission.filter((grant) =>\n grant.scope.includes(request.scope),\n );\n if (inScope.length === 0) {\n return Result.err(AuthorizationError.outOfScope(metadata));\n }\n\n return Result.ok(undefined);\n }\n}\n\nexport { RbacAuthorizer };\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\";\n\n/** The two halves of a permission: what is acted on, and what is done to it. */\ntype PermissionProps = {\n readonly resource: string;\n readonly action: string;\n};\n\nconst PERMISSION_FIELD = ValidationField.of(\"permission\");\n\n// A segment is either a whole-segment wildcard (`*`) or a run of lowercase\n// alphanumerics, hyphens and underscores. Partial wildcards (`ord*`) are\n// rejected on purpose: a `*` only ever stands for an entire segment.\nconst SEGMENT_PATTERN = /^(?:\\*|[a-z0-9_-]+)$/u;\n\n/**\n * A `\"<resource>:<action>\"` capability such as `\"orders:cancel\"`, with a\n * single-level wildcard. Either segment may be the whole-segment wildcard `*`\n * (`\"orders:*\"`, `\"*:cancel\"`, `\"*:*\"`), which is what lets a broad grant cover\n * a narrower requirement — see {@link implies}.\n *\n * A zod-free value object: it validates its own format on construction\n * (throwing {@link InvalidValueException}) and is frozen thereafter. Build one\n * through {@link of} (from a raw `\"resource:action\"` string) or {@link for}\n * (from already-separated parts); the constructor is private.\n */\nclass Permission extends ValueObject<PermissionProps, string> {\n private constructor(props: PermissionProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Parses a `\"resource:action\"` string. Throws {@link InvalidValueException}\n * when the string is blank, lacks exactly one `:`, or carries an invalid\n * segment.\n */\n static of(raw: string): Permission {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.BLANK,\n `permission must be a non-empty \"resource:action\" string, got \"${raw}\"`,\n );\n }\n\n const parts = raw.split(\":\");\n if (parts.length !== 2) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.INVALID_FORMAT,\n `permission must have exactly one \":\" separating resource and action, got \"${raw}\"`,\n );\n }\n\n return Permission.for(parts[0], parts[1]);\n }\n\n /**\n * Builds from already-separated `resource` and `action` parts. Each must be\n * a valid segment (`[a-z0-9_-]+` or the whole-segment wildcard `*`).\n */\n static for(resource: string, action: string): Permission {\n Permission.assertSegment(resource, \"resource\");\n Permission.assertSegment(action, \"action\");\n return new Permission({ resource, action });\n }\n\n private static assertSegment(\n value: string,\n label: \"resource\" | \"action\",\n ): void {\n if (typeof value !== \"string\" || !SEGMENT_PATTERN.test(value)) {\n throw new InvalidValueException(\n PERMISSION_FIELD.nested(label),\n ValidationCode.INVALID_FORMAT,\n `permission ${label} must match [a-z0-9_-]+ or be the whole-segment wildcard \"*\" (no partial wildcards), got \"${value}\"`,\n );\n }\n }\n\n get resource(): string {\n return this.value.resource;\n }\n\n get action(): string {\n return this.value.action;\n }\n\n /**\n * Whether holding this permission grants `other`. A `*` segment matches any\n * value in the same position, so `\"orders:*\"` implies `\"orders:cancel\"` and\n * `\"*:*\"` implies everything. Concrete segments must match exactly.\n */\n implies(other: Permission): boolean {\n return (\n (this.resource === \"*\" || this.resource === other.resource) &&\n (this.action === \"*\" || this.action === other.action)\n );\n }\n\n /**\n * Whether either segment is the whole-segment wildcard `*`. A *granted*\n * permission may wildcard (see {@link implies}); a *required* one may not —\n * `RbacAuthorizer` rejects a wildcard requirement as caller misuse.\n */\n hasWildcard(): boolean {\n return this.value.resource === \"*\" || this.value.action === \"*\";\n }\n\n toPrimitive(): string {\n return `${this.value.resource}:${this.value.action}`;\n }\n}\n\nexport { Permission, type PermissionProps };\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\";\n\nimport { Permission } from \"./permission.js\";\n\n/**\n * The serializable shape of a role: a name plus its permissions kept as their\n * primitive `\"resource:action\"` strings (so the role round-trips through JSON\n * and stays deep-frozen). The {@link Role.permissions} getter rehydrates them\n * back into {@link Permission} objects.\n */\ntype RoleProps = {\n readonly name: string;\n readonly permissions: readonly string[];\n};\n\nconst ROLE_FIELD = ValidationField.of(\"role\");\n\n/**\n * A named bundle of {@link Permission}s — `\"cashier\"`, `\"manager\"`, `\"admin\"`.\n * There is **no role hierarchy** in v1: a role grants exactly the permissions\n * it lists (wildcards included). Duplicate permissions are collapsed on\n * construction, preserving first-seen order.\n *\n * A zod-free value object. Build through {@link of}; reconstruct a serialized\n * one through {@link fromProps}.\n */\nclass Role extends ValueObject<RoleProps, RoleProps> {\n // Parsed once, at construction, and cached: the decisor calls `grants` (and\n // `PermissionSet` reads `permissions`) repeatedly, so re-parsing the stored\n // strings on every access would be wasted work.\n private readonly _permissions: readonly Permission[];\n\n private constructor(props: RoleProps) {\n super(props);\n this._permissions = this.value.permissions.map(Permission.of);\n this.finalize();\n }\n\n /**\n * Builds a role from a name and its permissions. Throws\n * {@link InvalidValueException} when the name is blank. Duplicate\n * permissions are deduplicated.\n */\n static of(name: string, permissions: readonly Permission[]): Role {\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new InvalidValueException(\n ROLE_FIELD,\n ValidationCode.BLANK,\n `role name must be a non-empty string, got \"${name}\"`,\n );\n }\n\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const permission of permissions) {\n const primitive = permission.toPrimitive();\n if (seen.has(primitive)) continue;\n seen.add(primitive);\n deduped.push(primitive);\n }\n\n return new Role({ name, permissions: deduped });\n }\n\n /**\n * Rebuilds a role from its serialized {@link RoleProps} — the inverse of\n * {@link toPrimitive}. Each stored string is re-parsed through\n * {@link Permission.of}, so a malformed payload still fails loudly.\n */\n static fromProps(props: RoleProps): Role {\n return Role.of(props.name, props.permissions.map(Permission.of));\n }\n\n get name(): string {\n return this.value.name;\n }\n\n /** The role's permissions, rehydrated from their stored primitive form. */\n get permissions(): readonly Permission[] {\n return this._permissions;\n }\n\n /** Whether any permission in this role {@link Permission.implies | implies} `required`. */\n grants(required: Permission): boolean {\n return this._permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n toPrimitive(): RoleProps {\n return {\n name: this.value.name,\n permissions: [...this.value.permissions],\n };\n }\n}\n\nexport { Role, type RoleProps };\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\";\n\n/** The wrapped, already-validated scope string (`\"type:id\"` or `\"*\"`). */\ntype ScopeProps = { readonly raw: string };\n\nconst SCOPE_FIELD = ValidationField.of(\"scope\");\n\n// \"type:id\": a lowercase type segment, a colon, then a non-empty id. The id\n// charset is permissive enough for UUIDs and numeric keys (`school:42`,\n// `tenant:550e8400-e29b-41d4-a716-446655440000`).\nconst SCOPE_PATTERN = /^[a-z][a-z0-9_-]*:[A-Za-z0-9._-]+$/u;\n\n/**\n * Where a grant (or a target resource) lives, aligned with\n * `AuthorizationRequirement.scope` and the policy `PolicyScope`\n * (`tenantId`/`schoolId`): `\"tenant:{id}\"`, `\"school:{id}\"`, or the global\n * `\"*\"`.\n *\n * v1 keeps containment ({@link includes}) flat — the global scope contains\n * everything, and any other scope contains only itself. A real hierarchy\n * (`tenant` ⊇ `school`) is a deliberately deferred extension point.\n */\nclass Scope extends ValueObject<ScopeProps, string> {\n private constructor(props: ScopeProps) {\n super(props);\n this.finalize();\n }\n\n /** Validates `\"type:id\"` or the global `\"*\"`, throwing on anything else. */\n static of(raw: string): Scope {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.BLANK,\n `scope must be a non-empty \"type:id\" string or \"*\", got \"${raw}\"`,\n );\n }\n\n if (raw !== \"*\" && !SCOPE_PATTERN.test(raw)) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.INVALID_FORMAT,\n `scope must be \"type:id\" (e.g. \"school:42\") or the global \"*\", got \"${raw}\"`,\n );\n }\n\n return new Scope({ raw });\n }\n\n /** The global scope `\"*\"`, which {@link includes} every other scope. */\n static global(): Scope {\n return new Scope({ raw: \"*\" });\n }\n\n /** The type segment, or `\"*\"` for the global scope. */\n get type(): string {\n if (this.value.raw === \"*\") return \"*\";\n return this.value.raw.slice(0, this.value.raw.indexOf(\":\"));\n }\n\n /** The id segment, or `undefined` for the global scope. */\n get id(): string | undefined {\n if (this.value.raw === \"*\") return undefined;\n return this.value.raw.slice(this.value.raw.indexOf(\":\") + 1);\n }\n\n /**\n * Whether this scope contains `target`. The global scope contains\n * everything; any other scope contains only an exact match of itself.\n */\n includes(target: Scope): boolean {\n if (this.value.raw === \"*\") return true;\n return this.value.raw === target.value.raw;\n }\n\n toPrimitive(): string {\n return this.value.raw;\n }\n}\n\nexport { Scope, type ScopeProps };\n","import { RequestedBy } from \"../../application/commands/requested-by.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\n\nimport { Role, type RoleProps } from \"./role.js\";\nimport { Scope } from \"./scope.js\";\n\n/**\n * The serializable shape a consumer persists and loads: the actor's identity\n * (`RequestedBy.raw`), the role they hold (as {@link RoleProps}), and the scope\n * the role applies in (the {@link Scope} string).\n */\ntype GrantProps = {\n readonly subject: string;\n readonly role: RoleProps;\n readonly scope: string;\n};\n\n/**\n * A role binding — it ties one actor to one {@link Role} within one\n * {@link Scope}. This is the unit the consumer stores (a `(subject, role,\n * scope)` row) and rehydrates into the pure decisor; the kit itself persists\n * nothing.\n *\n * A zod-free value object built through {@link of} from live\n * `RequestedBy`/`Role`/`Scope` objects; the getters reconstruct those objects\n * back from the frozen primitive form.\n */\nclass Grant extends ValueObject<GrantProps, GrantProps> {\n // The live objects are rehydrated once, at construction, and cached — the\n // decisor reads `role`/`scope` at least once per grant, so parsing lazily on\n // each getter access would re-parse the same frozen primitives repeatedly.\n private readonly _subject: RequestedBy;\n private readonly _role: Role;\n private readonly _scope: Scope;\n\n private constructor(props: GrantProps) {\n super(props);\n this._subject = RequestedBy.parse(this.value.subject);\n this._role = Role.fromProps(this.value.role);\n this._scope = Scope.of(this.value.scope);\n this.finalize();\n }\n\n static of(params: {\n subject: RequestedBy;\n role: Role;\n scope: Scope;\n }): Grant {\n return new Grant({\n subject: params.subject.raw,\n role: params.role.toPrimitive(),\n scope: params.scope.toPrimitive(),\n });\n }\n\n /**\n * Rebuilds a grant from its serialized {@link GrantProps} — the inverse of\n * {@link toPrimitive}, symmetric to {@link Role.fromProps}. Each part is\n * re-validated (subject through {@link RequestedBy.parse}, role through\n * {@link Role.fromProps}, scope through {@link Scope.of}), so a malformed\n * payload still fails loudly.\n */\n static fromProps(props: GrantProps): Grant {\n return new Grant(props);\n }\n\n get subject(): RequestedBy {\n return this._subject;\n }\n\n get role(): Role {\n return this._role;\n }\n\n get scope(): Scope {\n return this._scope;\n }\n\n /** Whether this grant belongs to `actor`. */\n appliesTo(actor: RequestedBy): boolean {\n return this.value.subject === actor.raw;\n }\n\n toPrimitive(): GrantProps {\n return {\n subject: this.value.subject,\n role: {\n name: this.value.role.name,\n permissions: [...this.value.role.permissions],\n },\n scope: this.value.scope,\n };\n }\n}\n\nexport { Grant, type GrantProps };\n","import type { Grant } from \"./grant.js\";\nimport type { Permission } from \"./permission.js\";\n\n/**\n * The flattened, deduplicated permissions an actor effectively holds across a\n * set of {@link Grant}s, together with the role names that contributed them.\n *\n * Pure resolution, isolated from the decisor so the \"roles → permissions\"\n * collapse can be tested on its own. Build through {@link fromGrants}; pre-filter\n * the grants to a single actor first when that matters (the set does not know\n * which actor it describes).\n */\nclass PermissionSet {\n private constructor(\n private readonly permissions: readonly Permission[],\n private readonly _roleNames: readonly string[],\n ) {}\n\n /**\n * Collapses the roles across `grants` into one permission set. Permissions\n * are deduplicated by their primitive form and role names by value, both\n * preserving first-seen order.\n */\n static fromGrants(grants: readonly Grant[]): PermissionSet {\n const permissions: Permission[] = [];\n const seenPermissions = new Set<string>();\n const roleNames: string[] = [];\n const seenRoles = new Set<string>();\n\n for (const grant of grants) {\n const role = grant.role;\n\n if (!seenRoles.has(role.name)) {\n seenRoles.add(role.name);\n roleNames.push(role.name);\n }\n\n for (const permission of role.permissions) {\n const primitive = permission.toPrimitive();\n if (seenPermissions.has(primitive)) continue;\n seenPermissions.add(primitive);\n permissions.push(permission);\n }\n }\n\n return new PermissionSet(permissions, roleNames);\n }\n\n /** Whether any held permission {@link Permission.implies | implies} `required`. */\n has(required: Permission): boolean {\n return this.permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n /** The effective permissions, deduplicated. */\n toArray(): readonly Permission[] {\n return this.permissions;\n }\n\n /** The names of the roles that contributed to this set, deduplicated. */\n get roleNames(): readonly string[] {\n return this._roleNames;\n }\n}\n\nexport { PermissionSet };\n","import type { RequestedBy } from \"../application/commands/requested-by.js\";\n\nimport type { Grant } from \"./domain/grant.js\";\nimport { PermissionSet } from \"./domain/permission-set.js\";\n\n/**\n * Projects an actor's RBAC facts into flat {@link PolicyContext}-style fields so\n * a declarative gate policy can reason over them (ABAC) — without importing the\n * policy engine or `zod`. It only builds a plain object; the consumer merges it\n * into `seed.fields` of `PolicyService.evaluate(...)`.\n *\n * The grants are filtered to `actor` first, so passing an actor's full grant\n * list (or a mixed one) is safe.\n *\n * **Wildcards are projected literally — they are NOT expanded.** `actor.roles`\n * and `actor.permissions` are the exact stored strings, so a permission such as\n * `\"orders:*\"` or `\"*:*\"` appears verbatim. A gate condition like\n * `{ field: \"actor.permissions\", op: \"in\", value: \"orders:cancel\" }` does a\n * *literal* membership test and will therefore NOT match a holder of\n * `\"orders:*\"`. Open-ended wildcards cannot be enumerated into a finite list, so\n * for wildcard-aware checks either match on the finite `actor.roles` instead, or\n * make the wildcard-aware decision with `RbacAuthorizer` (which knows\n * {@link Permission.implies}) and use the gate only for the extra ABAC\n * conditions.\n *\n * @returns `{ \"actor.id\", \"actor.roles\", \"actor.permissions\" }`, where roles and\n * permissions are plain string arrays (wildcards left literal).\n */\nexport function rbacContextFields(\n actor: RequestedBy,\n grants: readonly Grant[],\n): Record<string, unknown> {\n const set = PermissionSet.fromGrants(\n grants.filter((grant) => grant.appliesTo(actor)),\n );\n\n return {\n \"actor.id\": actor.raw,\n \"actor.roles\": [...set.roleNames],\n \"actor.permissions\": set\n .toArray()\n .map((permission) => permission.toPrimitive()),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"policy-bridge.cjs","names":["ValidationField","InvalidValueException","ValidationCode","Result","AuthorizationError","ValidationField","ValueObject","InvalidValueException","ValidationCode","ValidationField","ValueObject","InvalidValueException","ValidationCode","ValidationField","ValueObject","InvalidValueException","ValidationCode","ValueObject","RequestedBy"],"sources":["../src/core/rbac/authorizer.ts","../src/core/rbac/domain/permission.ts","../src/core/rbac/domain/role.ts","../src/core/rbac/domain/scope.ts","../src/core/rbac/domain/grant.ts","../src/core/rbac/domain/permission-set.ts","../src/core/rbac/policy-bridge.ts"],"sourcesContent":["import {\n AuthorizationError,\n type AuthorizationRequirement,\n} from \"../errors/authorization-error.js\";\nimport { ValidationCode } from \"../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../exceptions/validation-field.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AccessRequest } from \"./access-request.js\";\nimport type { Grant } from \"./domain/grant.js\";\n\nconst REQUIRED_FIELD = ValidationField.of(\"required\");\n\n/**\n * The pure RBAC decisor. Given an {@link AccessRequest} and the {@link Grant}s\n * already loaded for the actor, it answers \"may this actor do this?\" with no\n * I/O — no storage, no network, no clock — returning a {@link Result}. An\n * authorization *decision* is never thrown, always a value, mirroring the\n * \"errors as values\" contract of `UseCase`/`PolicyService`. Loading the grants\n * is the consumer's job (behind an `AuthorizerPort` adapter); this class only\n * decides.\n *\n * The one thing it *does* throw is {@link InvalidValueException} when the caller\n * misuses the API by passing a wildcard `required` permission — that is a\n * programming error surfaced loudly, not an authorization outcome.\n */\nclass RbacAuthorizer {\n /**\n * Decides the request against the supplied grants. The checks run\n * role → capability → scope so the denial reason is the most informative\n * one available:\n *\n * 1. no grant for the actor → `missing_role`;\n * 2. has grants but none grants the permission → `missing_capability`;\n * 3. grants the permission but not in scope → `out_of_scope`;\n * 4. otherwise → ALLOW (`Result.ok`).\n *\n * ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of\n * *which* grant or role decided it. Denials are rich (see the metadata\n * below) because they need to explain themselves; an allow does not. When an\n * ERP needs a \"who allowed this, and why\" trail, that is the adapter's job —\n * reconstruct it from the grants it loaded, or gate on `PermissionSet` before\n * calling. Keeping the decisor's allow-path pure is the deliberate trade.\n *\n * @throws {@link InvalidValueException} when `request.required` is a wildcard\n * permission (only *granted* permissions may wildcard, never the *required*\n * one).\n */\n authorize(\n request: AccessRequest,\n grants: readonly Grant[],\n ): Result<void, AuthorizationError> {\n if (request.required.hasWildcard()) {\n throw new InvalidValueException(\n REQUIRED_FIELD,\n ValidationCode.INVALID_FORMAT,\n `AccessRequest.required must be a concrete permission, not a wildcard, got \"${request.required.toPrimitive()}\"`,\n );\n }\n\n const required: AuthorizationRequirement = {\n capability: request.required.toPrimitive(),\n scope: request.scope.toPrimitive(),\n };\n const metadata = {\n action: request.action,\n resource: request.resource,\n actor: { actorId: request.actor.raw },\n required,\n };\n\n const actorGrants = grants.filter((grant) =>\n grant.appliesTo(request.actor),\n );\n if (actorGrants.length === 0) {\n return Result.err(AuthorizationError.missingRole(metadata));\n }\n\n const withPermission = actorGrants.filter((grant) =>\n grant.role.grants(request.required),\n );\n if (withPermission.length === 0) {\n return Result.err(AuthorizationError.missingCapability(metadata));\n }\n\n const inScope = withPermission.filter((grant) =>\n grant.scope.includes(request.scope),\n );\n if (inScope.length === 0) {\n return Result.err(AuthorizationError.outOfScope(metadata));\n }\n\n return Result.ok(undefined);\n }\n}\n\nexport { RbacAuthorizer };\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\";\n\n/** The two halves of a permission: what is acted on, and what is done to it. */\ntype PermissionProps = {\n readonly resource: string;\n readonly action: string;\n};\n\nconst PERMISSION_FIELD = ValidationField.of(\"permission\");\n\n// A segment is either a whole-segment wildcard (`*`) or a run of lowercase\n// alphanumerics, hyphens and underscores. Partial wildcards (`ord*`) are\n// rejected on purpose: a `*` only ever stands for an entire segment.\nconst SEGMENT_PATTERN = /^(?:\\*|[a-z0-9_-]+)$/u;\n\n/**\n * A `\"<resource>:<action>\"` capability such as `\"orders:cancel\"`, with a\n * single-level wildcard. Either segment may be the whole-segment wildcard `*`\n * (`\"orders:*\"`, `\"*:cancel\"`, `\"*:*\"`), which is what lets a broad grant cover\n * a narrower requirement — see {@link implies}.\n *\n * A zod-free value object: it validates its own format on construction\n * (throwing {@link InvalidValueException}) and is frozen thereafter. Build one\n * through {@link of} (from a raw `\"resource:action\"` string) or {@link for}\n * (from already-separated parts); the constructor is private.\n */\nclass Permission extends ValueObject<PermissionProps, string> {\n private constructor(props: PermissionProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Parses a `\"resource:action\"` string. Throws {@link InvalidValueException}\n * when the string is blank, lacks exactly one `:`, or carries an invalid\n * segment.\n */\n static of(raw: string): Permission {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.BLANK,\n `permission must be a non-empty \"resource:action\" string, got \"${raw}\"`,\n );\n }\n\n const parts = raw.split(\":\");\n if (parts.length !== 2) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.INVALID_FORMAT,\n `permission must have exactly one \":\" separating resource and action, got \"${raw}\"`,\n );\n }\n\n return Permission.for(parts[0], parts[1]);\n }\n\n /**\n * Builds from already-separated `resource` and `action` parts. Each must be\n * a valid segment (`[a-z0-9_-]+` or the whole-segment wildcard `*`).\n */\n static for(resource: string, action: string): Permission {\n Permission.assertSegment(resource, \"resource\");\n Permission.assertSegment(action, \"action\");\n return new Permission({ resource, action });\n }\n\n private static assertSegment(\n value: string,\n label: \"resource\" | \"action\",\n ): void {\n if (typeof value !== \"string\" || !SEGMENT_PATTERN.test(value)) {\n throw new InvalidValueException(\n PERMISSION_FIELD.nested(label),\n ValidationCode.INVALID_FORMAT,\n `permission ${label} must match [a-z0-9_-]+ or be the whole-segment wildcard \"*\" (no partial wildcards), got \"${value}\"`,\n );\n }\n }\n\n get resource(): string {\n return this.value.resource;\n }\n\n get action(): string {\n return this.value.action;\n }\n\n /**\n * Whether holding this permission grants `other`. A `*` segment matches any\n * value in the same position, so `\"orders:*\"` implies `\"orders:cancel\"` and\n * `\"*:*\"` implies everything. Concrete segments must match exactly.\n */\n implies(other: Permission): boolean {\n return (\n (this.resource === \"*\" || this.resource === other.resource) &&\n (this.action === \"*\" || this.action === other.action)\n );\n }\n\n /**\n * Whether either segment is the whole-segment wildcard `*`. A *granted*\n * permission may wildcard (see {@link implies}); a *required* one may not —\n * `RbacAuthorizer` rejects a wildcard requirement as caller misuse.\n */\n hasWildcard(): boolean {\n return this.value.resource === \"*\" || this.value.action === \"*\";\n }\n\n toPrimitive(): string {\n return `${this.value.resource}:${this.value.action}`;\n }\n}\n\nexport { Permission, type PermissionProps };\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\";\n\nimport { Permission } from \"./permission.js\";\n\n/**\n * The serializable shape of a role: a name plus its permissions kept as their\n * primitive `\"resource:action\"` strings (so the role round-trips through JSON\n * and stays deep-frozen). The {@link Role.permissions} getter rehydrates them\n * back into {@link Permission} objects.\n */\ntype RoleProps = {\n readonly name: string;\n readonly permissions: readonly string[];\n};\n\nconst ROLE_FIELD = ValidationField.of(\"role\");\n\n/**\n * A named bundle of {@link Permission}s — `\"cashier\"`, `\"manager\"`, `\"admin\"`.\n * There is **no role hierarchy** in v1: a role grants exactly the permissions\n * it lists (wildcards included). Duplicate permissions are collapsed on\n * construction, preserving first-seen order.\n *\n * A zod-free value object. Build through {@link of}; reconstruct a serialized\n * one through {@link fromProps}.\n */\nclass Role extends ValueObject<RoleProps, RoleProps> {\n // Parsed once, at construction, and cached: the decisor calls `grants` (and\n // `PermissionSet` reads `permissions`) repeatedly, so re-parsing the stored\n // strings on every access would be wasted work.\n private readonly _permissions: readonly Permission[];\n\n private constructor(props: RoleProps) {\n super(props);\n this._permissions = this.value.permissions.map(Permission.of);\n this.finalize();\n }\n\n /**\n * Builds a role from a name and its permissions. Throws\n * {@link InvalidValueException} when the name is blank. Duplicate\n * permissions are deduplicated.\n */\n static of(name: string, permissions: readonly Permission[]): Role {\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new InvalidValueException(\n ROLE_FIELD,\n ValidationCode.BLANK,\n `role name must be a non-empty string, got \"${name}\"`,\n );\n }\n\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const permission of permissions) {\n const primitive = permission.toPrimitive();\n if (seen.has(primitive)) continue;\n seen.add(primitive);\n deduped.push(primitive);\n }\n\n return new Role({ name, permissions: deduped });\n }\n\n /**\n * Rebuilds a role from its serialized {@link RoleProps} — the inverse of\n * {@link toPrimitive}. Each stored string is re-parsed through\n * {@link Permission.of}, so a malformed payload still fails loudly.\n */\n static fromProps(props: RoleProps): Role {\n return Role.of(props.name, props.permissions.map(Permission.of));\n }\n\n get name(): string {\n return this.value.name;\n }\n\n /** The role's permissions, rehydrated from their stored primitive form. */\n get permissions(): readonly Permission[] {\n return this._permissions;\n }\n\n /**\n * Two roles are equal when they share a name and the same *set* of\n * permissions. Order-insensitive on purpose: a role is a bundle, not a\n * sequence, so `Role.of(\"cashier\", [read, cancel])` equals\n * `Role.of(\"cashier\", [cancel, read])`. Overrides the base\n * {@link ValueObject.equals}, whose stable-stringify keeps array order and\n * would call those two roles different. The stored order (and thus\n * {@link permissions}/{@link toPrimitive}) is still first-seen; only the\n * comparison sorts.\n */\n override equals(other: this): boolean {\n if (this.value.name !== other.value.name) return false;\n const mine = [...this.value.permissions].sort();\n const theirs = [...other.value.permissions].sort();\n return (\n mine.length === theirs.length &&\n mine.every((permission, index) => permission === theirs[index])\n );\n }\n\n /** Whether any permission in this role {@link Permission.implies | implies} `required`. */\n grants(required: Permission): boolean {\n return this._permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n toPrimitive(): RoleProps {\n return {\n name: this.value.name,\n permissions: [...this.value.permissions],\n };\n }\n}\n\nexport { Role, type RoleProps };\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\";\n\n/** The wrapped, already-validated scope string (`\"type:id\"` or `\"*\"`). */\ntype ScopeProps = { readonly raw: string };\n\nconst SCOPE_FIELD = ValidationField.of(\"scope\");\n\n// \"type:id\": a lowercase type segment, a colon, then a non-empty id. The id\n// charset is permissive enough for UUIDs and numeric keys (`school:42`,\n// `tenant:550e8400-e29b-41d4-a716-446655440000`).\nconst SCOPE_PATTERN = /^[a-z][a-z0-9_-]*:[A-Za-z0-9._-]+$/u;\n\n/**\n * Where a grant (or a target resource) lives, aligned with\n * `AuthorizationRequirement.scope` and the policy `PolicyScope`\n * (`tenantId`/`schoolId`): `\"tenant:{id}\"`, `\"school:{id}\"`, or the global\n * `\"*\"`.\n *\n * v1 keeps containment ({@link includes}) flat — the global scope contains\n * everything, and any other scope contains only itself. A real hierarchy\n * (`tenant` ⊇ `school`) is a deliberately deferred extension point.\n */\nclass Scope extends ValueObject<ScopeProps, string> {\n private constructor(props: ScopeProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Validates `\"type:id\"` or the global `\"*\"`, throwing on anything else.\n * `Scope.of(\"*\")` is accepted and is equivalent to {@link Scope.global} —\n * prefer `global()` at call-sites for intent, `of(\"*\")` when rehydrating a\n * stored string.\n */\n static of(raw: string): Scope {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.BLANK,\n `scope must be a non-empty \"type:id\" string or \"*\", got \"${raw}\"`,\n );\n }\n\n if (raw !== \"*\" && !SCOPE_PATTERN.test(raw)) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.INVALID_FORMAT,\n `scope must be \"type:id\" (e.g. \"school:42\") or the global \"*\", got \"${raw}\"`,\n );\n }\n\n return new Scope({ raw });\n }\n\n /** The global scope `\"*\"`, which {@link includes} every other scope. */\n static global(): Scope {\n return new Scope({ raw: \"*\" });\n }\n\n /** The type segment, or `\"*\"` for the global scope. */\n get type(): string {\n if (this.value.raw === \"*\") return \"*\";\n return this.value.raw.slice(0, this.value.raw.indexOf(\":\"));\n }\n\n /** The id segment, or `undefined` for the global scope. */\n get id(): string | undefined {\n if (this.value.raw === \"*\") return undefined;\n return this.value.raw.slice(this.value.raw.indexOf(\":\") + 1);\n }\n\n /**\n * Whether this scope contains `target`. The global scope contains\n * everything; any other scope contains only an exact match of itself.\n */\n includes(target: Scope): boolean {\n if (this.value.raw === \"*\") return true;\n return this.value.raw === target.value.raw;\n }\n\n toPrimitive(): string {\n return this.value.raw;\n }\n}\n\nexport { Scope, type ScopeProps };\n","import { RequestedBy } from \"../../application/commands/requested-by.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\n\nimport { Role, type RoleProps } from \"./role.js\";\nimport { Scope } from \"./scope.js\";\n\n/**\n * The serializable shape a consumer persists and loads: the actor's identity\n * (`RequestedBy.raw`), the role they hold (as {@link RoleProps}), and the scope\n * the role applies in (the {@link Scope} string).\n */\ntype GrantProps = {\n readonly subject: string;\n readonly role: RoleProps;\n readonly scope: string;\n};\n\n/**\n * A role binding — it ties one actor to one {@link Role} within one\n * {@link Scope}. This is the unit the consumer stores (a `(subject, role,\n * scope)` row) and rehydrates into the pure decisor; the kit itself persists\n * nothing.\n *\n * A zod-free value object built through {@link of} from live\n * `RequestedBy`/`Role`/`Scope` objects; the getters reconstruct those objects\n * back from the frozen primitive form.\n */\nclass Grant extends ValueObject<GrantProps, GrantProps> {\n // The live objects are rehydrated once, at construction, and cached — the\n // decisor reads `role`/`scope` at least once per grant, so parsing lazily on\n // each getter access would re-parse the same frozen primitives repeatedly.\n private readonly _subject: RequestedBy;\n private readonly _role: Role;\n private readonly _scope: Scope;\n\n private constructor(props: GrantProps) {\n super(props);\n this._subject = RequestedBy.parse(this.value.subject);\n this._role = Role.fromProps(this.value.role);\n this._scope = Scope.of(this.value.scope);\n this.finalize();\n }\n\n static of(params: {\n subject: RequestedBy;\n role: Role;\n scope: Scope;\n }): Grant {\n return new Grant({\n subject: params.subject.raw,\n role: params.role.toPrimitive(),\n scope: params.scope.toPrimitive(),\n });\n }\n\n /**\n * Rebuilds a grant from its serialized {@link GrantProps} — the inverse of\n * {@link toPrimitive}, symmetric to {@link Role.fromProps}. Each part is\n * re-validated (subject through {@link RequestedBy.parse}, role through\n * {@link Role.fromProps}, scope through {@link Scope.of}), so a malformed\n * payload still fails loudly.\n */\n static fromProps(props: GrantProps): Grant {\n return new Grant(props);\n }\n\n get subject(): RequestedBy {\n return this._subject;\n }\n\n get role(): Role {\n return this._role;\n }\n\n get scope(): Scope {\n return this._scope;\n }\n\n /** Whether this grant belongs to `actor`. */\n appliesTo(actor: RequestedBy): boolean {\n return this.value.subject === actor.raw;\n }\n\n toPrimitive(): GrantProps {\n return {\n subject: this.value.subject,\n role: {\n name: this.value.role.name,\n permissions: [...this.value.role.permissions],\n },\n scope: this.value.scope,\n };\n }\n}\n\nexport { Grant, type GrantProps };\n","import type { Grant } from \"./grant.js\";\nimport type { Permission } from \"./permission.js\";\n\n/**\n * The flattened, deduplicated permissions an actor effectively holds across a\n * set of {@link Grant}s, together with the role names that contributed them.\n *\n * Pure resolution, isolated from the decisor so the \"roles → permissions\"\n * collapse can be tested on its own. Build through {@link fromGrants}; pre-filter\n * the grants to a single actor first when that matters (the set does not know\n * which actor it describes).\n */\nclass PermissionSet {\n private constructor(\n private readonly permissions: readonly Permission[],\n private readonly _roleNames: readonly string[],\n ) {}\n\n /**\n * Collapses the roles across `grants` into one permission set. Permissions\n * are deduplicated by their primitive form and role names by value, both\n * preserving first-seen order.\n */\n static fromGrants(grants: readonly Grant[]): PermissionSet {\n const permissions: Permission[] = [];\n const seenPermissions = new Set<string>();\n const roleNames: string[] = [];\n const seenRoles = new Set<string>();\n\n for (const grant of grants) {\n const role = grant.role;\n\n if (!seenRoles.has(role.name)) {\n seenRoles.add(role.name);\n roleNames.push(role.name);\n }\n\n for (const permission of role.permissions) {\n const primitive = permission.toPrimitive();\n if (seenPermissions.has(primitive)) continue;\n seenPermissions.add(primitive);\n permissions.push(permission);\n }\n }\n\n return new PermissionSet(permissions, roleNames);\n }\n\n /** Whether any held permission {@link Permission.implies | implies} `required`. */\n has(required: Permission): boolean {\n return this.permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n /** The effective permissions, deduplicated. */\n toArray(): readonly Permission[] {\n return this.permissions;\n }\n\n /** The names of the roles that contributed to this set, deduplicated. */\n get roleNames(): readonly string[] {\n return this._roleNames;\n }\n}\n\nexport { PermissionSet };\n","import type { RequestedBy } from \"../application/commands/requested-by.js\";\n\nimport type { Grant } from \"./domain/grant.js\";\nimport { PermissionSet } from \"./domain/permission-set.js\";\n\n/**\n * Projects an actor's RBAC facts into flat {@link PolicyContext}-style fields so\n * a declarative gate policy can reason over them (ABAC) — without importing the\n * policy engine or `zod`. It only builds a plain object; the consumer merges it\n * into `seed.fields` of `PolicyService.evaluate(...)`.\n *\n * The grants are filtered to `actor` first, so passing an actor's full grant\n * list (or a mixed one) is safe.\n *\n * **Wildcards are projected literally — they are NOT expanded.** `actor.roles`\n * and `actor.permissions` are the exact stored strings, so a permission such as\n * `\"orders:*\"` or `\"*:*\"` appears verbatim. A gate condition like\n * `{ field: \"actor.permissions\", op: \"in\", value: \"orders:cancel\" }` does a\n * *literal* membership test and will therefore NOT match a holder of\n * `\"orders:*\"`. Open-ended wildcards cannot be enumerated into a finite list, so\n * for wildcard-aware checks either match on the finite `actor.roles` instead, or\n * make the wildcard-aware decision with `RbacAuthorizer` (which knows\n * {@link Permission.implies}) and use the gate only for the extra ABAC\n * conditions.\n *\n * @returns `{ \"actor.id\", \"actor.roles\", \"actor.permissions\" }`, where roles and\n * permissions are plain string arrays (wildcards left literal).\n */\nexport function rbacContextFields(\n actor: RequestedBy,\n grants: readonly Grant[],\n): Record<string, unknown> {\n const set = PermissionSet.fromGrants(\n grants.filter((grant) => grant.appliesTo(actor)),\n );\n\n return {\n \"actor.id\": actor.raw,\n \"actor.roles\": [...set.roleNames],\n \"actor.permissions\": set\n .toArray()\n .map((permission) => permission.toPrimitive()),\n };\n}\n"],"mappings":"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"}
|
package/dist/policy-bridge.d.cts
CHANGED
|
@@ -43,6 +43,17 @@ declare class Role extends ValueObject<RoleProps, RoleProps> {
|
|
|
43
43
|
get name(): string;
|
|
44
44
|
/** The role's permissions, rehydrated from their stored primitive form. */
|
|
45
45
|
get permissions(): readonly Permission[];
|
|
46
|
+
/**
|
|
47
|
+
* Two roles are equal when they share a name and the same *set* of
|
|
48
|
+
* permissions. Order-insensitive on purpose: a role is a bundle, not a
|
|
49
|
+
* sequence, so `Role.of("cashier", [read, cancel])` equals
|
|
50
|
+
* `Role.of("cashier", [cancel, read])`. Overrides the base
|
|
51
|
+
* {@link ValueObject.equals}, whose stable-stringify keeps array order and
|
|
52
|
+
* would call those two roles different. The stored order (and thus
|
|
53
|
+
* {@link permissions}/{@link toPrimitive}) is still first-seen; only the
|
|
54
|
+
* comparison sorts.
|
|
55
|
+
*/
|
|
56
|
+
equals(other: this): boolean;
|
|
46
57
|
/** Whether any permission in this role {@link Permission.implies | implies} `required`. */
|
|
47
58
|
grants(required: Permission): boolean;
|
|
48
59
|
toPrimitive(): RoleProps;
|
|
@@ -120,6 +131,13 @@ declare class RbacAuthorizer {
|
|
|
120
131
|
* 3. grants the permission but not in scope → `out_of_scope`;
|
|
121
132
|
* 4. otherwise → ALLOW (`Result.ok`).
|
|
122
133
|
*
|
|
134
|
+
* ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of
|
|
135
|
+
* *which* grant or role decided it. Denials are rich (see the metadata
|
|
136
|
+
* below) because they need to explain themselves; an allow does not. When an
|
|
137
|
+
* ERP needs a "who allowed this, and why" trail, that is the adapter's job —
|
|
138
|
+
* reconstruct it from the grants it loaded, or gate on `PermissionSet` before
|
|
139
|
+
* calling. Keeping the decisor's allow-path pure is the deliberate trade.
|
|
140
|
+
*
|
|
123
141
|
* @throws {@link InvalidValueException} when `request.required` is a wildcard
|
|
124
142
|
* permission (only *granted* permissions may wildcard, never the *required*
|
|
125
143
|
* one).
|
package/dist/policy-bridge.d.ts
CHANGED
|
@@ -43,6 +43,17 @@ declare class Role extends ValueObject<RoleProps, RoleProps> {
|
|
|
43
43
|
get name(): string;
|
|
44
44
|
/** The role's permissions, rehydrated from their stored primitive form. */
|
|
45
45
|
get permissions(): readonly Permission[];
|
|
46
|
+
/**
|
|
47
|
+
* Two roles are equal when they share a name and the same *set* of
|
|
48
|
+
* permissions. Order-insensitive on purpose: a role is a bundle, not a
|
|
49
|
+
* sequence, so `Role.of("cashier", [read, cancel])` equals
|
|
50
|
+
* `Role.of("cashier", [cancel, read])`. Overrides the base
|
|
51
|
+
* {@link ValueObject.equals}, whose stable-stringify keeps array order and
|
|
52
|
+
* would call those two roles different. The stored order (and thus
|
|
53
|
+
* {@link permissions}/{@link toPrimitive}) is still first-seen; only the
|
|
54
|
+
* comparison sorts.
|
|
55
|
+
*/
|
|
56
|
+
equals(other: this): boolean;
|
|
46
57
|
/** Whether any permission in this role {@link Permission.implies | implies} `required`. */
|
|
47
58
|
grants(required: Permission): boolean;
|
|
48
59
|
toPrimitive(): RoleProps;
|
|
@@ -120,6 +131,13 @@ declare class RbacAuthorizer {
|
|
|
120
131
|
* 3. grants the permission but not in scope → `out_of_scope`;
|
|
121
132
|
* 4. otherwise → ALLOW (`Result.ok`).
|
|
122
133
|
*
|
|
134
|
+
* ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of
|
|
135
|
+
* *which* grant or role decided it. Denials are rich (see the metadata
|
|
136
|
+
* below) because they need to explain themselves; an allow does not. When an
|
|
137
|
+
* ERP needs a "who allowed this, and why" trail, that is the adapter's job —
|
|
138
|
+
* reconstruct it from the grants it loaded, or gate on `PermissionSet` before
|
|
139
|
+
* calling. Keeping the decisor's allow-path pure is the deliberate trade.
|
|
140
|
+
*
|
|
123
141
|
* @throws {@link InvalidValueException} when `request.required` is a wildcard
|
|
124
142
|
* permission (only *granted* permissions may wildcard, never the *required*
|
|
125
143
|
* one).
|
package/dist/policy-bridge.js
CHANGED
|
@@ -31,6 +31,13 @@ var RbacAuthorizer = class {
|
|
|
31
31
|
* 3. grants the permission but not in scope → `out_of_scope`;
|
|
32
32
|
* 4. otherwise → ALLOW (`Result.ok`).
|
|
33
33
|
*
|
|
34
|
+
* ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of
|
|
35
|
+
* *which* grant or role decided it. Denials are rich (see the metadata
|
|
36
|
+
* below) because they need to explain themselves; an allow does not. When an
|
|
37
|
+
* ERP needs a "who allowed this, and why" trail, that is the adapter's job —
|
|
38
|
+
* reconstruct it from the grants it loaded, or gate on `PermissionSet` before
|
|
39
|
+
* calling. Keeping the decisor's allow-path pure is the deliberate trade.
|
|
40
|
+
*
|
|
34
41
|
* @throws {@link InvalidValueException} when `request.required` is a wildcard
|
|
35
42
|
* permission (only *granted* permissions may wildcard, never the *required*
|
|
36
43
|
* one).
|
|
@@ -44,7 +51,7 @@ var RbacAuthorizer = class {
|
|
|
44
51
|
const metadata = {
|
|
45
52
|
action: request.action,
|
|
46
53
|
resource: request.resource,
|
|
47
|
-
actor: {
|
|
54
|
+
actor: { actorId: request.actor.raw },
|
|
48
55
|
required
|
|
49
56
|
};
|
|
50
57
|
const actorGrants = grants.filter((grant) => grant.appliesTo(request.actor));
|
|
@@ -180,6 +187,22 @@ var Role = class Role extends ValueObject {
|
|
|
180
187
|
get permissions() {
|
|
181
188
|
return this._permissions;
|
|
182
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Two roles are equal when they share a name and the same *set* of
|
|
192
|
+
* permissions. Order-insensitive on purpose: a role is a bundle, not a
|
|
193
|
+
* sequence, so `Role.of("cashier", [read, cancel])` equals
|
|
194
|
+
* `Role.of("cashier", [cancel, read])`. Overrides the base
|
|
195
|
+
* {@link ValueObject.equals}, whose stable-stringify keeps array order and
|
|
196
|
+
* would call those two roles different. The stored order (and thus
|
|
197
|
+
* {@link permissions}/{@link toPrimitive}) is still first-seen; only the
|
|
198
|
+
* comparison sorts.
|
|
199
|
+
*/
|
|
200
|
+
equals(other) {
|
|
201
|
+
if (this.value.name !== other.value.name) return false;
|
|
202
|
+
const mine = [...this.value.permissions].sort();
|
|
203
|
+
const theirs = [...other.value.permissions].sort();
|
|
204
|
+
return mine.length === theirs.length && mine.every((permission, index) => permission === theirs[index]);
|
|
205
|
+
}
|
|
183
206
|
/** Whether any permission in this role {@link Permission.implies | implies} `required`. */
|
|
184
207
|
grants(required) {
|
|
185
208
|
return this._permissions.some((permission) => permission.implies(required));
|
|
@@ -210,7 +233,12 @@ var Scope = class Scope extends ValueObject {
|
|
|
210
233
|
super(props);
|
|
211
234
|
this.finalize();
|
|
212
235
|
}
|
|
213
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* Validates `"type:id"` or the global `"*"`, throwing on anything else.
|
|
238
|
+
* `Scope.of("*")` is accepted and is equivalent to {@link Scope.global} —
|
|
239
|
+
* prefer `global()` at call-sites for intent, `of("*")` when rehydrating a
|
|
240
|
+
* stored string.
|
|
241
|
+
*/
|
|
214
242
|
static of(raw) {
|
|
215
243
|
if (typeof raw !== "string" || raw.trim().length === 0) throw new InvalidValueException(SCOPE_FIELD, ValidationCode.BLANK, `scope must be a non-empty "type:id" string or "*", got "${raw}"`);
|
|
216
244
|
if (raw !== "*" && !SCOPE_PATTERN.test(raw)) throw new InvalidValueException(SCOPE_FIELD, ValidationCode.INVALID_FORMAT, `scope must be "type:id" (e.g. "school:42") or the global "*", got "${raw}"`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-bridge.js","names":[],"sources":["../src/core/rbac/authorizer.ts","../src/core/rbac/domain/permission.ts","../src/core/rbac/domain/role.ts","../src/core/rbac/domain/scope.ts","../src/core/rbac/domain/grant.ts","../src/core/rbac/domain/permission-set.ts","../src/core/rbac/policy-bridge.ts"],"sourcesContent":["import {\n AuthorizationError,\n type AuthorizationRequirement,\n} from \"../errors/authorization-error.js\";\nimport { ValidationCode } from \"../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../exceptions/validation-field.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AccessRequest } from \"./access-request.js\";\nimport type { Grant } from \"./domain/grant.js\";\n\nconst REQUIRED_FIELD = ValidationField.of(\"required\");\n\n/**\n * The pure RBAC decisor. Given an {@link AccessRequest} and the {@link Grant}s\n * already loaded for the actor, it answers \"may this actor do this?\" with no\n * I/O — no storage, no network, no clock — returning a {@link Result}. An\n * authorization *decision* is never thrown, always a value, mirroring the\n * \"errors as values\" contract of `UseCase`/`PolicyService`. Loading the grants\n * is the consumer's job (behind an `AuthorizerPort` adapter); this class only\n * decides.\n *\n * The one thing it *does* throw is {@link InvalidValueException} when the caller\n * misuses the API by passing a wildcard `required` permission — that is a\n * programming error surfaced loudly, not an authorization outcome.\n */\nclass RbacAuthorizer {\n /**\n * Decides the request against the supplied grants. The checks run\n * role → capability → scope so the denial reason is the most informative\n * one available:\n *\n * 1. no grant for the actor → `missing_role`;\n * 2. has grants but none grants the permission → `missing_capability`;\n * 3. grants the permission but not in scope → `out_of_scope`;\n * 4. otherwise → ALLOW (`Result.ok`).\n *\n * @throws {@link InvalidValueException} when `request.required` is a wildcard\n * permission (only *granted* permissions may wildcard, never the *required*\n * one).\n */\n authorize(\n request: AccessRequest,\n grants: readonly Grant[],\n ): Result<void, AuthorizationError> {\n if (request.required.hasWildcard()) {\n throw new InvalidValueException(\n REQUIRED_FIELD,\n ValidationCode.INVALID_FORMAT,\n `AccessRequest.required must be a concrete permission, not a wildcard, got \"${request.required.toPrimitive()}\"`,\n );\n }\n\n const required: AuthorizationRequirement = {\n capability: request.required.toPrimitive(),\n scope: request.scope.toPrimitive(),\n };\n const metadata = {\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n required,\n };\n\n const actorGrants = grants.filter((grant) =>\n grant.appliesTo(request.actor),\n );\n if (actorGrants.length === 0) {\n return Result.err(AuthorizationError.missingRole(metadata));\n }\n\n const withPermission = actorGrants.filter((grant) =>\n grant.role.grants(request.required),\n );\n if (withPermission.length === 0) {\n return Result.err(AuthorizationError.missingCapability(metadata));\n }\n\n const inScope = withPermission.filter((grant) =>\n grant.scope.includes(request.scope),\n );\n if (inScope.length === 0) {\n return Result.err(AuthorizationError.outOfScope(metadata));\n }\n\n return Result.ok(undefined);\n }\n}\n\nexport { RbacAuthorizer };\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\";\n\n/** The two halves of a permission: what is acted on, and what is done to it. */\ntype PermissionProps = {\n readonly resource: string;\n readonly action: string;\n};\n\nconst PERMISSION_FIELD = ValidationField.of(\"permission\");\n\n// A segment is either a whole-segment wildcard (`*`) or a run of lowercase\n// alphanumerics, hyphens and underscores. Partial wildcards (`ord*`) are\n// rejected on purpose: a `*` only ever stands for an entire segment.\nconst SEGMENT_PATTERN = /^(?:\\*|[a-z0-9_-]+)$/u;\n\n/**\n * A `\"<resource>:<action>\"` capability such as `\"orders:cancel\"`, with a\n * single-level wildcard. Either segment may be the whole-segment wildcard `*`\n * (`\"orders:*\"`, `\"*:cancel\"`, `\"*:*\"`), which is what lets a broad grant cover\n * a narrower requirement — see {@link implies}.\n *\n * A zod-free value object: it validates its own format on construction\n * (throwing {@link InvalidValueException}) and is frozen thereafter. Build one\n * through {@link of} (from a raw `\"resource:action\"` string) or {@link for}\n * (from already-separated parts); the constructor is private.\n */\nclass Permission extends ValueObject<PermissionProps, string> {\n private constructor(props: PermissionProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Parses a `\"resource:action\"` string. Throws {@link InvalidValueException}\n * when the string is blank, lacks exactly one `:`, or carries an invalid\n * segment.\n */\n static of(raw: string): Permission {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.BLANK,\n `permission must be a non-empty \"resource:action\" string, got \"${raw}\"`,\n );\n }\n\n const parts = raw.split(\":\");\n if (parts.length !== 2) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.INVALID_FORMAT,\n `permission must have exactly one \":\" separating resource and action, got \"${raw}\"`,\n );\n }\n\n return Permission.for(parts[0], parts[1]);\n }\n\n /**\n * Builds from already-separated `resource` and `action` parts. Each must be\n * a valid segment (`[a-z0-9_-]+` or the whole-segment wildcard `*`).\n */\n static for(resource: string, action: string): Permission {\n Permission.assertSegment(resource, \"resource\");\n Permission.assertSegment(action, \"action\");\n return new Permission({ resource, action });\n }\n\n private static assertSegment(\n value: string,\n label: \"resource\" | \"action\",\n ): void {\n if (typeof value !== \"string\" || !SEGMENT_PATTERN.test(value)) {\n throw new InvalidValueException(\n PERMISSION_FIELD.nested(label),\n ValidationCode.INVALID_FORMAT,\n `permission ${label} must match [a-z0-9_-]+ or be the whole-segment wildcard \"*\" (no partial wildcards), got \"${value}\"`,\n );\n }\n }\n\n get resource(): string {\n return this.value.resource;\n }\n\n get action(): string {\n return this.value.action;\n }\n\n /**\n * Whether holding this permission grants `other`. A `*` segment matches any\n * value in the same position, so `\"orders:*\"` implies `\"orders:cancel\"` and\n * `\"*:*\"` implies everything. Concrete segments must match exactly.\n */\n implies(other: Permission): boolean {\n return (\n (this.resource === \"*\" || this.resource === other.resource) &&\n (this.action === \"*\" || this.action === other.action)\n );\n }\n\n /**\n * Whether either segment is the whole-segment wildcard `*`. A *granted*\n * permission may wildcard (see {@link implies}); a *required* one may not —\n * `RbacAuthorizer` rejects a wildcard requirement as caller misuse.\n */\n hasWildcard(): boolean {\n return this.value.resource === \"*\" || this.value.action === \"*\";\n }\n\n toPrimitive(): string {\n return `${this.value.resource}:${this.value.action}`;\n }\n}\n\nexport { Permission, type PermissionProps };\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\";\n\nimport { Permission } from \"./permission.js\";\n\n/**\n * The serializable shape of a role: a name plus its permissions kept as their\n * primitive `\"resource:action\"` strings (so the role round-trips through JSON\n * and stays deep-frozen). The {@link Role.permissions} getter rehydrates them\n * back into {@link Permission} objects.\n */\ntype RoleProps = {\n readonly name: string;\n readonly permissions: readonly string[];\n};\n\nconst ROLE_FIELD = ValidationField.of(\"role\");\n\n/**\n * A named bundle of {@link Permission}s — `\"cashier\"`, `\"manager\"`, `\"admin\"`.\n * There is **no role hierarchy** in v1: a role grants exactly the permissions\n * it lists (wildcards included). Duplicate permissions are collapsed on\n * construction, preserving first-seen order.\n *\n * A zod-free value object. Build through {@link of}; reconstruct a serialized\n * one through {@link fromProps}.\n */\nclass Role extends ValueObject<RoleProps, RoleProps> {\n // Parsed once, at construction, and cached: the decisor calls `grants` (and\n // `PermissionSet` reads `permissions`) repeatedly, so re-parsing the stored\n // strings on every access would be wasted work.\n private readonly _permissions: readonly Permission[];\n\n private constructor(props: RoleProps) {\n super(props);\n this._permissions = this.value.permissions.map(Permission.of);\n this.finalize();\n }\n\n /**\n * Builds a role from a name and its permissions. Throws\n * {@link InvalidValueException} when the name is blank. Duplicate\n * permissions are deduplicated.\n */\n static of(name: string, permissions: readonly Permission[]): Role {\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new InvalidValueException(\n ROLE_FIELD,\n ValidationCode.BLANK,\n `role name must be a non-empty string, got \"${name}\"`,\n );\n }\n\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const permission of permissions) {\n const primitive = permission.toPrimitive();\n if (seen.has(primitive)) continue;\n seen.add(primitive);\n deduped.push(primitive);\n }\n\n return new Role({ name, permissions: deduped });\n }\n\n /**\n * Rebuilds a role from its serialized {@link RoleProps} — the inverse of\n * {@link toPrimitive}. Each stored string is re-parsed through\n * {@link Permission.of}, so a malformed payload still fails loudly.\n */\n static fromProps(props: RoleProps): Role {\n return Role.of(props.name, props.permissions.map(Permission.of));\n }\n\n get name(): string {\n return this.value.name;\n }\n\n /** The role's permissions, rehydrated from their stored primitive form. */\n get permissions(): readonly Permission[] {\n return this._permissions;\n }\n\n /** Whether any permission in this role {@link Permission.implies | implies} `required`. */\n grants(required: Permission): boolean {\n return this._permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n toPrimitive(): RoleProps {\n return {\n name: this.value.name,\n permissions: [...this.value.permissions],\n };\n }\n}\n\nexport { Role, type RoleProps };\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\";\n\n/** The wrapped, already-validated scope string (`\"type:id\"` or `\"*\"`). */\ntype ScopeProps = { readonly raw: string };\n\nconst SCOPE_FIELD = ValidationField.of(\"scope\");\n\n// \"type:id\": a lowercase type segment, a colon, then a non-empty id. The id\n// charset is permissive enough for UUIDs and numeric keys (`school:42`,\n// `tenant:550e8400-e29b-41d4-a716-446655440000`).\nconst SCOPE_PATTERN = /^[a-z][a-z0-9_-]*:[A-Za-z0-9._-]+$/u;\n\n/**\n * Where a grant (or a target resource) lives, aligned with\n * `AuthorizationRequirement.scope` and the policy `PolicyScope`\n * (`tenantId`/`schoolId`): `\"tenant:{id}\"`, `\"school:{id}\"`, or the global\n * `\"*\"`.\n *\n * v1 keeps containment ({@link includes}) flat — the global scope contains\n * everything, and any other scope contains only itself. A real hierarchy\n * (`tenant` ⊇ `school`) is a deliberately deferred extension point.\n */\nclass Scope extends ValueObject<ScopeProps, string> {\n private constructor(props: ScopeProps) {\n super(props);\n this.finalize();\n }\n\n /** Validates `\"type:id\"` or the global `\"*\"`, throwing on anything else. */\n static of(raw: string): Scope {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.BLANK,\n `scope must be a non-empty \"type:id\" string or \"*\", got \"${raw}\"`,\n );\n }\n\n if (raw !== \"*\" && !SCOPE_PATTERN.test(raw)) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.INVALID_FORMAT,\n `scope must be \"type:id\" (e.g. \"school:42\") or the global \"*\", got \"${raw}\"`,\n );\n }\n\n return new Scope({ raw });\n }\n\n /** The global scope `\"*\"`, which {@link includes} every other scope. */\n static global(): Scope {\n return new Scope({ raw: \"*\" });\n }\n\n /** The type segment, or `\"*\"` for the global scope. */\n get type(): string {\n if (this.value.raw === \"*\") return \"*\";\n return this.value.raw.slice(0, this.value.raw.indexOf(\":\"));\n }\n\n /** The id segment, or `undefined` for the global scope. */\n get id(): string | undefined {\n if (this.value.raw === \"*\") return undefined;\n return this.value.raw.slice(this.value.raw.indexOf(\":\") + 1);\n }\n\n /**\n * Whether this scope contains `target`. The global scope contains\n * everything; any other scope contains only an exact match of itself.\n */\n includes(target: Scope): boolean {\n if (this.value.raw === \"*\") return true;\n return this.value.raw === target.value.raw;\n }\n\n toPrimitive(): string {\n return this.value.raw;\n }\n}\n\nexport { Scope, type ScopeProps };\n","import { RequestedBy } from \"../../application/commands/requested-by.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\n\nimport { Role, type RoleProps } from \"./role.js\";\nimport { Scope } from \"./scope.js\";\n\n/**\n * The serializable shape a consumer persists and loads: the actor's identity\n * (`RequestedBy.raw`), the role they hold (as {@link RoleProps}), and the scope\n * the role applies in (the {@link Scope} string).\n */\ntype GrantProps = {\n readonly subject: string;\n readonly role: RoleProps;\n readonly scope: string;\n};\n\n/**\n * A role binding — it ties one actor to one {@link Role} within one\n * {@link Scope}. This is the unit the consumer stores (a `(subject, role,\n * scope)` row) and rehydrates into the pure decisor; the kit itself persists\n * nothing.\n *\n * A zod-free value object built through {@link of} from live\n * `RequestedBy`/`Role`/`Scope` objects; the getters reconstruct those objects\n * back from the frozen primitive form.\n */\nclass Grant extends ValueObject<GrantProps, GrantProps> {\n // The live objects are rehydrated once, at construction, and cached — the\n // decisor reads `role`/`scope` at least once per grant, so parsing lazily on\n // each getter access would re-parse the same frozen primitives repeatedly.\n private readonly _subject: RequestedBy;\n private readonly _role: Role;\n private readonly _scope: Scope;\n\n private constructor(props: GrantProps) {\n super(props);\n this._subject = RequestedBy.parse(this.value.subject);\n this._role = Role.fromProps(this.value.role);\n this._scope = Scope.of(this.value.scope);\n this.finalize();\n }\n\n static of(params: {\n subject: RequestedBy;\n role: Role;\n scope: Scope;\n }): Grant {\n return new Grant({\n subject: params.subject.raw,\n role: params.role.toPrimitive(),\n scope: params.scope.toPrimitive(),\n });\n }\n\n /**\n * Rebuilds a grant from its serialized {@link GrantProps} — the inverse of\n * {@link toPrimitive}, symmetric to {@link Role.fromProps}. Each part is\n * re-validated (subject through {@link RequestedBy.parse}, role through\n * {@link Role.fromProps}, scope through {@link Scope.of}), so a malformed\n * payload still fails loudly.\n */\n static fromProps(props: GrantProps): Grant {\n return new Grant(props);\n }\n\n get subject(): RequestedBy {\n return this._subject;\n }\n\n get role(): Role {\n return this._role;\n }\n\n get scope(): Scope {\n return this._scope;\n }\n\n /** Whether this grant belongs to `actor`. */\n appliesTo(actor: RequestedBy): boolean {\n return this.value.subject === actor.raw;\n }\n\n toPrimitive(): GrantProps {\n return {\n subject: this.value.subject,\n role: {\n name: this.value.role.name,\n permissions: [...this.value.role.permissions],\n },\n scope: this.value.scope,\n };\n }\n}\n\nexport { Grant, type GrantProps };\n","import type { Grant } from \"./grant.js\";\nimport type { Permission } from \"./permission.js\";\n\n/**\n * The flattened, deduplicated permissions an actor effectively holds across a\n * set of {@link Grant}s, together with the role names that contributed them.\n *\n * Pure resolution, isolated from the decisor so the \"roles → permissions\"\n * collapse can be tested on its own. Build through {@link fromGrants}; pre-filter\n * the grants to a single actor first when that matters (the set does not know\n * which actor it describes).\n */\nclass PermissionSet {\n private constructor(\n private readonly permissions: readonly Permission[],\n private readonly _roleNames: readonly string[],\n ) {}\n\n /**\n * Collapses the roles across `grants` into one permission set. Permissions\n * are deduplicated by their primitive form and role names by value, both\n * preserving first-seen order.\n */\n static fromGrants(grants: readonly Grant[]): PermissionSet {\n const permissions: Permission[] = [];\n const seenPermissions = new Set<string>();\n const roleNames: string[] = [];\n const seenRoles = new Set<string>();\n\n for (const grant of grants) {\n const role = grant.role;\n\n if (!seenRoles.has(role.name)) {\n seenRoles.add(role.name);\n roleNames.push(role.name);\n }\n\n for (const permission of role.permissions) {\n const primitive = permission.toPrimitive();\n if (seenPermissions.has(primitive)) continue;\n seenPermissions.add(primitive);\n permissions.push(permission);\n }\n }\n\n return new PermissionSet(permissions, roleNames);\n }\n\n /** Whether any held permission {@link Permission.implies | implies} `required`. */\n has(required: Permission): boolean {\n return this.permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n /** The effective permissions, deduplicated. */\n toArray(): readonly Permission[] {\n return this.permissions;\n }\n\n /** The names of the roles that contributed to this set, deduplicated. */\n get roleNames(): readonly string[] {\n return this._roleNames;\n }\n}\n\nexport { PermissionSet };\n","import type { RequestedBy } from \"../application/commands/requested-by.js\";\n\nimport type { Grant } from \"./domain/grant.js\";\nimport { PermissionSet } from \"./domain/permission-set.js\";\n\n/**\n * Projects an actor's RBAC facts into flat {@link PolicyContext}-style fields so\n * a declarative gate policy can reason over them (ABAC) — without importing the\n * policy engine or `zod`. It only builds a plain object; the consumer merges it\n * into `seed.fields` of `PolicyService.evaluate(...)`.\n *\n * The grants are filtered to `actor` first, so passing an actor's full grant\n * list (or a mixed one) is safe.\n *\n * **Wildcards are projected literally — they are NOT expanded.** `actor.roles`\n * and `actor.permissions` are the exact stored strings, so a permission such as\n * `\"orders:*\"` or `\"*:*\"` appears verbatim. A gate condition like\n * `{ field: \"actor.permissions\", op: \"in\", value: \"orders:cancel\" }` does a\n * *literal* membership test and will therefore NOT match a holder of\n * `\"orders:*\"`. Open-ended wildcards cannot be enumerated into a finite list, so\n * for wildcard-aware checks either match on the finite `actor.roles` instead, or\n * make the wildcard-aware decision with `RbacAuthorizer` (which knows\n * {@link Permission.implies}) and use the gate only for the extra ABAC\n * conditions.\n *\n * @returns `{ \"actor.id\", \"actor.roles\", \"actor.permissions\" }`, where roles and\n * permissions are plain string arrays (wildcards left literal).\n */\nexport function rbacContextFields(\n actor: RequestedBy,\n grants: readonly Grant[],\n): Record<string, unknown> {\n const set = PermissionSet.fromGrants(\n grants.filter((grant) => grant.appliesTo(actor)),\n );\n\n return {\n \"actor.id\": actor.raw,\n \"actor.roles\": [...set.roleNames],\n \"actor.permissions\": set\n .toArray()\n .map((permission) => permission.toPrimitive()),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"policy-bridge.js","names":[],"sources":["../src/core/rbac/authorizer.ts","../src/core/rbac/domain/permission.ts","../src/core/rbac/domain/role.ts","../src/core/rbac/domain/scope.ts","../src/core/rbac/domain/grant.ts","../src/core/rbac/domain/permission-set.ts","../src/core/rbac/policy-bridge.ts"],"sourcesContent":["import {\n AuthorizationError,\n type AuthorizationRequirement,\n} from \"../errors/authorization-error.js\";\nimport { ValidationCode } from \"../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../exceptions/validation-field.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AccessRequest } from \"./access-request.js\";\nimport type { Grant } from \"./domain/grant.js\";\n\nconst REQUIRED_FIELD = ValidationField.of(\"required\");\n\n/**\n * The pure RBAC decisor. Given an {@link AccessRequest} and the {@link Grant}s\n * already loaded for the actor, it answers \"may this actor do this?\" with no\n * I/O — no storage, no network, no clock — returning a {@link Result}. An\n * authorization *decision* is never thrown, always a value, mirroring the\n * \"errors as values\" contract of `UseCase`/`PolicyService`. Loading the grants\n * is the consumer's job (behind an `AuthorizerPort` adapter); this class only\n * decides.\n *\n * The one thing it *does* throw is {@link InvalidValueException} when the caller\n * misuses the API by passing a wildcard `required` permission — that is a\n * programming error surfaced loudly, not an authorization outcome.\n */\nclass RbacAuthorizer {\n /**\n * Decides the request against the supplied grants. The checks run\n * role → capability → scope so the denial reason is the most informative\n * one available:\n *\n * 1. no grant for the actor → `missing_role`;\n * 2. has grants but none grants the permission → `missing_capability`;\n * 3. grants the permission but not in scope → `out_of_scope`;\n * 4. otherwise → ALLOW (`Result.ok`).\n *\n * ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of\n * *which* grant or role decided it. Denials are rich (see the metadata\n * below) because they need to explain themselves; an allow does not. When an\n * ERP needs a \"who allowed this, and why\" trail, that is the adapter's job —\n * reconstruct it from the grants it loaded, or gate on `PermissionSet` before\n * calling. Keeping the decisor's allow-path pure is the deliberate trade.\n *\n * @throws {@link InvalidValueException} when `request.required` is a wildcard\n * permission (only *granted* permissions may wildcard, never the *required*\n * one).\n */\n authorize(\n request: AccessRequest,\n grants: readonly Grant[],\n ): Result<void, AuthorizationError> {\n if (request.required.hasWildcard()) {\n throw new InvalidValueException(\n REQUIRED_FIELD,\n ValidationCode.INVALID_FORMAT,\n `AccessRequest.required must be a concrete permission, not a wildcard, got \"${request.required.toPrimitive()}\"`,\n );\n }\n\n const required: AuthorizationRequirement = {\n capability: request.required.toPrimitive(),\n scope: request.scope.toPrimitive(),\n };\n const metadata = {\n action: request.action,\n resource: request.resource,\n actor: { actorId: request.actor.raw },\n required,\n };\n\n const actorGrants = grants.filter((grant) =>\n grant.appliesTo(request.actor),\n );\n if (actorGrants.length === 0) {\n return Result.err(AuthorizationError.missingRole(metadata));\n }\n\n const withPermission = actorGrants.filter((grant) =>\n grant.role.grants(request.required),\n );\n if (withPermission.length === 0) {\n return Result.err(AuthorizationError.missingCapability(metadata));\n }\n\n const inScope = withPermission.filter((grant) =>\n grant.scope.includes(request.scope),\n );\n if (inScope.length === 0) {\n return Result.err(AuthorizationError.outOfScope(metadata));\n }\n\n return Result.ok(undefined);\n }\n}\n\nexport { RbacAuthorizer };\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\";\n\n/** The two halves of a permission: what is acted on, and what is done to it. */\ntype PermissionProps = {\n readonly resource: string;\n readonly action: string;\n};\n\nconst PERMISSION_FIELD = ValidationField.of(\"permission\");\n\n// A segment is either a whole-segment wildcard (`*`) or a run of lowercase\n// alphanumerics, hyphens and underscores. Partial wildcards (`ord*`) are\n// rejected on purpose: a `*` only ever stands for an entire segment.\nconst SEGMENT_PATTERN = /^(?:\\*|[a-z0-9_-]+)$/u;\n\n/**\n * A `\"<resource>:<action>\"` capability such as `\"orders:cancel\"`, with a\n * single-level wildcard. Either segment may be the whole-segment wildcard `*`\n * (`\"orders:*\"`, `\"*:cancel\"`, `\"*:*\"`), which is what lets a broad grant cover\n * a narrower requirement — see {@link implies}.\n *\n * A zod-free value object: it validates its own format on construction\n * (throwing {@link InvalidValueException}) and is frozen thereafter. Build one\n * through {@link of} (from a raw `\"resource:action\"` string) or {@link for}\n * (from already-separated parts); the constructor is private.\n */\nclass Permission extends ValueObject<PermissionProps, string> {\n private constructor(props: PermissionProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Parses a `\"resource:action\"` string. Throws {@link InvalidValueException}\n * when the string is blank, lacks exactly one `:`, or carries an invalid\n * segment.\n */\n static of(raw: string): Permission {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.BLANK,\n `permission must be a non-empty \"resource:action\" string, got \"${raw}\"`,\n );\n }\n\n const parts = raw.split(\":\");\n if (parts.length !== 2) {\n throw new InvalidValueException(\n PERMISSION_FIELD,\n ValidationCode.INVALID_FORMAT,\n `permission must have exactly one \":\" separating resource and action, got \"${raw}\"`,\n );\n }\n\n return Permission.for(parts[0], parts[1]);\n }\n\n /**\n * Builds from already-separated `resource` and `action` parts. Each must be\n * a valid segment (`[a-z0-9_-]+` or the whole-segment wildcard `*`).\n */\n static for(resource: string, action: string): Permission {\n Permission.assertSegment(resource, \"resource\");\n Permission.assertSegment(action, \"action\");\n return new Permission({ resource, action });\n }\n\n private static assertSegment(\n value: string,\n label: \"resource\" | \"action\",\n ): void {\n if (typeof value !== \"string\" || !SEGMENT_PATTERN.test(value)) {\n throw new InvalidValueException(\n PERMISSION_FIELD.nested(label),\n ValidationCode.INVALID_FORMAT,\n `permission ${label} must match [a-z0-9_-]+ or be the whole-segment wildcard \"*\" (no partial wildcards), got \"${value}\"`,\n );\n }\n }\n\n get resource(): string {\n return this.value.resource;\n }\n\n get action(): string {\n return this.value.action;\n }\n\n /**\n * Whether holding this permission grants `other`. A `*` segment matches any\n * value in the same position, so `\"orders:*\"` implies `\"orders:cancel\"` and\n * `\"*:*\"` implies everything. Concrete segments must match exactly.\n */\n implies(other: Permission): boolean {\n return (\n (this.resource === \"*\" || this.resource === other.resource) &&\n (this.action === \"*\" || this.action === other.action)\n );\n }\n\n /**\n * Whether either segment is the whole-segment wildcard `*`. A *granted*\n * permission may wildcard (see {@link implies}); a *required* one may not —\n * `RbacAuthorizer` rejects a wildcard requirement as caller misuse.\n */\n hasWildcard(): boolean {\n return this.value.resource === \"*\" || this.value.action === \"*\";\n }\n\n toPrimitive(): string {\n return `${this.value.resource}:${this.value.action}`;\n }\n}\n\nexport { Permission, type PermissionProps };\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\";\n\nimport { Permission } from \"./permission.js\";\n\n/**\n * The serializable shape of a role: a name plus its permissions kept as their\n * primitive `\"resource:action\"` strings (so the role round-trips through JSON\n * and stays deep-frozen). The {@link Role.permissions} getter rehydrates them\n * back into {@link Permission} objects.\n */\ntype RoleProps = {\n readonly name: string;\n readonly permissions: readonly string[];\n};\n\nconst ROLE_FIELD = ValidationField.of(\"role\");\n\n/**\n * A named bundle of {@link Permission}s — `\"cashier\"`, `\"manager\"`, `\"admin\"`.\n * There is **no role hierarchy** in v1: a role grants exactly the permissions\n * it lists (wildcards included). Duplicate permissions are collapsed on\n * construction, preserving first-seen order.\n *\n * A zod-free value object. Build through {@link of}; reconstruct a serialized\n * one through {@link fromProps}.\n */\nclass Role extends ValueObject<RoleProps, RoleProps> {\n // Parsed once, at construction, and cached: the decisor calls `grants` (and\n // `PermissionSet` reads `permissions`) repeatedly, so re-parsing the stored\n // strings on every access would be wasted work.\n private readonly _permissions: readonly Permission[];\n\n private constructor(props: RoleProps) {\n super(props);\n this._permissions = this.value.permissions.map(Permission.of);\n this.finalize();\n }\n\n /**\n * Builds a role from a name and its permissions. Throws\n * {@link InvalidValueException} when the name is blank. Duplicate\n * permissions are deduplicated.\n */\n static of(name: string, permissions: readonly Permission[]): Role {\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new InvalidValueException(\n ROLE_FIELD,\n ValidationCode.BLANK,\n `role name must be a non-empty string, got \"${name}\"`,\n );\n }\n\n const seen = new Set<string>();\n const deduped: string[] = [];\n for (const permission of permissions) {\n const primitive = permission.toPrimitive();\n if (seen.has(primitive)) continue;\n seen.add(primitive);\n deduped.push(primitive);\n }\n\n return new Role({ name, permissions: deduped });\n }\n\n /**\n * Rebuilds a role from its serialized {@link RoleProps} — the inverse of\n * {@link toPrimitive}. Each stored string is re-parsed through\n * {@link Permission.of}, so a malformed payload still fails loudly.\n */\n static fromProps(props: RoleProps): Role {\n return Role.of(props.name, props.permissions.map(Permission.of));\n }\n\n get name(): string {\n return this.value.name;\n }\n\n /** The role's permissions, rehydrated from their stored primitive form. */\n get permissions(): readonly Permission[] {\n return this._permissions;\n }\n\n /**\n * Two roles are equal when they share a name and the same *set* of\n * permissions. Order-insensitive on purpose: a role is a bundle, not a\n * sequence, so `Role.of(\"cashier\", [read, cancel])` equals\n * `Role.of(\"cashier\", [cancel, read])`. Overrides the base\n * {@link ValueObject.equals}, whose stable-stringify keeps array order and\n * would call those two roles different. The stored order (and thus\n * {@link permissions}/{@link toPrimitive}) is still first-seen; only the\n * comparison sorts.\n */\n override equals(other: this): boolean {\n if (this.value.name !== other.value.name) return false;\n const mine = [...this.value.permissions].sort();\n const theirs = [...other.value.permissions].sort();\n return (\n mine.length === theirs.length &&\n mine.every((permission, index) => permission === theirs[index])\n );\n }\n\n /** Whether any permission in this role {@link Permission.implies | implies} `required`. */\n grants(required: Permission): boolean {\n return this._permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n toPrimitive(): RoleProps {\n return {\n name: this.value.name,\n permissions: [...this.value.permissions],\n };\n }\n}\n\nexport { Role, type RoleProps };\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\";\n\n/** The wrapped, already-validated scope string (`\"type:id\"` or `\"*\"`). */\ntype ScopeProps = { readonly raw: string };\n\nconst SCOPE_FIELD = ValidationField.of(\"scope\");\n\n// \"type:id\": a lowercase type segment, a colon, then a non-empty id. The id\n// charset is permissive enough for UUIDs and numeric keys (`school:42`,\n// `tenant:550e8400-e29b-41d4-a716-446655440000`).\nconst SCOPE_PATTERN = /^[a-z][a-z0-9_-]*:[A-Za-z0-9._-]+$/u;\n\n/**\n * Where a grant (or a target resource) lives, aligned with\n * `AuthorizationRequirement.scope` and the policy `PolicyScope`\n * (`tenantId`/`schoolId`): `\"tenant:{id}\"`, `\"school:{id}\"`, or the global\n * `\"*\"`.\n *\n * v1 keeps containment ({@link includes}) flat — the global scope contains\n * everything, and any other scope contains only itself. A real hierarchy\n * (`tenant` ⊇ `school`) is a deliberately deferred extension point.\n */\nclass Scope extends ValueObject<ScopeProps, string> {\n private constructor(props: ScopeProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Validates `\"type:id\"` or the global `\"*\"`, throwing on anything else.\n * `Scope.of(\"*\")` is accepted and is equivalent to {@link Scope.global} —\n * prefer `global()` at call-sites for intent, `of(\"*\")` when rehydrating a\n * stored string.\n */\n static of(raw: string): Scope {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.BLANK,\n `scope must be a non-empty \"type:id\" string or \"*\", got \"${raw}\"`,\n );\n }\n\n if (raw !== \"*\" && !SCOPE_PATTERN.test(raw)) {\n throw new InvalidValueException(\n SCOPE_FIELD,\n ValidationCode.INVALID_FORMAT,\n `scope must be \"type:id\" (e.g. \"school:42\") or the global \"*\", got \"${raw}\"`,\n );\n }\n\n return new Scope({ raw });\n }\n\n /** The global scope `\"*\"`, which {@link includes} every other scope. */\n static global(): Scope {\n return new Scope({ raw: \"*\" });\n }\n\n /** The type segment, or `\"*\"` for the global scope. */\n get type(): string {\n if (this.value.raw === \"*\") return \"*\";\n return this.value.raw.slice(0, this.value.raw.indexOf(\":\"));\n }\n\n /** The id segment, or `undefined` for the global scope. */\n get id(): string | undefined {\n if (this.value.raw === \"*\") return undefined;\n return this.value.raw.slice(this.value.raw.indexOf(\":\") + 1);\n }\n\n /**\n * Whether this scope contains `target`. The global scope contains\n * everything; any other scope contains only an exact match of itself.\n */\n includes(target: Scope): boolean {\n if (this.value.raw === \"*\") return true;\n return this.value.raw === target.value.raw;\n }\n\n toPrimitive(): string {\n return this.value.raw;\n }\n}\n\nexport { Scope, type ScopeProps };\n","import { RequestedBy } from \"../../application/commands/requested-by.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\n\nimport { Role, type RoleProps } from \"./role.js\";\nimport { Scope } from \"./scope.js\";\n\n/**\n * The serializable shape a consumer persists and loads: the actor's identity\n * (`RequestedBy.raw`), the role they hold (as {@link RoleProps}), and the scope\n * the role applies in (the {@link Scope} string).\n */\ntype GrantProps = {\n readonly subject: string;\n readonly role: RoleProps;\n readonly scope: string;\n};\n\n/**\n * A role binding — it ties one actor to one {@link Role} within one\n * {@link Scope}. This is the unit the consumer stores (a `(subject, role,\n * scope)` row) and rehydrates into the pure decisor; the kit itself persists\n * nothing.\n *\n * A zod-free value object built through {@link of} from live\n * `RequestedBy`/`Role`/`Scope` objects; the getters reconstruct those objects\n * back from the frozen primitive form.\n */\nclass Grant extends ValueObject<GrantProps, GrantProps> {\n // The live objects are rehydrated once, at construction, and cached — the\n // decisor reads `role`/`scope` at least once per grant, so parsing lazily on\n // each getter access would re-parse the same frozen primitives repeatedly.\n private readonly _subject: RequestedBy;\n private readonly _role: Role;\n private readonly _scope: Scope;\n\n private constructor(props: GrantProps) {\n super(props);\n this._subject = RequestedBy.parse(this.value.subject);\n this._role = Role.fromProps(this.value.role);\n this._scope = Scope.of(this.value.scope);\n this.finalize();\n }\n\n static of(params: {\n subject: RequestedBy;\n role: Role;\n scope: Scope;\n }): Grant {\n return new Grant({\n subject: params.subject.raw,\n role: params.role.toPrimitive(),\n scope: params.scope.toPrimitive(),\n });\n }\n\n /**\n * Rebuilds a grant from its serialized {@link GrantProps} — the inverse of\n * {@link toPrimitive}, symmetric to {@link Role.fromProps}. Each part is\n * re-validated (subject through {@link RequestedBy.parse}, role through\n * {@link Role.fromProps}, scope through {@link Scope.of}), so a malformed\n * payload still fails loudly.\n */\n static fromProps(props: GrantProps): Grant {\n return new Grant(props);\n }\n\n get subject(): RequestedBy {\n return this._subject;\n }\n\n get role(): Role {\n return this._role;\n }\n\n get scope(): Scope {\n return this._scope;\n }\n\n /** Whether this grant belongs to `actor`. */\n appliesTo(actor: RequestedBy): boolean {\n return this.value.subject === actor.raw;\n }\n\n toPrimitive(): GrantProps {\n return {\n subject: this.value.subject,\n role: {\n name: this.value.role.name,\n permissions: [...this.value.role.permissions],\n },\n scope: this.value.scope,\n };\n }\n}\n\nexport { Grant, type GrantProps };\n","import type { Grant } from \"./grant.js\";\nimport type { Permission } from \"./permission.js\";\n\n/**\n * The flattened, deduplicated permissions an actor effectively holds across a\n * set of {@link Grant}s, together with the role names that contributed them.\n *\n * Pure resolution, isolated from the decisor so the \"roles → permissions\"\n * collapse can be tested on its own. Build through {@link fromGrants}; pre-filter\n * the grants to a single actor first when that matters (the set does not know\n * which actor it describes).\n */\nclass PermissionSet {\n private constructor(\n private readonly permissions: readonly Permission[],\n private readonly _roleNames: readonly string[],\n ) {}\n\n /**\n * Collapses the roles across `grants` into one permission set. Permissions\n * are deduplicated by their primitive form and role names by value, both\n * preserving first-seen order.\n */\n static fromGrants(grants: readonly Grant[]): PermissionSet {\n const permissions: Permission[] = [];\n const seenPermissions = new Set<string>();\n const roleNames: string[] = [];\n const seenRoles = new Set<string>();\n\n for (const grant of grants) {\n const role = grant.role;\n\n if (!seenRoles.has(role.name)) {\n seenRoles.add(role.name);\n roleNames.push(role.name);\n }\n\n for (const permission of role.permissions) {\n const primitive = permission.toPrimitive();\n if (seenPermissions.has(primitive)) continue;\n seenPermissions.add(primitive);\n permissions.push(permission);\n }\n }\n\n return new PermissionSet(permissions, roleNames);\n }\n\n /** Whether any held permission {@link Permission.implies | implies} `required`. */\n has(required: Permission): boolean {\n return this.permissions.some((permission) =>\n permission.implies(required),\n );\n }\n\n /** The effective permissions, deduplicated. */\n toArray(): readonly Permission[] {\n return this.permissions;\n }\n\n /** The names of the roles that contributed to this set, deduplicated. */\n get roleNames(): readonly string[] {\n return this._roleNames;\n }\n}\n\nexport { PermissionSet };\n","import type { RequestedBy } from \"../application/commands/requested-by.js\";\n\nimport type { Grant } from \"./domain/grant.js\";\nimport { PermissionSet } from \"./domain/permission-set.js\";\n\n/**\n * Projects an actor's RBAC facts into flat {@link PolicyContext}-style fields so\n * a declarative gate policy can reason over them (ABAC) — without importing the\n * policy engine or `zod`. It only builds a plain object; the consumer merges it\n * into `seed.fields` of `PolicyService.evaluate(...)`.\n *\n * The grants are filtered to `actor` first, so passing an actor's full grant\n * list (or a mixed one) is safe.\n *\n * **Wildcards are projected literally — they are NOT expanded.** `actor.roles`\n * and `actor.permissions` are the exact stored strings, so a permission such as\n * `\"orders:*\"` or `\"*:*\"` appears verbatim. A gate condition like\n * `{ field: \"actor.permissions\", op: \"in\", value: \"orders:cancel\" }` does a\n * *literal* membership test and will therefore NOT match a holder of\n * `\"orders:*\"`. Open-ended wildcards cannot be enumerated into a finite list, so\n * for wildcard-aware checks either match on the finite `actor.roles` instead, or\n * make the wildcard-aware decision with `RbacAuthorizer` (which knows\n * {@link Permission.implies}) and use the gate only for the extra ABAC\n * conditions.\n *\n * @returns `{ \"actor.id\", \"actor.roles\", \"actor.permissions\" }`, where roles and\n * permissions are plain string arrays (wildcards left literal).\n */\nexport function rbacContextFields(\n actor: RequestedBy,\n grants: readonly Grant[],\n): Record<string, unknown> {\n const set = PermissionSet.fromGrants(\n grants.filter((grant) => grant.appliesTo(actor)),\n );\n\n return {\n \"actor.id\": actor.raw,\n \"actor.roles\": [...set.roleNames],\n \"actor.permissions\": set\n .toArray()\n .map((permission) => permission.toPrimitive()),\n };\n}\n"],"mappings":"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"}
|