@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.
@@ -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 };