@cqrs-ddd/pipeline-casl 0.5.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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +38 -0
- package/dist/casl.behavior.d.ts +27 -0
- package/dist/casl.behavior.d.ts.map +1 -0
- package/dist/casl.behavior.js +57 -0
- package/dist/casl.behavior.js.map +1 -0
- package/dist/constants/tokens.d.ts +29 -0
- package/dist/constants/tokens.d.ts.map +1 -0
- package/dist/constants/tokens.js +28 -0
- package/dist/constants/tokens.js.map +1 -0
- package/dist/errors/missing-ability.error.d.ts +14 -0
- package/dist/errors/missing-ability.error.d.ts.map +1 -0
- package/dist/errors/missing-ability.error.js +19 -0
- package/dist/errors/missing-ability.error.js.map +1 -0
- package/dist/errors/unauthorized-action.exception.d.ts +19 -0
- package/dist/errors/unauthorized-action.exception.d.ts.map +1 -0
- package/dist/errors/unauthorized-action.exception.js +26 -0
- package/dist/errors/unauthorized-action.exception.js.map +1 -0
- package/dist/helpers/ability-digest.d.ts +47 -0
- package/dist/helpers/ability-digest.d.ts.map +1 -0
- package/dist/helpers/ability-digest.js +64 -0
- package/dist/helpers/ability-digest.js.map +1 -0
- package/dist/helpers/ability.d.ts +52 -0
- package/dist/helpers/ability.d.ts.map +1 -0
- package/dist/helpers/ability.js +148 -0
- package/dist/helpers/ability.js.map +1 -0
- package/dist/helpers/authorizer.d.ts +73 -0
- package/dist/helpers/authorizer.d.ts.map +1 -0
- package/dist/helpers/authorizer.js +173 -0
- package/dist/helpers/authorizer.js.map +1 -0
- package/dist/helpers/capability.d.ts +80 -0
- package/dist/helpers/capability.d.ts.map +1 -0
- package/dist/helpers/capability.js +287 -0
- package/dist/helpers/capability.js.map +1 -0
- package/dist/helpers/projection.d.ts +11 -0
- package/dist/helpers/projection.d.ts.map +1 -0
- package/dist/helpers/projection.js +140 -0
- package/dist/helpers/projection.js.map +1 -0
- package/dist/helpers/requires.d.ts +27 -0
- package/dist/helpers/requires.d.ts.map +1 -0
- package/dist/helpers/requires.js +28 -0
- package/dist/helpers/requires.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +25 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/permission-source.interface.d.ts +20 -0
- package/dist/interfaces/permission-source.interface.d.ts.map +1 -0
- package/dist/interfaces/permission-source.interface.js +3 -0
- package/dist/interfaces/permission-source.interface.js.map +1 -0
- package/dist/types/casl.types.d.ts +25 -0
- package/dist/types/casl.types.d.ts.map +1 -0
- package/dist/types/casl.types.js +3 -0
- package/dist/types/casl.types.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/helpers/projection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAmBzD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE,GAC1C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE,CAsIrC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
const OMIT = Symbol('unreadable-field');
|
|
3
|
+
/** Defines an own data property, so a `__proto__` key stays a plain field. */
|
|
4
|
+
function defineField(target, key, value) {
|
|
5
|
+
Object.defineProperty(target, key, {
|
|
6
|
+
value,
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Projects a snapshot to the fields `ability` permits for `action`. Conditions
|
|
14
|
+
* are evaluated against the full subject. A granted parent path authorizes its
|
|
15
|
+
* descendants unless a descendant is explicitly denied; this differs from
|
|
16
|
+
* `ability.can(action, subject, 'parent.child')`, which needs
|
|
17
|
+
* `parent.*`/`parent.**`. Denied array elements become `null`. Cyclic input
|
|
18
|
+
* and more than 1024 path aliases throw.
|
|
19
|
+
*/
|
|
20
|
+
export function projectPermittedFields(ability, action, typedSubject, record) {
|
|
21
|
+
const ancestors = new WeakSet();
|
|
22
|
+
// Evaluate full field paths using CASL. Conditions use the complete subject.
|
|
23
|
+
function ruleFor(paths, inherited) {
|
|
24
|
+
let rule = null;
|
|
25
|
+
for (const field of paths) {
|
|
26
|
+
if (!field)
|
|
27
|
+
continue;
|
|
28
|
+
const candidate = ability.relevantRuleFor(action, typedSubject, field);
|
|
29
|
+
if (candidate) {
|
|
30
|
+
if (candidate.inverted) {
|
|
31
|
+
if (!rule?.inverted || candidate.priority < rule.priority) {
|
|
32
|
+
rule = candidate;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (!rule ||
|
|
36
|
+
(!rule.inverted && candidate.priority < rule.priority)) {
|
|
37
|
+
rule = candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// A direct leaf denial always takes precedence over an inherited grant.
|
|
42
|
+
if (rule?.inverted) {
|
|
43
|
+
return rule;
|
|
44
|
+
}
|
|
45
|
+
// Parent denials can mask containers unless explicitly overridden.
|
|
46
|
+
if (inherited?.inverted && (!rule || inherited.priority < rule.priority)) {
|
|
47
|
+
return inherited;
|
|
48
|
+
}
|
|
49
|
+
if (rule) {
|
|
50
|
+
return rule;
|
|
51
|
+
}
|
|
52
|
+
// Inherit parent grant for descendants that are not explicitly denied.
|
|
53
|
+
if (inherited && !inherited.inverted) {
|
|
54
|
+
return inherited;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function project(value, paths, inherited) {
|
|
59
|
+
const rule = ruleFor(paths, inherited);
|
|
60
|
+
const allowed = !!rule && !rule.inverted;
|
|
61
|
+
if (value === null || typeof value !== 'object' || value instanceof Date) {
|
|
62
|
+
return allowed ? value : OMIT;
|
|
63
|
+
}
|
|
64
|
+
if (ancestors.has(value)) {
|
|
65
|
+
throw new TypeError('Cannot project a cyclic authorization snapshot.');
|
|
66
|
+
}
|
|
67
|
+
ancestors.add(value);
|
|
68
|
+
try {
|
|
69
|
+
const toJSON = value.toJSON;
|
|
70
|
+
if (typeof toJSON === 'function') {
|
|
71
|
+
return project(toJSON.call(value), paths, rule);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
if (value.length === 0)
|
|
75
|
+
return allowed ? [] : OMIT;
|
|
76
|
+
let visible = false;
|
|
77
|
+
const result = value.map((item, index) => {
|
|
78
|
+
// Named paths apply to every element; indexed paths can additionally
|
|
79
|
+
// restrict individual elements. Keep positions stable after masking.
|
|
80
|
+
const elementPaths = [
|
|
81
|
+
...new Set([
|
|
82
|
+
...paths,
|
|
83
|
+
...paths.map((path) => path ? `${path}.${index}` : String(index)),
|
|
84
|
+
]),
|
|
85
|
+
];
|
|
86
|
+
// Nested arrays have both named and indexed aliases. Bound their
|
|
87
|
+
// combinations so pathological input fails closed instead of exploding.
|
|
88
|
+
if (elementPaths.length > 1024) {
|
|
89
|
+
throw new TypeError('Authorization snapshot has too many array path aliases.');
|
|
90
|
+
}
|
|
91
|
+
const projected = project(item, elementPaths, rule);
|
|
92
|
+
if (projected !== OMIT)
|
|
93
|
+
visible = true;
|
|
94
|
+
return projected === OMIT ? null : projected;
|
|
95
|
+
});
|
|
96
|
+
return visible ? result : OMIT;
|
|
97
|
+
}
|
|
98
|
+
const entries = Object.entries(value);
|
|
99
|
+
if (entries.length === 0)
|
|
100
|
+
return allowed ? {} : OMIT;
|
|
101
|
+
const result = {};
|
|
102
|
+
for (const [key, child] of entries) {
|
|
103
|
+
const projected = project(child, paths.map((path) => (path ? `${path}.${key}` : key)), rule);
|
|
104
|
+
if (projected !== OMIT)
|
|
105
|
+
defineField(result, key, projected);
|
|
106
|
+
}
|
|
107
|
+
return Object.keys(result).length > 0 ? result : OMIT;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
ancestors.delete(value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(record)) {
|
|
114
|
+
ancestors.add(record);
|
|
115
|
+
try {
|
|
116
|
+
return record.map((item, index) => {
|
|
117
|
+
// The empty path gives named fields the same meaning for every item.
|
|
118
|
+
const projected = project(item, ['', String(index)], null);
|
|
119
|
+
return projected === OMIT ? null : projected;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
ancestors.delete(record);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const result = {};
|
|
127
|
+
ancestors.add(record);
|
|
128
|
+
try {
|
|
129
|
+
for (const [key, value] of Object.entries(record)) {
|
|
130
|
+
const projected = project(value, [key], null);
|
|
131
|
+
if (projected !== OMIT)
|
|
132
|
+
defineField(result, key, projected);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
ancestors.delete(record);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.js","sourceRoot":"","sources":["../../src/helpers/projection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PipelineBehaviorTuple } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import { CaslBehavior, type CaslBehaviorOptions } from '../casl.behavior.js';
|
|
3
|
+
import type { AbilityRequirement } from '../types/casl.types.js';
|
|
4
|
+
/**
|
|
5
|
+
* `[CaslBehavior, { rules }]` for `pipeline.wrap()`; every requirement must pass.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* class Users {
|
|
10
|
+
* @pipeline.wrap({ kind: 'query' }, requires({ action: 'read', subject: 'User' }))
|
|
11
|
+
* async find(query: GetUserQuery) {}
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* class Posts {
|
|
15
|
+
* @pipeline.wrap(
|
|
16
|
+
* { kind: 'command' },
|
|
17
|
+
* requires(
|
|
18
|
+
* { action: 'update', subject: 'Post', field: 'title' },
|
|
19
|
+
* { action: 'update', subject: 'Post', field: 'body' },
|
|
20
|
+
* ),
|
|
21
|
+
* )
|
|
22
|
+
* async edit(command: EditPostCommand) {}
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function requires(...rules: [AbilityRequirement, ...AbilityRequirement[]]): PipelineBehaviorTuple<CaslBehavior, CaslBehaviorOptions>;
|
|
27
|
+
//# sourceMappingURL=requires.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requires.d.ts","sourceRoot":"","sources":["../../src/helpers/requires.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CACtB,GAAG,KAAK,EAAE,CAAC,kBAAkB,EAAE,GAAG,kBAAkB,EAAE,CAAC,GACtD,qBAAqB,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAE1D"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { CaslBehavior } from '../casl.behavior.js';
|
|
3
|
+
/**
|
|
4
|
+
* `[CaslBehavior, { rules }]` for `pipeline.wrap()`; every requirement must pass.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* class Users {
|
|
9
|
+
* @pipeline.wrap({ kind: 'query' }, requires({ action: 'read', subject: 'User' }))
|
|
10
|
+
* async find(query: GetUserQuery) {}
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* class Posts {
|
|
14
|
+
* @pipeline.wrap(
|
|
15
|
+
* { kind: 'command' },
|
|
16
|
+
* requires(
|
|
17
|
+
* { action: 'update', subject: 'Post', field: 'title' },
|
|
18
|
+
* { action: 'update', subject: 'Post', field: 'body' },
|
|
19
|
+
* ),
|
|
20
|
+
* )
|
|
21
|
+
* async edit(command: EditPostCommand) {}
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function requires(...rules) {
|
|
26
|
+
return [CaslBehavior, { rules }];
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=requires.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requires.js","sourceRoot":"","sources":["../../src/helpers/requires.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAGtE,OAAO,EAAE,YAAY,EAA4B,MAAM,qBAAqB,CAAC;AAG7E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,QAAQ,CACtB,GAAG,KAAoD;IAEvD,OAAO,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AACnC,CAAC"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UnauthorizedActionException } from './errors/unauthorized-action.exception.js';
|
|
2
|
+
/** An HTTP answer for an error: status, JSON body and headers. */
|
|
3
|
+
export interface HttpResponse {
|
|
4
|
+
status: number;
|
|
5
|
+
body: Record<string, unknown>;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The HTTP answer for an {@link UnauthorizedActionException}: `403 Forbidden`, naming the
|
|
10
|
+
* denied action and subject.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const { status, body } = toHttpResponse(error);
|
|
15
|
+
* res.status(status).json(body);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function toHttpResponse(error: UnauthorizedActionException): HttpResponse;
|
|
19
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC;AAE7F,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,2BAA2B,GACjC,YAAY,CAYd"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/**
|
|
3
|
+
* The HTTP answer for an {@link UnauthorizedActionException}: `403 Forbidden`, naming the
|
|
4
|
+
* denied action and subject.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const { status, body } = toHttpResponse(error);
|
|
9
|
+
* res.status(status).json(body);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export function toHttpResponse(error) {
|
|
13
|
+
return {
|
|
14
|
+
status: 403,
|
|
15
|
+
body: {
|
|
16
|
+
statusCode: 403,
|
|
17
|
+
error: 'Forbidden',
|
|
18
|
+
message: error.message,
|
|
19
|
+
action: error.action,
|
|
20
|
+
subject: error.subject,
|
|
21
|
+
},
|
|
22
|
+
headers: {},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAWtE;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAkC;IAElC,OAAO;QACL,MAAM,EAAE,GAAG;QACX,IAAI,EAAE;YACJ,UAAU,EAAE,GAAG;YACf,KAAK,EAAE,WAAW;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB;QACD,OAAO,EAAE,EAAE;KACZ,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CASL authorization for wrapped functions and methods: the behavior, its permission source contract and `requires()`. The HTTP mapping of its errors is the `@cqrs-ddd/pipeline-casl/http` entry point.
|
|
3
|
+
*
|
|
4
|
+
* @module main
|
|
5
|
+
*/
|
|
6
|
+
export { CASL_BEHAVIOR_ID, CaslBehavior, type CaslBehaviorOptions, } from './casl.behavior.js';
|
|
7
|
+
export { CASL_ABILITY_KEY, CASL_ACTIONS, CASL_PRINCIPAL_KEY, CASL_SUBJECTS, type CaslAction, type CaslSubject, } from './constants/tokens.js';
|
|
8
|
+
export { MissingAbilityError } from './errors/missing-ability.error.js';
|
|
9
|
+
export { type UnauthorizedActionDetails, UnauthorizedActionException, } from './errors/unauthorized-action.exception.js';
|
|
10
|
+
export { buildAbility, interpolateConditions } from './helpers/ability.js';
|
|
11
|
+
export { abilityDigest, requireAbilityDigest, } from './helpers/ability-digest.js';
|
|
12
|
+
export { CaslAuthorizer, getCaslAbility, getCaslPrincipal, hasEntityConditions, } from './helpers/authorizer.js';
|
|
13
|
+
export { normalizeCapability, parseCapabilityString, serializeCapability, } from './helpers/capability.js';
|
|
14
|
+
export { requires } from './helpers/requires.js';
|
|
15
|
+
export type { CaslAuthorizationInput, CaslPrincipal, ICaslPermissionSource, } from './interfaces/permission-source.interface.js';
|
|
16
|
+
export type { AbilityRequirement, AppAbility, AppRawRule, Capability, CapabilityString, Projected, } from './types/casl.types.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EACL,KAAK,yBAAyB,EAC9B,2BAA2B,GAC5B,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EACL,aAAa,EACb,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,YAAY,EACV,sBAAsB,EACtB,aAAa,EACb,qBAAqB,GACtB,MAAM,6CAA6C,CAAC;AACrD,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,SAAS,GACV,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/**
|
|
3
|
+
* CASL authorization for wrapped functions and methods: the behavior, its permission source contract and `requires()`. The HTTP mapping of its errors is the `@cqrs-ddd/pipeline-casl/http` entry point.
|
|
4
|
+
*
|
|
5
|
+
* @module main
|
|
6
|
+
*/
|
|
7
|
+
export { CASL_BEHAVIOR_ID, CaslBehavior, } from './casl.behavior.js';
|
|
8
|
+
export { CASL_ABILITY_KEY, CASL_ACTIONS, CASL_PRINCIPAL_KEY, CASL_SUBJECTS, } from './constants/tokens.js';
|
|
9
|
+
export { MissingAbilityError } from './errors/missing-ability.error.js';
|
|
10
|
+
export { UnauthorizedActionException, } from './errors/unauthorized-action.exception.js';
|
|
11
|
+
export { buildAbility, interpolateConditions } from './helpers/ability.js';
|
|
12
|
+
export { abilityDigest, requireAbilityDigest, } from './helpers/ability-digest.js';
|
|
13
|
+
export { CaslAuthorizer, getCaslAbility, getCaslPrincipal, hasEntityConditions, } from './helpers/authorizer.js';
|
|
14
|
+
export { normalizeCapability, parseCapabilityString, serializeCapability, } from './helpers/capability.js';
|
|
15
|
+
export { requires } from './helpers/requires.js';
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;;;GAIG;AAEH,OAAO,EACL,gBAAgB,EAChB,YAAY,GAEb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,aAAa,GAGd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAEL,2BAA2B,GAC5B,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EACL,aAAa,EACb,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IPipelineContext } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { Capability } from '../types/casl.types.js';
|
|
3
|
+
/** The authenticated caller. Attributes are available to `${user.<path>}` placeholders. */
|
|
4
|
+
export interface CaslPrincipal {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly [attribute: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface CaslAuthorizationInput {
|
|
9
|
+
readonly principal: CaslPrincipal;
|
|
10
|
+
/** The caller's rules in any order; direct rules are applied before inverted ones. */
|
|
11
|
+
readonly rules: readonly Capability[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Application port: resolves the caller and their rules for one pipeline
|
|
15
|
+
* execution. Return `null` when the caller is unauthenticated.
|
|
16
|
+
*/
|
|
17
|
+
export interface ICaslPermissionSource {
|
|
18
|
+
load(context: IPipelineContext): Promise<CaslAuthorizationInput | null>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=permission-source.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-source.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/permission-source.interface.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,sFAAsF;IACtF,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;CACzE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-source.interface.js","sourceRoot":"","sources":["../../src/interfaces/permission-source.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MongoAbility, RawRuleOf } from '@casl/ability';
|
|
2
|
+
/** One permission rule. `conditions` may contain `${user.<path>}` placeholders. */
|
|
3
|
+
export interface Capability {
|
|
4
|
+
subject: string;
|
|
5
|
+
action: string;
|
|
6
|
+
conditions?: Record<string, unknown>;
|
|
7
|
+
fields?: string[];
|
|
8
|
+
inverted?: boolean;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Compact form: `[!]subject|action[|conditions[|fields[|reason]]]`. */
|
|
12
|
+
export type CapabilityString = string;
|
|
13
|
+
/** A type-level permission requirement checked by `CaslBehavior`. */
|
|
14
|
+
export interface AbilityRequirement {
|
|
15
|
+
action: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
field?: string;
|
|
18
|
+
}
|
|
19
|
+
export type AppAbility = MongoAbility<[string, string]>;
|
|
20
|
+
export type AppRawRule = RawRuleOf<AppAbility>;
|
|
21
|
+
/** A candidate after field projection: every property may be absent; denied array items become null. */
|
|
22
|
+
export type Projected<T> = T extends Date ? T : T extends readonly (infer U)[] ? (Projected<U> | null)[] : T extends object ? {
|
|
23
|
+
[K in keyof T]?: Projected<T[K]>;
|
|
24
|
+
} : T;
|
|
25
|
+
//# sourceMappingURL=casl.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"casl.types.d.ts","sourceRoot":"","sources":["../../src/types/casl.types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE7D,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAE/C,wGAAwG;AACxG,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,GACrC,CAAC,GACD,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GACvB,CAAC,SAAS,MAAM,GACd;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GACpC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"casl.types.js","sourceRoot":"","sources":["../../src/types/casl.types.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cqrs-ddd/pipeline-casl",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "CASL authorization for @cqrs-ddd/pipeline: type-level checks before the handler, entity and field checks after it",
|
|
5
|
+
"author": "Aristotelis",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12.0"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/aristoteliss/ddd-cqrs.git",
|
|
14
|
+
"directory": "packages/pipeline-casl"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-casl/",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/aristoteliss/ddd-cqrs/issues"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./http": {
|
|
26
|
+
"types": "./dist/http.d.ts",
|
|
27
|
+
"default": "./dist/http.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"main": "dist/index.js",
|
|
32
|
+
"types": "dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"COMMERCIAL_LICENSE.txt"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@cqrs-ddd/safe-stringify": "^0.5.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@casl/ability": "^7.0.0",
|
|
46
|
+
"@cqrs-ddd/pipeline": "^0.5.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@casl/ability": "^7.0.1",
|
|
50
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
51
|
+
"vitest": "^5.0.3",
|
|
52
|
+
"@cqrs-ddd/pipeline": "0.5.0"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc -p tsconfig.build.json",
|
|
57
|
+
"build:watch": "tsc -p tsconfig.build.json --watch",
|
|
58
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
59
|
+
"rebuild": "pnpm run clean && pnpm run build",
|
|
60
|
+
"lint": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest"
|
|
63
|
+
}
|
|
64
|
+
}
|