@nmnmcc/ability 0.1.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/LICENSE +201 -0
- package/README.md +251 -0
- package/dist/Ability.d.ts +516 -0
- package/dist/Ability.js +211 -0
- package/dist/AbilityExtra.d.ts +79 -0
- package/dist/AbilityExtra.js +195 -0
- package/dist/AbilityRef.d.ts +110 -0
- package/dist/AbilityRef.js +160 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/internal/ability.d.ts +114 -0
- package/dist/internal/ability.js +624 -0
- package/dist/internal/serializable.js +22 -0
- package/package.json +75 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { cloneSerializable } from "./serializable.js";
|
|
2
|
+
import { guard } from "@ucast/mongo2js";
|
|
3
|
+
import * as Data from "effect/Data";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Equal from "effect/Equal";
|
|
6
|
+
import * as Formatter from "effect/Formatter";
|
|
7
|
+
import { dual } from "effect/Function";
|
|
8
|
+
import * as Hash from "effect/Hash";
|
|
9
|
+
import * as Inspectable from "effect/Inspectable";
|
|
10
|
+
import * as Pipeable from "effect/Pipeable";
|
|
11
|
+
import * as Predicate from "effect/Predicate";
|
|
12
|
+
//#region src/internal/ability.ts
|
|
13
|
+
/**
|
|
14
|
+
* @since 0.1.0
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
const TypeId = Symbol.for("@nmnmcc/ability/Ability");
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
const SubjectTypeId = Symbol.for("@nmnmcc/ability/Subject");
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
const RuleTypeId = Symbol.for("@nmnmcc/ability/Rule");
|
|
28
|
+
const anyAction = "manage";
|
|
29
|
+
const anySubject = "all";
|
|
30
|
+
const RuleIndexTypeId = Symbol.for("@nmnmcc/ability/Ability/ruleIndex");
|
|
31
|
+
/**
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
* @category Errors
|
|
34
|
+
*/
|
|
35
|
+
var AuthorizationError = class extends Data.TaggedError("AuthorizationError") {};
|
|
36
|
+
/**
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
* @category Errors
|
|
39
|
+
*/
|
|
40
|
+
var ConditionError = class extends Data.TaggedError("ConditionError") {};
|
|
41
|
+
/**
|
|
42
|
+
* @since 0.1.0
|
|
43
|
+
* @category Errors
|
|
44
|
+
*/
|
|
45
|
+
var PredicateEvaluationError = class extends Data.TaggedError("PredicateEvaluationError") {};
|
|
46
|
+
/**
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
* @category Errors
|
|
49
|
+
*/
|
|
50
|
+
var SerializationError = class extends Data.TaggedError("SerializationError") {};
|
|
51
|
+
/**
|
|
52
|
+
* @since 0.1.0
|
|
53
|
+
* @category Errors
|
|
54
|
+
*/
|
|
55
|
+
var RawRuleError = class extends Data.TaggedError("RawRuleError") {};
|
|
56
|
+
/**
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
* @category Errors
|
|
59
|
+
*/
|
|
60
|
+
var SubjectDetectionError = class extends Data.TaggedError("SubjectDetectionError") {};
|
|
61
|
+
/**
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
* @category Errors
|
|
64
|
+
*/
|
|
65
|
+
var AliasError = class extends Data.TaggedError("AliasError") {};
|
|
66
|
+
/**
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
* @category Errors
|
|
69
|
+
*/
|
|
70
|
+
var QueryGenerationError = class extends Data.TaggedError("QueryGenerationError") {};
|
|
71
|
+
const copyArray = (value) => Object.freeze(Array.from(value));
|
|
72
|
+
const wrapArray = (value) => Array.isArray(value) ? value : [value];
|
|
73
|
+
const normalizeArray = (value) => Array.isArray(value) ? copyArray(value) : value;
|
|
74
|
+
const unique = (values) => Object.freeze(Array.from(new Set(values)));
|
|
75
|
+
const aliasError = (reason, action) => {
|
|
76
|
+
const error = { reason };
|
|
77
|
+
if (action !== void 0) error.action = action;
|
|
78
|
+
return new AliasError(error);
|
|
79
|
+
};
|
|
80
|
+
const normalizeActionAliases = (aliases) => {
|
|
81
|
+
if (aliases === void 0) return Object.freeze({});
|
|
82
|
+
const normalized = {};
|
|
83
|
+
for (const action of Object.keys(aliases)) {
|
|
84
|
+
if (action === anyAction) throw aliasError(`Cannot use "${anyAction}" as an action alias`, action);
|
|
85
|
+
const targets = wrapArray(aliases[action]);
|
|
86
|
+
if (targets.length === 0) throw aliasError("Action alias cannot target an empty action list", action);
|
|
87
|
+
for (const target of targets) if (target === anyAction) throw aliasError(`Cannot create an alias to reserved action "${anyAction}"`, action);
|
|
88
|
+
normalized[action] = copyArray(targets);
|
|
89
|
+
}
|
|
90
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
91
|
+
const visited = /* @__PURE__ */ new Set();
|
|
92
|
+
const visit = (action, path) => {
|
|
93
|
+
if (visited.has(action)) return;
|
|
94
|
+
if (visiting.has(action)) throw aliasError(`Detected cyclic action alias: ${[...path, action].join(" -> ")}`, action);
|
|
95
|
+
const targets = normalized[action];
|
|
96
|
+
if (targets === void 0) return;
|
|
97
|
+
visiting.add(action);
|
|
98
|
+
for (const target of targets) {
|
|
99
|
+
if (target === action) throw aliasError(`Action alias "${action}" cannot reference itself`, action);
|
|
100
|
+
visit(target, [...path, action]);
|
|
101
|
+
}
|
|
102
|
+
visiting.delete(action);
|
|
103
|
+
visited.add(action);
|
|
104
|
+
};
|
|
105
|
+
for (const action of Object.keys(normalized)) visit(action, []);
|
|
106
|
+
return Object.freeze(normalized);
|
|
107
|
+
};
|
|
108
|
+
const expandAction = (aliases, action, output) => {
|
|
109
|
+
if (output.has(action)) return;
|
|
110
|
+
output.add(action);
|
|
111
|
+
const targets = aliases[action];
|
|
112
|
+
if (targets === void 0) return;
|
|
113
|
+
for (const target of targets) expandAction(aliases, target, output);
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
const createAliasResolver = (aliases) => {
|
|
119
|
+
const normalized = normalizeActionAliases(aliases);
|
|
120
|
+
return (action) => {
|
|
121
|
+
const actions = /* @__PURE__ */ new Set();
|
|
122
|
+
for (const value of wrapArray(action)) expandAction(normalized, value, actions);
|
|
123
|
+
return unique(actions);
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
function* ruleIterator(rule) {
|
|
127
|
+
return yield rule;
|
|
128
|
+
}
|
|
129
|
+
const inspectableToString = function() {
|
|
130
|
+
return Formatter.format(this.toJSON(), {
|
|
131
|
+
ignoreToString: true,
|
|
132
|
+
space: 2
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const inspectableNode = function() {
|
|
136
|
+
return this.toJSON();
|
|
137
|
+
};
|
|
138
|
+
const ruleHashInput = (rule) => ({
|
|
139
|
+
_tag: rule._tag,
|
|
140
|
+
action: rule.action,
|
|
141
|
+
subject: rule.subject,
|
|
142
|
+
fields: rule.fields,
|
|
143
|
+
conditions: rule.conditions,
|
|
144
|
+
when: rule.when,
|
|
145
|
+
reason: rule.reason
|
|
146
|
+
});
|
|
147
|
+
const RuleProto = {
|
|
148
|
+
...Pipeable.Prototype,
|
|
149
|
+
[RuleTypeId]: RuleTypeId,
|
|
150
|
+
[Symbol.iterator]() {
|
|
151
|
+
return ruleIterator(this);
|
|
152
|
+
},
|
|
153
|
+
asEffect() {
|
|
154
|
+
return Effect.succeed(this);
|
|
155
|
+
},
|
|
156
|
+
[Equal.symbol](that) {
|
|
157
|
+
return isRule(that) && Equal.equals(ruleHashInput(this), ruleHashInput(that));
|
|
158
|
+
},
|
|
159
|
+
[Hash.symbol]() {
|
|
160
|
+
return Hash.structure(ruleHashInput(this));
|
|
161
|
+
},
|
|
162
|
+
toString: inspectableToString,
|
|
163
|
+
toJSON() {
|
|
164
|
+
const json = {
|
|
165
|
+
_tag: this._tag,
|
|
166
|
+
action: this.action,
|
|
167
|
+
subject: this.subject
|
|
168
|
+
};
|
|
169
|
+
if (this.fields !== void 0) json.fields = this.fields;
|
|
170
|
+
if (this.conditions !== void 0) json.conditions = this.conditions;
|
|
171
|
+
if (this.reason !== void 0) json.reason = this.reason;
|
|
172
|
+
return json;
|
|
173
|
+
},
|
|
174
|
+
[Inspectable.NodeInspectSymbol]: inspectableNode
|
|
175
|
+
};
|
|
176
|
+
const AbilityProto = {
|
|
177
|
+
...Pipeable.Prototype,
|
|
178
|
+
[TypeId]: TypeId,
|
|
179
|
+
[Equal.symbol](that) {
|
|
180
|
+
return isAbility(that) && Equal.equals(this.rules, that.rules) && Equal.equals(this.options, that.options);
|
|
181
|
+
},
|
|
182
|
+
[Hash.symbol]() {
|
|
183
|
+
return Hash.structure({
|
|
184
|
+
rules: this.rules,
|
|
185
|
+
options: this.options
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
toString: inspectableToString,
|
|
189
|
+
toJSON() {
|
|
190
|
+
return {
|
|
191
|
+
_id: "Ability",
|
|
192
|
+
rules: this.rules,
|
|
193
|
+
options: this.options
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
[Inspectable.NodeInspectSymbol]: inspectableNode
|
|
197
|
+
};
|
|
198
|
+
const SubjectProto = {
|
|
199
|
+
...Pipeable.Prototype,
|
|
200
|
+
[SubjectTypeId]: SubjectTypeId,
|
|
201
|
+
[Equal.symbol](that) {
|
|
202
|
+
return isSubject(that) && this.subject === that.subject && Equal.equals(this.value, that.value);
|
|
203
|
+
},
|
|
204
|
+
[Hash.symbol]() {
|
|
205
|
+
return Hash.structure({
|
|
206
|
+
subject: this.subject,
|
|
207
|
+
value: this.value
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
toString: inspectableToString,
|
|
211
|
+
toJSON() {
|
|
212
|
+
return {
|
|
213
|
+
_id: "Subject",
|
|
214
|
+
subject: this.subject,
|
|
215
|
+
value: this.value
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
[Inspectable.NodeInspectSymbol]: inspectableNode
|
|
219
|
+
};
|
|
220
|
+
const freezeOptions = (options) => {
|
|
221
|
+
const copy = {};
|
|
222
|
+
if (options?.detectSubjectType !== void 0) copy.detectSubjectType = options.detectSubjectType;
|
|
223
|
+
if (options?.actionAliases !== void 0) copy.actionAliases = normalizeActionAliases(options.actionAliases);
|
|
224
|
+
return Object.freeze(copy);
|
|
225
|
+
};
|
|
226
|
+
const freezeRule = (rule) => {
|
|
227
|
+
const copy = Object.create(RuleProto);
|
|
228
|
+
copy._tag = rule._tag;
|
|
229
|
+
copy.action = normalizeArray(rule.action);
|
|
230
|
+
copy.subject = normalizeArray(rule.subject);
|
|
231
|
+
if (rule.fields !== void 0) copy.fields = copyArray(rule.fields);
|
|
232
|
+
if (rule.conditions !== void 0) copy.conditions = cloneSerializable(rule.conditions);
|
|
233
|
+
if (rule.when !== void 0) copy.when = rule.when;
|
|
234
|
+
if (rule.reason !== void 0) copy.reason = rule.reason;
|
|
235
|
+
return Object.freeze(copy);
|
|
236
|
+
};
|
|
237
|
+
const addIndexedRule = (index, subjectName, action, indexedRule) => {
|
|
238
|
+
let subjectIndex = index.get(subjectName);
|
|
239
|
+
if (subjectIndex === void 0) {
|
|
240
|
+
subjectIndex = /* @__PURE__ */ new Map();
|
|
241
|
+
index.set(subjectName, subjectIndex);
|
|
242
|
+
}
|
|
243
|
+
let rules = subjectIndex.get(action);
|
|
244
|
+
if (rules === void 0) {
|
|
245
|
+
rules = [];
|
|
246
|
+
subjectIndex.set(action, rules);
|
|
247
|
+
}
|
|
248
|
+
rules.push(indexedRule);
|
|
249
|
+
};
|
|
250
|
+
const freezeRuleIndex = (index) => {
|
|
251
|
+
const subjects = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const [subjectName, actions] of index) {
|
|
253
|
+
const actionIndex = /* @__PURE__ */ new Map();
|
|
254
|
+
for (const [action, rules] of actions) actionIndex.set(action, Object.freeze(rules.slice().sort((a, b) => b.index - a.index)));
|
|
255
|
+
subjects.set(subjectName, actionIndex);
|
|
256
|
+
}
|
|
257
|
+
return Object.freeze({ rulesBySubject: subjects });
|
|
258
|
+
};
|
|
259
|
+
const buildRuleIndex = (rules, aliases) => {
|
|
260
|
+
const resolver = createAliasResolver(aliases ?? {});
|
|
261
|
+
const index = /* @__PURE__ */ new Map();
|
|
262
|
+
let ruleIndex = 0;
|
|
263
|
+
for (const rule of rules) {
|
|
264
|
+
const indexedRule = {
|
|
265
|
+
rule,
|
|
266
|
+
index: ruleIndex
|
|
267
|
+
};
|
|
268
|
+
const actions = resolver(rule.action);
|
|
269
|
+
const subjects = wrapArray(rule.subject);
|
|
270
|
+
for (const subjectName of subjects) for (const action of actions) addIndexedRule(index, subjectName, action, indexedRule);
|
|
271
|
+
ruleIndex = ruleIndex + 1;
|
|
272
|
+
}
|
|
273
|
+
return freezeRuleIndex(index);
|
|
274
|
+
};
|
|
275
|
+
const getRuleIndex = (self) => self[RuleIndexTypeId];
|
|
276
|
+
/**
|
|
277
|
+
* @internal
|
|
278
|
+
*/
|
|
279
|
+
const make = (rules, options) => {
|
|
280
|
+
const self = Object.create(AbilityProto);
|
|
281
|
+
self.rules = Object.freeze(Array.from(rules, freezeRule));
|
|
282
|
+
self.options = freezeOptions(options);
|
|
283
|
+
self[RuleIndexTypeId] = buildRuleIndex(self.rules, self.options.actionAliases);
|
|
284
|
+
return Object.freeze(self);
|
|
285
|
+
};
|
|
286
|
+
const makeRule = (kind, action, subjectName, options) => {
|
|
287
|
+
const rule = Object.create(RuleProto);
|
|
288
|
+
rule._tag = kind;
|
|
289
|
+
rule.action = normalizeArray(action);
|
|
290
|
+
rule.subject = normalizeArray(subjectName);
|
|
291
|
+
if (options?.fields !== void 0) rule.fields = copyArray(wrapArray(options.fields));
|
|
292
|
+
if (options?.conditions !== void 0) rule.conditions = cloneSerializable(options.conditions);
|
|
293
|
+
if (options?.when !== void 0) rule.when = options.when;
|
|
294
|
+
if (options?.reason !== void 0) rule.reason = options.reason;
|
|
295
|
+
return Object.freeze(rule);
|
|
296
|
+
};
|
|
297
|
+
const builder = () => ({
|
|
298
|
+
allow: (action, subjectName, options) => makeRule("Allow", action, subjectName, options),
|
|
299
|
+
deny: (action, subjectName, options) => makeRule("Deny", action, subjectName, options)
|
|
300
|
+
});
|
|
301
|
+
/**
|
|
302
|
+
* @internal
|
|
303
|
+
*/
|
|
304
|
+
const define = (() => (body, options) => {
|
|
305
|
+
const rules = [];
|
|
306
|
+
const iterator = body(builder());
|
|
307
|
+
let state = iterator.next();
|
|
308
|
+
while (!state.done) {
|
|
309
|
+
rules.push(state.value);
|
|
310
|
+
state = iterator.next(state.value);
|
|
311
|
+
}
|
|
312
|
+
return make(rules, options);
|
|
313
|
+
});
|
|
314
|
+
/**
|
|
315
|
+
* @internal
|
|
316
|
+
*/
|
|
317
|
+
const subject = (subjectName, value) => {
|
|
318
|
+
const wrapped = Object.create(SubjectProto);
|
|
319
|
+
wrapped._tag = "Subject";
|
|
320
|
+
wrapped.subject = subjectName;
|
|
321
|
+
wrapped.value = value;
|
|
322
|
+
return Object.freeze(wrapped);
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* @internal
|
|
326
|
+
*/
|
|
327
|
+
const isSubject = (value) => Predicate.hasProperty(value, SubjectTypeId);
|
|
328
|
+
/**
|
|
329
|
+
* @internal
|
|
330
|
+
*/
|
|
331
|
+
const isRule = (value) => Predicate.hasProperty(value, RuleTypeId);
|
|
332
|
+
/**
|
|
333
|
+
* @internal
|
|
334
|
+
*/
|
|
335
|
+
const isAbility = (value) => Predicate.hasProperty(value, TypeId);
|
|
336
|
+
/**
|
|
337
|
+
* @internal
|
|
338
|
+
*/
|
|
339
|
+
const unwrapSubject = (value) => isSubject(value) ? value.value : value;
|
|
340
|
+
const detectFromConstructor = (value) => {
|
|
341
|
+
const constructor = value.constructor;
|
|
342
|
+
if (typeof constructor?.modelName === "string" && constructor.modelName.length > 0) return constructor.modelName;
|
|
343
|
+
if (typeof constructor?.name === "string" && constructor.name.length > 0) return constructor.name;
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* @internal
|
|
347
|
+
*/
|
|
348
|
+
const detectSubjectType = Effect.fnUntraced(function* (self, value) {
|
|
349
|
+
if (isSubject(value)) return value.subject;
|
|
350
|
+
if (self.options.detectSubjectType !== void 0) return yield* Effect.try({
|
|
351
|
+
try: () => self.options.detectSubjectType?.(value),
|
|
352
|
+
catch: (cause) => new SubjectDetectionError({
|
|
353
|
+
reason: "Failed to detect subject type with the configured detector",
|
|
354
|
+
cause
|
|
355
|
+
})
|
|
356
|
+
});
|
|
357
|
+
const detected = detectFromConstructor(value);
|
|
358
|
+
if (detected !== void 0) return detected;
|
|
359
|
+
return yield* Effect.fail(new SubjectDetectionError({ reason: "Unable to detect subject type" }));
|
|
360
|
+
});
|
|
361
|
+
const hasOwnSubject = (request) => "subject" in request && typeof request.subject === "string";
|
|
362
|
+
const hasOwnValue = (request) => "value" in request && typeof request.value === "object" && request.value !== null;
|
|
363
|
+
const toRuntimeRequest = Effect.fnUntraced(function* (self, request) {
|
|
364
|
+
const runtime = {
|
|
365
|
+
action: request.action,
|
|
366
|
+
subject: ""
|
|
367
|
+
};
|
|
368
|
+
if (hasOwnSubject(request)) runtime.subject = request.subject;
|
|
369
|
+
else if (hasOwnValue(request)) runtime.subject = yield* detectSubjectType(self, request.value);
|
|
370
|
+
else return yield* Effect.fail(new SubjectDetectionError({ reason: "A check request must include either a subject or a value" }));
|
|
371
|
+
if (hasOwnValue(request)) runtime.value = unwrapSubject(request.value);
|
|
372
|
+
if (request.field !== void 0) runtime.field = request.field;
|
|
373
|
+
return runtime;
|
|
374
|
+
});
|
|
375
|
+
const toRuntimeSubjectRequest = Effect.fnUntraced(function* (self, request) {
|
|
376
|
+
const runtime = { subject: "" };
|
|
377
|
+
if (hasOwnSubject(request)) runtime.subject = request.subject;
|
|
378
|
+
else if (hasOwnValue(request)) runtime.subject = yield* detectSubjectType(self, request.value);
|
|
379
|
+
else return yield* Effect.fail(new SubjectDetectionError({ reason: "A subject query request must include either a subject or a value" }));
|
|
380
|
+
if (hasOwnValue(request)) runtime.value = unwrapSubject(request.value);
|
|
381
|
+
return runtime;
|
|
382
|
+
});
|
|
383
|
+
const regexpSpecialChars = /[-/\\^$+?.()|[\]{}]/g;
|
|
384
|
+
const regexpAny = /\.?\*+\.?/g;
|
|
385
|
+
const regexpStars = /\*+/;
|
|
386
|
+
const regexpDot = /\./g;
|
|
387
|
+
const detectRegexpPattern = (match, index, source) => {
|
|
388
|
+
const quantifier = source[0] === "*" || match[0] === "." && match[match.length - 1] === "." ? "+" : "*";
|
|
389
|
+
const matcher = match.includes("**") ? "." : "[^.]";
|
|
390
|
+
const pattern = match.replace(regexpDot, "\\$&").replace(regexpStars, matcher + quantifier);
|
|
391
|
+
return index + match.length === source.length ? `(?:${pattern})?` : pattern;
|
|
392
|
+
};
|
|
393
|
+
const escapeRegexp = (match, index, source) => {
|
|
394
|
+
if (match === "." && (source[index - 1] === "*" || source[index + 1] === "*")) return match;
|
|
395
|
+
return `\\${match}`;
|
|
396
|
+
};
|
|
397
|
+
const createFieldPattern = (fields) => {
|
|
398
|
+
const patterns = fields.map((field) => field.replace(regexpSpecialChars, escapeRegexp).replace(regexpAny, detectRegexpPattern));
|
|
399
|
+
const pattern = patterns.length > 1 ? `(?:${patterns.join("|")})` : patterns[0] ?? "";
|
|
400
|
+
return new RegExp(`^${pattern}$`);
|
|
401
|
+
};
|
|
402
|
+
const createFieldMatcher = (fields) => {
|
|
403
|
+
const pattern = fields.some((field) => field.includes("*")) ? createFieldPattern(fields) : void 0;
|
|
404
|
+
return pattern === void 0 ? (field) => fields.includes(field) : (field) => pattern.test(field);
|
|
405
|
+
};
|
|
406
|
+
const matchesField = (rule, request) => {
|
|
407
|
+
if (rule.fields === void 0) return true;
|
|
408
|
+
if (request.field === void 0) return rule._tag === "Allow";
|
|
409
|
+
return createFieldMatcher(rule.fields)(request.field);
|
|
410
|
+
};
|
|
411
|
+
const isEmptyCondition = (conditions) => Object.keys(conditions).length === 0;
|
|
412
|
+
const makeConditionError = (request, cause) => {
|
|
413
|
+
const error = {
|
|
414
|
+
action: request.action,
|
|
415
|
+
subject: request.subject,
|
|
416
|
+
cause
|
|
417
|
+
};
|
|
418
|
+
if (request.field !== void 0) error.field = request.field;
|
|
419
|
+
return new ConditionError(error);
|
|
420
|
+
};
|
|
421
|
+
const makePredicateEvaluationError = (request, cause) => {
|
|
422
|
+
const error = {
|
|
423
|
+
action: request.action,
|
|
424
|
+
subject: request.subject,
|
|
425
|
+
cause
|
|
426
|
+
};
|
|
427
|
+
if (request.field !== void 0) error.field = request.field;
|
|
428
|
+
return new PredicateEvaluationError(error);
|
|
429
|
+
};
|
|
430
|
+
const matchesConditions = Effect.fnUntraced(function* (rule, request) {
|
|
431
|
+
if (rule.conditions === void 0) return true;
|
|
432
|
+
if (request.value === void 0) return rule._tag === "Allow" || isEmptyCondition(rule.conditions);
|
|
433
|
+
return yield* Effect.try({
|
|
434
|
+
try: () => guard(rule.conditions)(request.value),
|
|
435
|
+
catch: (cause) => makeConditionError(request, cause)
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
const matchesPredicate = Effect.fnUntraced(function* (rule, request) {
|
|
439
|
+
if (rule.when === void 0) return true;
|
|
440
|
+
if (request.value === void 0) return false;
|
|
441
|
+
const result = yield* Effect.try({
|
|
442
|
+
try: () => rule.when?.(request.value) ?? true,
|
|
443
|
+
catch: (cause) => makePredicateEvaluationError(request, cause)
|
|
444
|
+
});
|
|
445
|
+
return Effect.isEffect(result) ? yield* result : result;
|
|
446
|
+
});
|
|
447
|
+
const matchesCandidateRule = Effect.fnUntraced(function* (rule, request) {
|
|
448
|
+
if (!matchesField(rule, request)) return false;
|
|
449
|
+
if (!(yield* matchesConditions(rule, request))) return false;
|
|
450
|
+
return yield* matchesPredicate(rule, request);
|
|
451
|
+
});
|
|
452
|
+
const collectIndexedRules = (self, request, order) => {
|
|
453
|
+
const collected = /* @__PURE__ */ new Map();
|
|
454
|
+
const pairs = [[request.subject, request.action]];
|
|
455
|
+
if (request.action !== anyAction) pairs.push([request.subject, anyAction]);
|
|
456
|
+
if (request.subject !== anySubject) pairs.push([anySubject, request.action]);
|
|
457
|
+
if (request.subject !== anySubject && request.action !== anyAction) pairs.push([anySubject, anyAction]);
|
|
458
|
+
for (const [subjectName, action] of pairs) {
|
|
459
|
+
const rules = getRuleIndex(self).rulesBySubject.get(subjectName)?.get(action);
|
|
460
|
+
if (rules !== void 0) for (const indexedRule of rules) collected.set(indexedRule.rule, indexedRule);
|
|
461
|
+
}
|
|
462
|
+
const sorted = Array.from(collected.values()).sort((a, b) => order === "descending" ? b.index - a.index : a.index - b.index);
|
|
463
|
+
return Object.freeze(sorted.map((indexedRule) => indexedRule.rule));
|
|
464
|
+
};
|
|
465
|
+
const possibleRulesForRequest = (self, request) => collectIndexedRules(self, request, "descending");
|
|
466
|
+
const possibleRulesForDefinitionOrder = (self, request) => collectIndexedRules(self, request, "ascending");
|
|
467
|
+
const relevantRuleForEffect = Effect.fnUntraced(function* (self, request) {
|
|
468
|
+
const rules = possibleRulesForRequest(self, request);
|
|
469
|
+
let index = 0;
|
|
470
|
+
while (index < rules.length) {
|
|
471
|
+
const rule = rules[index];
|
|
472
|
+
if (rule !== void 0) {
|
|
473
|
+
if (yield* matchesCandidateRule(rule, request)) return rule;
|
|
474
|
+
}
|
|
475
|
+
index = index + 1;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
const makeAuthorizationError = (request, rule) => {
|
|
479
|
+
const error = {
|
|
480
|
+
action: request.action,
|
|
481
|
+
subject: request.subject
|
|
482
|
+
};
|
|
483
|
+
if (request.field !== void 0) error.field = request.field;
|
|
484
|
+
if (rule?.reason !== void 0) error.reason = rule.reason;
|
|
485
|
+
return new AuthorizationError(error);
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* @internal
|
|
489
|
+
*/
|
|
490
|
+
const check = dual(2, Effect.fnUntraced(function* (self, request) {
|
|
491
|
+
const runtimeRequest = yield* toRuntimeRequest(self, request);
|
|
492
|
+
const rule = yield* relevantRuleForEffect(self, runtimeRequest);
|
|
493
|
+
if (rule?._tag === "Allow") return;
|
|
494
|
+
return yield* Effect.fail(makeAuthorizationError(runtimeRequest, rule));
|
|
495
|
+
}));
|
|
496
|
+
/**
|
|
497
|
+
* @internal
|
|
498
|
+
*/
|
|
499
|
+
const possibleRulesFor = dual(2, Effect.fnUntraced(function* (self, request) {
|
|
500
|
+
return possibleRulesForRequest(self, yield* toRuntimeRequest(self, request));
|
|
501
|
+
}));
|
|
502
|
+
/**
|
|
503
|
+
* @internal
|
|
504
|
+
*/
|
|
505
|
+
const rulesFor = dual(2, Effect.fnUntraced(function* (self, request) {
|
|
506
|
+
const runtimeRequest = yield* toRuntimeRequest(self, request);
|
|
507
|
+
return Object.freeze(possibleRulesForRequest(self, runtimeRequest).filter((rule) => matchesField(rule, runtimeRequest)));
|
|
508
|
+
}));
|
|
509
|
+
/**
|
|
510
|
+
* @internal
|
|
511
|
+
*/
|
|
512
|
+
const relevantRuleFor = dual(2, Effect.fnUntraced(function* (self, request) {
|
|
513
|
+
return yield* relevantRuleForEffect(self, yield* toRuntimeRequest(self, request));
|
|
514
|
+
}));
|
|
515
|
+
/**
|
|
516
|
+
* @internal
|
|
517
|
+
*/
|
|
518
|
+
const actionsFor = dual(2, Effect.fnUntraced(function* (self, request) {
|
|
519
|
+
const runtimeRequest = yield* toRuntimeSubjectRequest(self, request);
|
|
520
|
+
const actions = /* @__PURE__ */ new Set();
|
|
521
|
+
const collect = (subjectName) => {
|
|
522
|
+
const subjectIndex = getRuleIndex(self).rulesBySubject.get(subjectName);
|
|
523
|
+
if (subjectIndex !== void 0) for (const action of subjectIndex.keys()) actions.add(action);
|
|
524
|
+
};
|
|
525
|
+
collect(runtimeRequest.subject);
|
|
526
|
+
if (runtimeRequest.subject !== anySubject) collect(anySubject);
|
|
527
|
+
return unique(actions);
|
|
528
|
+
}));
|
|
529
|
+
const rawRuleError = (reason, ruleIndex, cause) => {
|
|
530
|
+
const error = {
|
|
531
|
+
reason,
|
|
532
|
+
ruleIndex
|
|
533
|
+
};
|
|
534
|
+
if (cause !== void 0) error.cause = cause;
|
|
535
|
+
return new RawRuleError(error);
|
|
536
|
+
};
|
|
537
|
+
const validateArrayValue = (value, name, index) => {
|
|
538
|
+
if (Array.isArray(value) && value.length === 0) return Effect.fail(rawRuleError(`rawRule.${name} cannot be an empty array`, index));
|
|
539
|
+
return Effect.void;
|
|
540
|
+
};
|
|
541
|
+
const validateRawConditions = (conditions, index) => {
|
|
542
|
+
if (conditions === void 0) return Effect.void;
|
|
543
|
+
return Effect.asVoid(Effect.try({
|
|
544
|
+
try: () => guard(conditions),
|
|
545
|
+
catch: (cause) => rawRuleError("rawRule.conditions cannot be compiled", index, cause)
|
|
546
|
+
}));
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* @internal
|
|
550
|
+
*/
|
|
551
|
+
const fromRawRules = Effect.fnUntraced(function* (rules, options) {
|
|
552
|
+
const built = [];
|
|
553
|
+
let index = 0;
|
|
554
|
+
for (const rawRule of rules) {
|
|
555
|
+
yield* validateArrayValue(rawRule.action, "action", index);
|
|
556
|
+
yield* validateArrayValue(rawRule.subject, "subject", index);
|
|
557
|
+
if (rawRule.fields !== void 0) yield* validateArrayValue(rawRule.fields, "fields", index);
|
|
558
|
+
yield* validateRawConditions(rawRule.conditions, index);
|
|
559
|
+
const ruleOptions = {};
|
|
560
|
+
if (rawRule.fields !== void 0) ruleOptions.fields = rawRule.fields;
|
|
561
|
+
if (rawRule.conditions !== void 0) ruleOptions.conditions = rawRule.conditions;
|
|
562
|
+
if (rawRule.reason !== void 0) ruleOptions.reason = rawRule.reason;
|
|
563
|
+
built.push(makeRule(rawRule.inverted === true ? "Deny" : "Allow", rawRule.action, rawRule.subject, ruleOptions));
|
|
564
|
+
index = index + 1;
|
|
565
|
+
}
|
|
566
|
+
return yield* Effect.try({
|
|
567
|
+
try: () => make(built, options),
|
|
568
|
+
catch: (cause) => cause instanceof AliasError ? cause : rawRuleError("Unable to construct Ability from raw rules", index, cause)
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
const ruleToRaw = (rule) => {
|
|
572
|
+
const raw = {
|
|
573
|
+
action: cloneSerializable(rule.action),
|
|
574
|
+
subject: cloneSerializable(rule.subject)
|
|
575
|
+
};
|
|
576
|
+
if (rule.fields !== void 0) raw.fields = cloneSerializable(rule.fields);
|
|
577
|
+
if (rule.conditions !== void 0) raw.conditions = cloneSerializable(rule.conditions);
|
|
578
|
+
if (rule._tag === "Deny") raw.inverted = true;
|
|
579
|
+
if (rule.reason !== void 0) raw.reason = rule.reason;
|
|
580
|
+
return Object.freeze(raw);
|
|
581
|
+
};
|
|
582
|
+
/**
|
|
583
|
+
* @internal
|
|
584
|
+
*/
|
|
585
|
+
const toRawRules = Effect.fnUntraced(function* (self, options) {
|
|
586
|
+
const strict = options?.strict !== false;
|
|
587
|
+
const rawRules = [];
|
|
588
|
+
let index = 0;
|
|
589
|
+
for (const rule of self.rules) {
|
|
590
|
+
if (strict && rule.when !== void 0) return yield* Effect.fail(new SerializationError({
|
|
591
|
+
reason: "Cannot serialize rules with function predicates in strict mode",
|
|
592
|
+
ruleIndex: index
|
|
593
|
+
}));
|
|
594
|
+
rawRules.push(ruleToRaw(rule));
|
|
595
|
+
index = index + 1;
|
|
596
|
+
}
|
|
597
|
+
return Object.freeze(rawRules);
|
|
598
|
+
});
|
|
599
|
+
const fieldsForRule = (rule, options) => rule.fields ?? options.fieldsFrom(rule);
|
|
600
|
+
/**
|
|
601
|
+
* @internal
|
|
602
|
+
*/
|
|
603
|
+
const permittedFields = dual(3, Effect.fnUntraced(function* (self, request, options) {
|
|
604
|
+
const runtimeRequest = yield* toRuntimeRequest(self, request);
|
|
605
|
+
const rules = possibleRulesForDefinitionOrder(self, runtimeRequest);
|
|
606
|
+
const fields = /* @__PURE__ */ new Set();
|
|
607
|
+
let index = 0;
|
|
608
|
+
while (index < rules.length) {
|
|
609
|
+
const rule = rules[index];
|
|
610
|
+
if (rule !== void 0) {
|
|
611
|
+
if (yield* matchesConditions(rule, runtimeRequest)) {
|
|
612
|
+
if (yield* matchesPredicate(rule, runtimeRequest)) {
|
|
613
|
+
const ruleFields = fieldsForRule(rule, options);
|
|
614
|
+
for (const field of ruleFields) if (rule._tag === "Allow") fields.add(field);
|
|
615
|
+
else fields.delete(field);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
index = index + 1;
|
|
620
|
+
}
|
|
621
|
+
return Object.freeze(Array.from(fields));
|
|
622
|
+
}));
|
|
623
|
+
//#endregion
|
|
624
|
+
export { AliasError, AuthorizationError, ConditionError, PredicateEvaluationError, QueryGenerationError, RawRuleError, RuleTypeId, SerializationError, SubjectDetectionError, SubjectTypeId, TypeId, actionsFor, check, createAliasResolver, define, detectSubjectType, fromRawRules, isAbility, isRule, isSubject, make, permittedFields, possibleRulesFor, relevantRuleFor, rulesFor, subject, toRawRules, unwrapSubject };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/internal/serializable.ts
|
|
2
|
+
/**
|
|
3
|
+
* @since 0.1.0
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
const cloneSerializable = (value) => {
|
|
13
|
+
if (Array.isArray(value)) return Object.freeze(value.map(cloneSerializable));
|
|
14
|
+
if (isPlainObject(value)) {
|
|
15
|
+
const output = {};
|
|
16
|
+
for (const key of Object.keys(value)) output[key] = cloneSerializable(value[key]);
|
|
17
|
+
return Object.freeze(output);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { cloneSerializable, isPlainObject };
|