@via-profit/ability 3.7.1 → 3.7.4

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/dist/index.js CHANGED
@@ -1,3399 +1 @@
1
- function brand$4(code) {
2
- return code;
3
- }
4
- const AbilityCompare = {
5
- or: brand$4('or'),
6
- and: brand$4('and'),
7
- };
8
-
9
- class AbilityError extends Error {
10
- constructor(message, options) {
11
- super(message, options);
12
- this.name = 'AbilityError';
13
- if (Error.captureStackTrace) {
14
- Error.captureStackTrace(this, this.constructor);
15
- }
16
- }
17
- }
18
- class AbilityParserError extends Error {
19
- constructor(message, options) {
20
- super(message, options);
21
- this.name = 'AbilityParserError';
22
- if (Error.captureStackTrace) {
23
- Error.captureStackTrace(this, this.constructor);
24
- }
25
- }
26
- }
27
-
28
- function brand$3(code) {
29
- return code;
30
- }
31
- const AbilityCondition = {
32
- equals: brand$3('='),
33
- defined: brand$3('defined'),
34
- not_defined: brand$3('not_defined'),
35
- not_equals: brand$3('<>'),
36
- greater_than: brand$3('>'),
37
- less_than: brand$3('<'),
38
- less_or_equal: brand$3('<='),
39
- greater_or_equal: brand$3('>='),
40
- in: brand$3('in'),
41
- not_in: brand$3('not in'),
42
- contains: brand$3('contains'),
43
- not_contains: brand$3('not contains'),
44
- length_greater_than: brand$3('length greater than'),
45
- length_less_than: brand$3('length less than'),
46
- length_equals: brand$3('length equals'),
47
- always: brand$3('always'),
48
- never: brand$3('never'),
49
- };
50
- function fromLiteral(literal) {
51
- const map = {
52
- equals: AbilityCondition.equals,
53
- not_equals: AbilityCondition.not_equals,
54
- greater_than: AbilityCondition.greater_than,
55
- less_than: AbilityCondition.less_than,
56
- less_or_equal: AbilityCondition.less_or_equal,
57
- greater_or_equal: AbilityCondition.greater_or_equal,
58
- in: AbilityCondition.in,
59
- not_in: AbilityCondition.not_in,
60
- contains: AbilityCondition.contains,
61
- not_contains: AbilityCondition.not_contains,
62
- length_greater_than: AbilityCondition.length_greater_than,
63
- length_less_than: AbilityCondition.length_less_than,
64
- length_equals: AbilityCondition.length_equals,
65
- always: AbilityCondition.always,
66
- never: AbilityCondition.never,
67
- defined: AbilityCondition.defined,
68
- not_defined: AbilityCondition.not_defined,
69
- };
70
- const value = map[literal];
71
- if (!value) {
72
- const expected = Object.keys(map).join(', ');
73
- throw new AbilityParserError(`Literal "${literal}" does not found in AbilityCondition. Expected one of: ${expected}`);
74
- }
75
- return value;
76
- }
77
- function toLiteral(cond) {
78
- switch (cond) {
79
- case AbilityCondition.equals:
80
- return 'equals';
81
- case AbilityCondition.not_equals:
82
- return 'not_equals';
83
- case AbilityCondition.greater_than:
84
- return 'greater_than';
85
- case AbilityCondition.less_than:
86
- return 'less_than';
87
- case AbilityCondition.less_or_equal:
88
- return 'less_or_equal';
89
- case AbilityCondition.greater_or_equal:
90
- return 'greater_or_equal';
91
- case AbilityCondition.in:
92
- return 'in';
93
- case AbilityCondition.not_in:
94
- return 'not_in';
95
- case AbilityCondition.contains:
96
- return 'contains';
97
- case AbilityCondition.not_contains:
98
- return 'not_contains';
99
- case AbilityCondition.length_greater_than:
100
- return 'length_greater_than';
101
- case AbilityCondition.length_less_than:
102
- return 'length_less_than';
103
- case AbilityCondition.length_equals:
104
- return 'length_equals';
105
- case AbilityCondition.always:
106
- return 'always';
107
- case AbilityCondition.never:
108
- return 'never';
109
- case AbilityCondition.defined:
110
- return 'defined';
111
- case AbilityCondition.not_defined:
112
- return 'not_defined';
113
- default:
114
- return 'never';
115
- }
116
- }
117
- function isConditionEqual(a, b) {
118
- return a !== null && b !== null && a === b;
119
- }
120
- function isConditionNotEqual(a, b) {
121
- return !isConditionEqual(a, b);
122
- }
123
-
124
- function brand$2(code) {
125
- return code;
126
- }
127
- const AbilityMatch = {
128
- pending: brand$2('pending'),
129
- match: brand$2('match'),
130
- mismatch: brand$2('mismatch'),
131
- exceptMismatch: brand$2('except-mismatch'),
132
- disabled: brand$2('disabled'),
133
- };
134
-
135
- class AbilityExplain {
136
- type;
137
- children;
138
- name;
139
- match;
140
- debugInfo;
141
- constructor(config, children = []) {
142
- this.type = config.type;
143
- this.children = children;
144
- this.name = config.name;
145
- this.match = config.match;
146
- this.debugInfo = config.debugInfo;
147
- }
148
- toString(indentPrefix = '', isLast = true) {
149
- const isMatch = this.match === AbilityMatch.match;
150
- const isMismatch = this.match === AbilityMatch.mismatch;
151
- const isPending = this.match === AbilityMatch.pending;
152
- // const isDisabled = this.match === AbilityMatch.disabled;
153
- const mark = isMatch
154
- ? `<match ✓>`
155
- : isMismatch
156
- ? `<mismatch ✗>`
157
- : isPending
158
- ? `<pending …>`
159
- : `<disabled ⊘>`;
160
- let label;
161
- switch (this.type) {
162
- case 'policy':
163
- label = `POLICY`;
164
- break;
165
- case 'ruleSet':
166
- label = `RULESET`;
167
- break;
168
- default:
169
- label = `RULE`;
170
- }
171
- const branch = indentPrefix.length === 0
172
- ? ''
173
- : isLast
174
- ? `└─ `
175
- : `├─ `;
176
- let out = `${indentPrefix}${branch}${label} ${this.name} — ${mark}`;
177
- if (this.debugInfo)
178
- out += ` (${this.debugInfo})`;
179
- const nextIndent = indentPrefix + (isLast ? ' ' : `│ `);
180
- this.children.forEach((child, idx) => {
181
- out += '\n' + child.toString(nextIndent, idx === this.children.length - 1);
182
- });
183
- return out;
184
- }
185
- }
186
- class AbilityExplainRule extends AbilityExplain {
187
- constructor(rule) {
188
- super({
189
- type: 'rule',
190
- match: rule.state,
191
- name: rule.name,
192
- debugInfo: `${rule.subject} ${rule.condition} ${JSON.stringify(rule.resource)}`,
193
- });
194
- }
195
- }
196
- class AbilityExplainRuleSet extends AbilityExplain {
197
- constructor(ruleSet) {
198
- const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
199
- super({
200
- type: 'ruleSet',
201
- match: ruleSet.state,
202
- name: ruleSet.name,
203
- }, children);
204
- }
205
- }
206
- class AbilityExplainPolicy extends AbilityExplain {
207
- constructor(policy) {
208
- const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
209
- super({
210
- type: 'policy',
211
- name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
212
- match: policy.matchState,
213
- }, children);
214
- }
215
- }
216
-
217
- class AbilityResult {
218
- effect;
219
- strategy;
220
- constructor(effect, strategy) {
221
- this.effect = effect;
222
- this.strategy = strategy;
223
- }
224
- /**
225
- * Returns a list of explanations for each policy involved in the ability evaluation.
226
- * Each item describes how a specific policy contributed to the final permission result.
227
- *
228
- * Useful for debugging, logging, or building UI tools that visualize permission logic.
229
- */
230
- explain() {
231
- const resMarker = this.strategy.isDenied() ? '== DENIED==' : '== ALLOWED ==';
232
- const policiesExplain = this.strategy.policies
233
- .map(policy => {
234
- return new AbilityExplainPolicy(policy).toString();
235
- })
236
- .join('\n');
237
- return `${resMarker}\n${policiesExplain}\n`;
238
- }
239
- decisive() {
240
- return this.strategy.decisivePolicy();
241
- }
242
- explainDecisive() {
243
- const policy = this.decisive();
244
- if (!policy) {
245
- return null;
246
- }
247
- return new AbilityExplainPolicy(policy).toString();
248
- }
249
- isAllowed = () => {
250
- return this.strategy.isAllowed();
251
- };
252
- isDenied = () => {
253
- return this.strategy.isDenied();
254
- };
255
- }
256
-
257
- function brand$1(code) {
258
- return code;
259
- }
260
- const AbilityPolicyEffect = {
261
- deny: brand$1('deny'),
262
- permit: brand$1('permit'),
263
- };
264
-
265
- class AbilityResolver {
266
- onDeny;
267
- onAllow;
268
- StrategyClass;
269
- policyEntries;
270
- constructor(
271
- /**
272
- * `Important!` The incorrect Resources type was intentionally passed to AbilityPolicy so that TypeScript could suggest the name of the permission and the structure of its resource in the parse method.
273
- */
274
- policyOrListOfPolicies, strategy, options = {}) {
275
- const policies = this.toArray(policyOrListOfPolicies);
276
- this.onDeny = options.onDeny;
277
- this.onAllow = options.onAllow;
278
- const filtered = options.tags
279
- ? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
280
- : policies;
281
- const sorted = [...filtered].sort((a, b) => b.priority - a.priority);
282
- this.policyEntries = sorted.map(policy => ({
283
- policy,
284
- normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
285
- segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
286
- }));
287
- this.StrategyClass = strategy;
288
- }
289
- /**
290
- * Resolve policy for the resource and permission key
291
- *
292
- * @param permission - Permission key
293
- * @param resource - Resource
294
- * @param environment
295
- */
296
- resolve(permission, resource, environment) {
297
- const inputNormalized = AbilityResolver.normalizePermission(String(permission));
298
- const inputSegments = inputNormalized.split('.');
299
- const filteredPolicies = this.policyEntries
300
- .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
301
- .map(entry => entry.policy);
302
- // 2. check the policies
303
- for (const policy of filteredPolicies) {
304
- if (policy.disabled) {
305
- continue;
306
- }
307
- const policyMatchState = policy.check(resource, environment);
308
- if (policyMatchState === AbilityMatch.pending) {
309
- throw new AbilityError(`The policy "${policy.name}" is still in a pending state. Make sure to call "check" to evaluate the policy before resolving permissions.`);
310
- }
311
- }
312
- // 3. Use strategy
313
- const strategy = new this.StrategyClass(filteredPolicies);
314
- const effect = strategy.evaluate();
315
- const result = new AbilityResult(effect, strategy);
316
- if (effect === AbilityPolicyEffect.deny && this.onDeny) {
317
- this.onDeny(result);
318
- }
319
- if (effect === AbilityPolicyEffect.permit && this.onAllow) {
320
- this.onAllow(result);
321
- }
322
- return result;
323
- }
324
- enforce(permission, resource, environment, options) {
325
- const result = this.resolve(permission, resource, environment);
326
- if (result.isDenied()) {
327
- options?.onDeny && options?.onDeny(result);
328
- throw new AbilityError(`Permission denied`);
329
- }
330
- }
331
- /**
332
- * @deprecated - will be removed
333
- *
334
- * Check if the permission key is contained in another permission key
335
- * @param permissionA - The first permission to check
336
- * @param permissionB - The second permission to check
337
- */
338
- static isInPermissionContain(permissionA, permissionB) {
339
- const A = permissionA.split('.');
340
- const B = permissionB.split('.');
341
- const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
342
- return shorter.every((chunk, i) => {
343
- return chunk === '*' || longer[i] === '*' || chunk === longer[i];
344
- });
345
- }
346
- toArray(value) {
347
- return [...(Array.isArray(value) ? value : [value])];
348
- }
349
- static normalizePermission(permission) {
350
- return permission
351
- .trim()
352
- .replace(/^permission\./, '') // remove prefix
353
- .replace(/\.+/g, '.') // collapse multiple dots
354
- .toLowerCase(); // optional: make case-insensitive
355
- }
356
- static matchPermissions(policySegments, inputSegments) {
357
- let i = 0;
358
- for (; i < policySegments.length; i++) {
359
- const pSeg = policySegments[i];
360
- const iSeg = inputSegments[i];
361
- // '*' — глобальный wildcard: матчим всё, что дальше
362
- if (pSeg === '*') {
363
- return true;
364
- }
365
- // input закончился раньше — mismatch
366
- if (iSeg === undefined) {
367
- return false;
368
- }
369
- // обычное сравнение
370
- if (pSeg !== iSeg) {
371
- return false;
372
- }
373
- }
374
- // Если политика закончилась, но input длиннее — match только если последний сегмент был '*'
375
- return i === inputSegments.length;
376
- }
377
- }
378
-
379
- class AbilityTypeGenerator {
380
- policies;
381
- policyEntries;
382
- constructor(policies) {
383
- this.policies = policies;
384
- this.policyEntries = policies.map(policy => ({
385
- policy,
386
- normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
387
- segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
388
- }));
389
- }
390
- /**
391
- * Generates TypeScript type definitions based on the provided policies.
392
- * @returns A generated type definitions.
393
- */
394
- generateTypeDefs() {
395
- // Structure to store types: { [action]: { [subjectPath]: type } }
396
- const resorceStructure = {};
397
- const environmentStructure = {};
398
- // tags
399
- const allTags = new Set();
400
- // Iterate through all policies
401
- this.policies.forEach(policy => {
402
- policy.tags.forEach(tag => allTags.add(tag));
403
- const action = policy.permission;
404
- // Initialize object for action if it doesn't exist
405
- if (!resorceStructure[action]) {
406
- resorceStructure[action] = {};
407
- }
408
- // Iterate through all ruleSets in the policy
409
- policy.ruleSet.forEach(ruleSet => {
410
- // Iterate through all rules in the ruleSet
411
- ruleSet.rules.forEach(rule => {
412
- const subjectPath = rule.subject;
413
- const ruleType = this.determineTypeFromRule(rule);
414
- if (!ruleType) {
415
- return;
416
- }
417
- // -----------------------------
418
- // ENVIRONMENT HANDLING (subject)
419
- // -----------------------------
420
- if (subjectPath.startsWith('env.')) {
421
- const envPath = subjectPath.replace(/^env\./, '');
422
- if (!environmentStructure[action]) {
423
- environmentStructure[action] = {};
424
- }
425
- environmentStructure[action][envPath] = ruleType;
426
- }
427
- else {
428
- const existingType = resorceStructure[action][subjectPath];
429
- if (existingType && existingType !== ruleType) {
430
- resorceStructure[action][subjectPath] = `${existingType} | ${ruleType}`;
431
- }
432
- else {
433
- resorceStructure[action][subjectPath] = ruleType;
434
- }
435
- }
436
- // -----------------------------
437
- // RESOURCE PATH HANDLING (right side)
438
- // -----------------------------
439
- if (typeof rule.resource === 'string' && this.isPath(rule.resource)) {
440
- const resourcePath = rule.resource;
441
- // env.* справа
442
- if (resourcePath.startsWith('env.')) {
443
- const envPath = resourcePath.replace(/^env\./, '');
444
- if (!environmentStructure[action]) {
445
- environmentStructure[action] = {};
446
- }
447
- const existingEnvType = environmentStructure[action][envPath];
448
- const targetType = ruleType;
449
- if (existingEnvType && existingEnvType !== targetType) {
450
- environmentStructure[action][envPath] = `${existingEnvType} | ${targetType}`;
451
- }
452
- else {
453
- environmentStructure[action][envPath] = targetType;
454
- }
455
- }
456
- else {
457
- // обычный ресурс справа
458
- if (!resorceStructure[action]) {
459
- resorceStructure[action] = {};
460
- }
461
- const existingResType = resorceStructure[action][resourcePath];
462
- const targetType = ruleType; // или 'unknown'
463
- if (existingResType && existingResType !== targetType) {
464
- resorceStructure[action][resourcePath] = `${existingResType} | ${targetType}`;
465
- }
466
- else {
467
- resorceStructure[action][resourcePath] = targetType;
468
- }
469
- }
470
- }
471
- });
472
- });
473
- });
474
- const filteredStructure = {};
475
- Object.entries(resorceStructure).forEach(([action, fields]) => {
476
- if (!action.endsWith('.*')) {
477
- filteredStructure[action] = fields;
478
- }
479
- });
480
- // Transform flat structure into nested structure for easier use
481
- const nestedStructure = this.buildNestedStructure(filteredStructure);
482
- const nestedEnvironment = this.buildNestedStructure(environmentStructure);
483
- return this.formatTypeDefinitions(nestedStructure, nestedEnvironment, allTags);
484
- }
485
- isPath(value) {
486
- if (typeof value !== 'string') {
487
- return false;
488
- }
489
- if (value.startsWith('"') || value.startsWith("'")) {
490
- return false;
491
- }
492
- return value.includes('.');
493
- }
494
- /**
495
- * Determines TypeScript type based on the rule
496
- * @param rule - The rule to analyze
497
- * @returns TypeScript type as string
498
- */
499
- determineTypeFromRule(rule) {
500
- if (rule.condition === AbilityCondition.never || rule.condition === AbilityCondition.always) {
501
- return null;
502
- }
503
- if (rule.condition === AbilityCondition.contains ||
504
- rule.condition === AbilityCondition.not_contains) {
505
- return this.getArrayType(rule.resource);
506
- }
507
- if (rule.condition === AbilityCondition.length_equals ||
508
- rule.condition === AbilityCondition.length_greater_than ||
509
- rule.condition === AbilityCondition.length_less_than) {
510
- return 'string | readonly unknown[]';
511
- }
512
- // Numeric comparisons - always number
513
- if (rule.condition === AbilityCondition.greater_than ||
514
- rule.condition === AbilityCondition.greater_or_equal ||
515
- rule.condition === AbilityCondition.less_than ||
516
- rule.condition === AbilityCondition.less_or_equal) {
517
- return 'number';
518
- }
519
- // Array operations
520
- if (rule.condition === AbilityCondition.in || rule.condition === AbilityCondition.not_in) {
521
- return this.getInArrayType(rule.resource);
522
- }
523
- // Equality/Inequality operations
524
- if (rule.condition === AbilityCondition.equals ||
525
- rule.condition === AbilityCondition.not_equals) {
526
- return this.getPrimitiveType(rule.resource);
527
- }
528
- return 'any';
529
- }
530
- /**
531
- * Gets TypeScript type for array values
532
- * @param resource - The resource value to analyze
533
- * @returns TypeScript array type as string
534
- */
535
- getArrayType(resource) {
536
- const elementType = this.getInArrayType(resource);
537
- return `readonly ${elementType}[]`;
538
- }
539
- getInArrayType(resource) {
540
- if (Array.isArray(resource)) {
541
- if (resource.length === 0) {
542
- return 'unknown';
543
- }
544
- // Determine types of array elements
545
- const elementTypes = new Set(resource.map(item => this.getPrimitiveType(item)));
546
- return elementTypes.size === 1
547
- ? Array.from(elementTypes)[0]
548
- : `(${Array.from(elementTypes).join(' | ')})`;
549
- }
550
- // If resource is not an array but condition is in/not_in,
551
- // it expects an array of such elements
552
- return this.getPrimitiveType(resource);
553
- }
554
- /**
555
- * Gets primitive TypeScript type for a value
556
- * @param value - The value to analyze
557
- * @returns TypeScript primitive type as string
558
- */
559
- getPrimitiveType(value) {
560
- if (value === null) {
561
- return 'null | unknown';
562
- }
563
- if (value === undefined) {
564
- return 'undefined';
565
- }
566
- if (typeof value === 'string' && this.isPath(value)) {
567
- // This is not a string literal, but a path to another field.
568
- return 'unknown';
569
- }
570
- switch (typeof value) {
571
- case 'string':
572
- return 'string';
573
- case 'number':
574
- return 'number';
575
- case 'boolean':
576
- return 'boolean';
577
- case 'object':
578
- if (Array.isArray(value)) {
579
- return 'array'; // special marker, handled separately
580
- }
581
- return 'object';
582
- default:
583
- return 'any';
584
- }
585
- }
586
- /**
587
- * Builds nested structure from flat paths
588
- * Example: 'user.profile.name' -> { user: { profile: { name: 'string' } } }
589
- * @param flatStructure - Flat structure with dot notation paths
590
- * @returns Nested object structure
591
- */
592
- buildNestedStructure(flatStructure) {
593
- const result = {};
594
- Object.entries(flatStructure).forEach(([action, paths]) => {
595
- result[action] = {};
596
- Object.entries(paths).forEach(([path, type]) => {
597
- const parts = path.split('.');
598
- let current = result[action];
599
- // Iterate through all parts except the last one
600
- for (let i = 0; i < parts.length - 1; i++) {
601
- const part = parts[i];
602
- const currentValue = current[part];
603
- if (!currentValue || typeof currentValue !== 'object') {
604
- const newObj = {};
605
- current[part] = newObj;
606
- current = newObj;
607
- }
608
- else {
609
- current = currentValue;
610
- }
611
- }
612
- // Set type for the last part
613
- const lastPart = parts[parts.length - 1];
614
- current[lastPart] = type;
615
- });
616
- });
617
- return result;
618
- }
619
- /**
620
- * Formats type structure into a string
621
- * @param structure - Nested type structure
622
- * @param environment
623
- * @param allTags
624
- * @returns Formatted TypeScript type definition string
625
- */
626
- formatTypeDefinitions(structure, environment, allTags) {
627
- let output = '// Automatically generated by via-profit/ability\n';
628
- output += '// Do not edit manually\n';
629
- output += 'export type Resources = {\n';
630
- const sortedActions = Object.keys(structure).sort();
631
- sortedActions.forEach(permission => {
632
- const actionObj = structure[permission];
633
- const isEmpty = Object.keys(actionObj).length === 0;
634
- const inputNormalized = AbilityResolver.normalizePermission(permission);
635
- const inputSegments = inputNormalized.split('.');
636
- const filteredPolicies = this.policyEntries
637
- .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
638
- .map(entry => entry.policy);
639
- // Effects
640
- const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
641
- // Policies list
642
- const items = filteredPolicies
643
- .sort((a, b) => a.id.localeCompare(b.id))
644
- .map(p => {
645
- const effect = p.effect.padEnd(6, ' '); // permit / deny / audit
646
- const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
647
- return ` * - ${effect} ${p.id} "${displayName}"`;
648
- })
649
- .join('\n');
650
- //
651
- output += `
652
- /**
653
- * Permission: ${permission}
654
- * Effects: ${effects.join(', ')}
655
- * Policies:
656
- ${items}
657
- */
658
- `;
659
- if (isEmpty) {
660
- // empty object → undefined
661
- output += ` ['${permission}']: undefined;\n`;
662
- }
663
- else {
664
- // not empty object
665
- output += ` ['${permission}']: {\n`;
666
- output += this.formatNestedObject(actionObj, 4);
667
- output += ' } | null | undefined;\n';
668
- }
669
- });
670
- output += '}\n';
671
- // tags
672
- const tagsUnion = allTags.size > 0
673
- ? Array.from(allTags)
674
- .sort()
675
- .map(tag => `'${tag}'`)
676
- .join(' | ')
677
- : 'never';
678
- output += `\n\nexport type PolicyTags = ${tagsUnion};\n`;
679
- // environments
680
- output += '\n\nexport type Environment = {\n';
681
- Object.entries(environment).forEach(([permission, envObj]) => {
682
- const isEmpty = Object.keys(envObj).length === 0;
683
- const inputNormalized = AbilityResolver.normalizePermission(permission);
684
- const inputSegments = inputNormalized.split('.');
685
- const filteredPolicies = this.policyEntries
686
- .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
687
- .map(entry => entry.policy);
688
- const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
689
- const items = filteredPolicies
690
- .sort((a, b) => a.id.localeCompare(b.id))
691
- .map(p => {
692
- const effect = p.effect.padEnd(6, ' ');
693
- const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
694
- return ` * - ${effect} ${p.id} "${displayName}"`;
695
- })
696
- .join('\n');
697
- output += `
698
- /**
699
- * Permission: ${permission}
700
- * Effects: ${effects.join(', ')}
701
- * Policies:
702
- ${items}
703
- */
704
- `;
705
- if (isEmpty) {
706
- output += ` ['${permission}']: undefined;\n`;
707
- }
708
- else {
709
- output += ` ['${permission}']: {\n`;
710
- output += this.formatNestedObject(envObj, 4);
711
- output += ' } | null | undefined;\n';
712
- }
713
- });
714
- output += '}\n';
715
- // complex
716
- return output;
717
- }
718
- /**
719
- * Recursively formats nested object
720
- * @param obj - Object to format
721
- * @param indent - Current indentation level
722
- * @returns Formatted string
723
- */
724
- formatNestedObject(obj, indent) {
725
- const spaces = ' '.repeat(indent);
726
- let output = '';
727
- // Sort keys for stable output
728
- const sortedKeys = Object.keys(obj).sort();
729
- sortedKeys.forEach(key => {
730
- const value = obj[key];
731
- if (typeof value === 'object' && value !== null) {
732
- // Nested object
733
- output += `${spaces}readonly ${key}: {\n`;
734
- output += this.formatNestedObject(value, indent + 2);
735
- output += `${spaces}} | null | undefined;\n`;
736
- }
737
- else {
738
- // Primitive type
739
- const va = [String(value)];
740
- let v = String(value);
741
- if (!v.match(/unknown/)) {
742
- if (!v.match(/null/)) {
743
- va.push('null');
744
- }
745
- if (!v.match(/undefined/)) {
746
- va.push('undefined');
747
- }
748
- }
749
- output += `${spaces}readonly ${key}: ${va.join(' | ')} \n`;
750
- }
751
- });
752
- return output;
753
- }
754
- }
755
-
756
- class AbilityHash {
757
- static sha1(message) {
758
- const msgBytes = AbilityHash.stringToBytes(message);
759
- const msgBitLength = msgBytes.length * 8;
760
- const withOne = new Uint8Array(msgBytes.length + 1);
761
- withOne.set(msgBytes, 0);
762
- withOne[msgBytes.length] = 0x80;
763
- let zeroBytes = (56 - (withOne.length % 64) + 64) % 64;
764
- const padded = new Uint8Array(withOne.length + zeroBytes + 8);
765
- padded.set(withOne, 0);
766
- const bitLenHigh = Math.floor(msgBitLength / 0x100000000);
767
- const bitLenLow = msgBitLength >>> 0;
768
- padded[padded.length - 8] = (bitLenHigh >>> 24) & 0xff;
769
- padded[padded.length - 7] = (bitLenHigh >>> 16) & 0xff;
770
- padded[padded.length - 6] = (bitLenHigh >>> 8) & 0xff;
771
- padded[padded.length - 5] = bitLenHigh & 0xff;
772
- padded[padded.length - 4] = (bitLenLow >>> 24) & 0xff;
773
- padded[padded.length - 3] = (bitLenLow >>> 16) & 0xff;
774
- padded[padded.length - 2] = (bitLenLow >>> 8) & 0xff;
775
- padded[padded.length - 1] = bitLenLow & 0xff;
776
- let h0 = 0x67452301;
777
- let h1 = 0xefcdab89;
778
- let h2 = 0x98badcfe;
779
- let h3 = 0x10325476;
780
- let h4 = 0xc3d2e1f0;
781
- const w = new Array(80);
782
- for (let i = 0; i < padded.length; i += 64) {
783
- for (let j = 0; j < 16; j++) {
784
- const idx = i + j * 4;
785
- w[j] =
786
- (padded[idx] << 24) | (padded[idx + 1] << 16) | (padded[idx + 2] << 8) | padded[idx + 3];
787
- }
788
- for (let j = 16; j < 80; j++) {
789
- w[j] = AbilityHash.leftRotate(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
790
- }
791
- let a = h0;
792
- let b = h1;
793
- let c = h2;
794
- let d = h3;
795
- let e = h4;
796
- for (let j = 0; j < 80; j++) {
797
- let f;
798
- let k;
799
- if (j < 20) {
800
- f = (b & c) | (~b & d);
801
- k = 0x5a827999;
802
- }
803
- else {
804
- if (j < 40) {
805
- f = b ^ c ^ d;
806
- k = 0x6ed9eba1;
807
- }
808
- else {
809
- if (j < 60) {
810
- f = (b & c) | (b & d) | (c & d);
811
- k = 0x8f1bbcdc;
812
- }
813
- else {
814
- f = b ^ c ^ d;
815
- k = 0xca62c1d6;
816
- }
817
- }
818
- }
819
- const temp = (AbilityHash.leftRotate(a, 5) + f + e + k + (w[j] | 0)) | 0;
820
- e = d;
821
- d = c;
822
- c = AbilityHash.leftRotate(b, 30);
823
- b = a;
824
- a = temp;
825
- }
826
- h0 = (h0 + a) | 0;
827
- h1 = (h1 + b) | 0;
828
- h2 = (h2 + c) | 0;
829
- h3 = (h3 + d) | 0;
830
- h4 = (h4 + e) | 0;
831
- }
832
- return [
833
- AbilityHash.toHex32(h0),
834
- AbilityHash.toHex32(h1),
835
- AbilityHash.toHex32(h2),
836
- AbilityHash.toHex32(h3),
837
- AbilityHash.toHex32(h4),
838
- ].join('');
839
- }
840
- static leftRotate(value, bits) {
841
- return ((value << bits) | (value >>> (32 - bits))) >>> 0;
842
- }
843
- static toHex32(num) {
844
- return (num >>> 0).toString(16).padStart(8, '0');
845
- }
846
- static stringToBytes(str) {
847
- if (typeof TextEncoder !== 'undefined') {
848
- const encoder = new TextEncoder();
849
- return encoder.encode(str);
850
- }
851
- else {
852
- const buf = Buffer.from(str, 'utf8');
853
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
854
- }
855
- }
856
- }
857
-
858
- class AbilityPolicy {
859
- matchState = AbilityMatch.pending;
860
- /**
861
- * List of rules
862
- */
863
- ruleSet = [];
864
- /**
865
- * Policy effect
866
- */
867
- effect;
868
- /**
869
- * Rules compare method.\
870
- * For the «and» method the rule will be permitted if all\
871
- * rules will be returns «permit» status and for the «or» - if\
872
- * one of the rules returns as «permit»
873
- */
874
- compareMethod = AbilityCompare.and;
875
- /**
876
- * Policy ID
877
- */
878
- id;
879
- /**
880
- * Policy name
881
- */
882
- name;
883
- description;
884
- /**
885
- * Running the `enforce` or `resolve` method
886
- * will select only those from all passed policies that fall under the specified permission key.
887
- */
888
- permission;
889
- priority = -1;
890
- disabled;
891
- tags;
892
- constructor(params) {
893
- const { name, description, id, permission, effect, compareMethod = AbilityCompare.and, priority, disabled, tags, } = params;
894
- this.permission = permission;
895
- this.description = description;
896
- this.effect = effect;
897
- this.compareMethod = compareMethod;
898
- this.priority = typeof priority === 'number' ? priority : -1;
899
- this.disabled = typeof disabled === 'boolean' ? disabled : false;
900
- this.tags = (tags || []);
901
- this.id = id || `p_${this.hash().slice(0, 10)}`;
902
- this.name = name || this.id;
903
- }
904
- /**
905
- * Add rule set to the policy
906
- * @param ruleSet - The rule set to add
907
- */
908
- addRuleSet(ruleSet) {
909
- this.ruleSet.push(ruleSet);
910
- return this;
911
- }
912
- /**
913
- * Add rule set to the policy
914
- * @param ruleSets - The array of rule set to add
915
- */
916
- addRuleSets(ruleSets) {
917
- for (const ruleSet of ruleSets) {
918
- this.ruleSet.push(ruleSet);
919
- }
920
- return this;
921
- }
922
- /**
923
- * Extract all rules of all ruleSets of this policy
924
- */
925
- extractRules() {
926
- const rules = [];
927
- for (const ruleSet of this.ruleSet) {
928
- for (const rule of ruleSet.rules) {
929
- rules.push(rule);
930
- }
931
- }
932
- return rules;
933
- }
934
- /**
935
- * Check if the policy is matched
936
- * @param resource - The resource to check
937
- * @param environment - The user environment object
938
- */
939
- check(resource, environment) {
940
- this.matchState = AbilityMatch.mismatch;
941
- if (this.disabled) {
942
- this.matchState = AbilityMatch.disabled;
943
- return this.matchState;
944
- }
945
- if (!this.ruleSet.length) {
946
- return this.matchState;
947
- }
948
- const normalGroups = this.ruleSet.filter(g => !g.isExcept);
949
- const exceptGroups = this.ruleSet.filter(g => g.isExcept);
950
- const normalStates = [];
951
- for (const group of normalGroups) {
952
- if (group.disabled) {
953
- continue;
954
- }
955
- const state = group.check(resource, environment);
956
- normalStates.push(state);
957
- if (AbilityCompare.and === this.compareMethod && AbilityMatch.mismatch === state) {
958
- this.matchState = AbilityMatch.mismatch;
959
- return this.matchState;
960
- }
961
- if (AbilityCompare.or === this.compareMethod && AbilityMatch.match === state) {
962
- this.matchState = AbilityMatch.match;
963
- // break to check except-rule sets
964
- break;
965
- }
966
- }
967
- // 3. Simple rule sets
968
- let normalMatch = false;
969
- if (AbilityCompare.and === this.compareMethod) {
970
- normalMatch = normalStates.every(s => AbilityMatch.match === s);
971
- }
972
- else {
973
- normalMatch = normalStates.some(s => AbilityMatch.match === s);
974
- }
975
- if (!normalMatch) {
976
- this.matchState = AbilityMatch.mismatch;
977
- return this.matchState;
978
- }
979
- // 4. except-rule sets
980
- for (const group of exceptGroups) {
981
- if (group.disabled) {
982
- continue;
983
- }
984
- const state = group.check(resource, environment);
985
- if (AbilityMatch.match === state) {
986
- this.matchState = AbilityMatch.exceptMismatch;
987
- return this.matchState;
988
- }
989
- }
990
- // 5. match
991
- this.matchState = AbilityMatch.match;
992
- return this.matchState;
993
- }
994
- explain() {
995
- if (this.matchState === AbilityMatch.pending) {
996
- throw new AbilityError('First, run the check method, then explain');
997
- }
998
- return new AbilityExplainPolicy(this);
999
- }
1000
- copyWith(props) {
1001
- const policy = new AbilityPolicy({
1002
- id: props.id ?? this.id,
1003
- name: props.name ?? this.name,
1004
- description: props.description ?? this.description,
1005
- priority: typeof props.priority !== 'undefined' ? props.priority : this.priority,
1006
- permission: props.permission ?? this.permission,
1007
- effect: props.effect ?? this.effect,
1008
- compareMethod: props.compareMethod ?? this.compareMethod,
1009
- });
1010
- const nextRuleSet = props.ruleSet ?? this.ruleSet;
1011
- for (const rule of nextRuleSet) {
1012
- policy.addRuleSet(rule);
1013
- }
1014
- return policy;
1015
- }
1016
- hash() {
1017
- const parts = [
1018
- `permission:${this.permission}`,
1019
- `effect:${this.effect}`,
1020
- `compareMethod:${this.compareMethod}`,
1021
- `priority:${this.priority}`,
1022
- `disabled:${this.disabled}`,
1023
- ];
1024
- if (this.tags && this.tags.length > 0) {
1025
- parts.push(`tags:${[...this.tags].sort().join(',')}`);
1026
- }
1027
- if (this.ruleSet && this.ruleSet.length > 0) {
1028
- const ruleHashes = this.ruleSet.map(r => r.hash());
1029
- parts.push(`rules:${ruleHashes.sort().join('|')}`);
1030
- }
1031
- const str = parts.join(';');
1032
- return AbilityHash.sha1(str);
1033
- }
1034
- }
1035
-
1036
- /**
1037
- * Represents a rule that defines a condition to be checked against a subject and resource.
1038
- */
1039
- class AbilityRule {
1040
- /**
1041
- * Subject key path like a 'user.name'
1042
- */
1043
- subject;
1044
- /**
1045
- * Resource key path like a 'user.name' or value
1046
- */
1047
- resource;
1048
- condition;
1049
- name;
1050
- description;
1051
- id;
1052
- state = AbilityMatch.pending;
1053
- disabled;
1054
- /**
1055
- * Creates an instance of AbilityRule.
1056
- * @param {string} params.id - The unique identifier of the rule.
1057
- * @param {string} params.name - The name of the rule.
1058
- * @param {AbilityCondition} params.condition - The condition to evaluate.
1059
- * @param {string} params.subject - The subject of the rule.
1060
- * @param {string} params.resource - The resource to compare against.
1061
- * @param {boolean} params.disabled - Disabling flag.
1062
- * @param params
1063
- */
1064
- constructor(params) {
1065
- const { id, name, subject, resource, condition, disabled, description } = params;
1066
- this.description = description;
1067
- this.disabled = typeof disabled === 'boolean' ? disabled : false;
1068
- this.subject = subject;
1069
- this.resource = resource;
1070
- this.condition = condition;
1071
- this.state = this.disabled ? AbilityMatch.disabled : this.state;
1072
- this.id = id || `r_${this.hash().slice(0, 10)}`;
1073
- this.name = name || this.id;
1074
- }
1075
- static isPrimitive(v) {
1076
- return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
1077
- }
1078
- static isNumber(v) {
1079
- return typeof v === 'number';
1080
- }
1081
- static isString(v) {
1082
- return typeof v === 'string';
1083
- }
1084
- static valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
1085
- static operatorHandlers = {
1086
- [toLiteral(AbilityCondition.always)]: () => true,
1087
- [toLiteral(AbilityCondition.defined)]: (a) => typeof a !== 'undefined',
1088
- [toLiteral(AbilityCondition.not_defined)]: (a) => typeof a === 'undefined',
1089
- [toLiteral(AbilityCondition.never)]: () => false,
1090
- [toLiteral(AbilityCondition.equals)]: (a, b) => a === b,
1091
- [toLiteral(AbilityCondition.not_equals)]: (a, b) => a !== b,
1092
- [toLiteral(AbilityCondition.contains)]: (a, b) => {
1093
- if (Array.isArray(a) && AbilityRule.isPrimitive(b)) {
1094
- return a.includes(b);
1095
- }
1096
- if (Array.isArray(a) && Array.isArray(b)) {
1097
- return a.some(v => b.includes(v));
1098
- }
1099
- return false;
1100
- },
1101
- [toLiteral(AbilityCondition.not_contains)]: (a, b) => {
1102
- if (Array.isArray(a) && AbilityRule.isPrimitive(b)) {
1103
- return !a.includes(b);
1104
- }
1105
- if (Array.isArray(a) && Array.isArray(b)) {
1106
- return !a.some(v => b.includes(v));
1107
- }
1108
- return false;
1109
- },
1110
- [toLiteral(AbilityCondition.in)]: (a, b) => {
1111
- if (AbilityRule.isPrimitive(a) && Array.isArray(b)) {
1112
- return b.includes(a);
1113
- }
1114
- if (Array.isArray(a) && Array.isArray(b)) {
1115
- return a.some(v => b.includes(v));
1116
- }
1117
- return false;
1118
- },
1119
- [toLiteral(AbilityCondition.not_in)]: (a, b) => {
1120
- if (AbilityRule.isPrimitive(a) && Array.isArray(b)) {
1121
- return !b.includes(a);
1122
- }
1123
- if (Array.isArray(a) && Array.isArray(b)) {
1124
- return !a.some(v => b.includes(v));
1125
- }
1126
- return false;
1127
- },
1128
- [toLiteral(AbilityCondition.greater_than)]: (a, b) => {
1129
- return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a > b : false;
1130
- },
1131
- [toLiteral(AbilityCondition.less_than)]: (a, b) => {
1132
- return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a < b : false;
1133
- },
1134
- [toLiteral(AbilityCondition.greater_or_equal)]: (a, b) => {
1135
- return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a >= b : false;
1136
- },
1137
- [toLiteral(AbilityCondition.less_or_equal)]: (a, b) => {
1138
- return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a <= b : false;
1139
- },
1140
- [toLiteral(AbilityCondition.length_greater_than)]: (a, b) => {
1141
- const alen = AbilityRule.valueLen(a);
1142
- if (alen === null) {
1143
- return false;
1144
- }
1145
- if (AbilityRule.isNumber(b)) {
1146
- return alen > b;
1147
- }
1148
- const bLen = AbilityRule.valueLen(b);
1149
- if (bLen !== null) {
1150
- return alen > bLen;
1151
- }
1152
- return false;
1153
- },
1154
- [toLiteral(AbilityCondition.length_less_than)]: (a, b) => {
1155
- const alen = AbilityRule.valueLen(a);
1156
- if (alen === null) {
1157
- return false;
1158
- }
1159
- if (AbilityRule.isNumber(b)) {
1160
- return alen < b;
1161
- }
1162
- const bLen = AbilityRule.valueLen(b);
1163
- if (bLen !== null) {
1164
- return alen < bLen;
1165
- }
1166
- return false;
1167
- },
1168
- [toLiteral(AbilityCondition.length_equals)]: (a, b) => {
1169
- const alen = AbilityRule.valueLen(a);
1170
- if (alen === null) {
1171
- return false;
1172
- }
1173
- if (AbilityRule.isNumber(b)) {
1174
- return alen === b;
1175
- }
1176
- const bLen = AbilityRule.valueLen(b);
1177
- if (bLen !== null) {
1178
- return alen === bLen;
1179
- }
1180
- return false;
1181
- },
1182
- };
1183
- /**
1184
- * Check if the rule is matched
1185
- * @param resource - The resource to check
1186
- * @param environment
1187
- */
1188
- check(resource, environment) {
1189
- if (this.disabled) {
1190
- this.state = AbilityMatch.disabled;
1191
- return this.state;
1192
- }
1193
- const [subjectValue, resourceValue] = this.extractValues(resource, environment);
1194
- const handler = AbilityRule.operatorHandlers[toLiteral(this.condition)];
1195
- const result = handler(subjectValue, resourceValue);
1196
- this.state = result ? AbilityMatch.match : AbilityMatch.mismatch;
1197
- return this.state;
1198
- }
1199
- /**
1200
- * Extract values from the resourceData
1201
- * @param resourceData - The resourceData to extract values from
1202
- * @param environment - Environment data
1203
- */
1204
- extractValues(resourceData, environment) {
1205
- let subjectValue;
1206
- let resourceValue;
1207
- if ((resourceData === null || typeof resourceData === 'undefined') &&
1208
- (environment === null || typeof environment === 'undefined')) {
1209
- return [NaN, NaN];
1210
- }
1211
- // left side resolve
1212
- if (this.subject.includes('.')) {
1213
- // if is environment
1214
- if (this.subject.startsWith('env.') && typeof environment !== 'undefined') {
1215
- subjectValue = this.getDotNotationValue(environment, this.subject.replace(/^env\./, ''));
1216
- // if is resource
1217
- }
1218
- else {
1219
- subjectValue = this.getDotNotationValue(resourceData, this.subject);
1220
- }
1221
- }
1222
- else {
1223
- subjectValue = this.subject;
1224
- }
1225
- // right side resolve
1226
- if (typeof this.resource === 'string' && this.resource.includes('.')) {
1227
- // if is environment
1228
- if (this.resource.startsWith('env.') && typeof environment !== 'undefined') {
1229
- resourceValue = this.getDotNotationValue(environment, this.resource.replace(/^env\./, ''));
1230
- }
1231
- else {
1232
- // if is resource
1233
- resourceValue = this.getDotNotationValue(resourceData, this.resource);
1234
- }
1235
- }
1236
- else {
1237
- resourceValue = this.resource;
1238
- }
1239
- return [subjectValue, resourceValue];
1240
- }
1241
- static _pathCache = new Map();
1242
- static _parsePath(desc) {
1243
- const cached = AbilityRule._pathCache.get(desc);
1244
- if (cached)
1245
- return cached;
1246
- const parts = desc.split('.');
1247
- const segments = [];
1248
- for (const part of parts) {
1249
- const bracketIdx = part.indexOf('[');
1250
- if (bracketIdx !== -1) {
1251
- // формат: "prop[index]" (индекс может быть только числом)
1252
- const prop = part.slice(0, bracketIdx);
1253
- const indexStr = part.slice(bracketIdx + 1, -1);
1254
- const index = Number(indexStr);
1255
- segments.push({ prop, index });
1256
- }
1257
- else {
1258
- segments.push(part);
1259
- }
1260
- }
1261
- AbilityRule._pathCache.set(desc, segments);
1262
- return segments;
1263
- }
1264
- /**
1265
- * Get the value of the object by dot notation
1266
- * @param resource - The object to get the value from
1267
- * @param desc - The dot notation string
1268
- */
1269
- getDotNotationValue(resource, desc) {
1270
- if (resource == null) {
1271
- return undefined;
1272
- }
1273
- const segments = AbilityRule._parsePath(desc);
1274
- let current = resource;
1275
- for (const seg of segments) {
1276
- if (current == null) {
1277
- return undefined;
1278
- }
1279
- if (typeof seg === 'string') {
1280
- current = current[seg];
1281
- }
1282
- else {
1283
- const arr = current[seg.prop];
1284
- current = Array.isArray(arr) ? arr[seg.index] : undefined;
1285
- }
1286
- }
1287
- return current;
1288
- }
1289
- toString() {
1290
- return `AbilityRule: ${this.name} condition: ${toLiteral(this.condition)} subject: "${this.subject?.toString()}" resource: "${this.resource?.toString()}"`;
1291
- }
1292
- copyWith(props) {
1293
- return new AbilityRule({
1294
- id: props.id ?? this.id,
1295
- name: props.name ?? this.name,
1296
- description: props.description ?? this.description,
1297
- subject: props.subject ?? this.subject,
1298
- resource: props.resource ?? this.resource,
1299
- condition: props.condition ?? this.condition,
1300
- });
1301
- }
1302
- hash() {
1303
- const parts = [];
1304
- parts.push(`subject:${this.subject}`);
1305
- parts.push(`resource:${JSON.stringify(this.resource)}`);
1306
- parts.push(`condition:${this.condition}`);
1307
- parts.push(`disabled:${this.disabled}`);
1308
- return AbilityHash.sha1(parts.join(';'));
1309
- }
1310
- static equals(subject, resource) {
1311
- return new AbilityRule({
1312
- condition: AbilityCondition.equals,
1313
- subject,
1314
- resource,
1315
- });
1316
- }
1317
- static notEquals(subject, resource) {
1318
- return new AbilityRule({
1319
- condition: AbilityCondition.not_equals,
1320
- subject,
1321
- resource,
1322
- });
1323
- }
1324
- static contains(subject, resource) {
1325
- return new AbilityRule({
1326
- condition: AbilityCondition.contains,
1327
- subject,
1328
- resource,
1329
- });
1330
- }
1331
- static notContains(subject, resource) {
1332
- return new AbilityRule({
1333
- condition: AbilityCondition.not_contains,
1334
- subject,
1335
- resource,
1336
- });
1337
- }
1338
- static notIn(subject, resource) {
1339
- return new AbilityRule({
1340
- condition: AbilityCondition.not_in,
1341
- subject,
1342
- resource,
1343
- });
1344
- }
1345
- static in(subject, resource) {
1346
- return new AbilityRule({
1347
- condition: AbilityCondition.in,
1348
- subject,
1349
- resource,
1350
- });
1351
- }
1352
- static notEqual(subject, resource) {
1353
- return new AbilityRule({
1354
- condition: AbilityCondition.not_equals,
1355
- subject,
1356
- resource,
1357
- });
1358
- }
1359
- static lessThan(subject, resource) {
1360
- return new AbilityRule({
1361
- condition: AbilityCondition.less_than,
1362
- subject,
1363
- resource,
1364
- });
1365
- }
1366
- static lessOrEqual(subject, resource) {
1367
- return new AbilityRule({
1368
- condition: AbilityCondition.less_or_equal,
1369
- subject,
1370
- resource,
1371
- });
1372
- }
1373
- static moreThan(subject, resource) {
1374
- return new AbilityRule({
1375
- condition: AbilityCondition.greater_than,
1376
- subject,
1377
- resource,
1378
- });
1379
- }
1380
- static moreOrEqual(subject, resource) {
1381
- return new AbilityRule({
1382
- condition: AbilityCondition.greater_or_equal,
1383
- subject,
1384
- resource,
1385
- });
1386
- }
1387
- }
1388
-
1389
- class AbilityRuleSet {
1390
- state = AbilityMatch.pending;
1391
- /**
1392
- * List of rules
1393
- */
1394
- rules = [];
1395
- /**
1396
- * Rules compare method.\
1397
- * For the «and» method the rule will be permitted if all\
1398
- * rules will be returns «permit» status and for the «or» - if\
1399
- * one of the rules returns as «permit»
1400
- */
1401
- compareMethod = AbilityCompare.and;
1402
- /**
1403
- * Group name
1404
- */
1405
- name;
1406
- description;
1407
- /**
1408
- * Group ID
1409
- */
1410
- id;
1411
- isExcept = false;
1412
- disabled;
1413
- constructor(params) {
1414
- const { name, id, compareMethod, isExcept, disabled, description } = params;
1415
- this.description = description;
1416
- this.compareMethod = compareMethod;
1417
- this.isExcept = isExcept;
1418
- this.disabled = typeof disabled === 'boolean' ? disabled : false;
1419
- this.state = this.disabled ? AbilityMatch.disabled : this.state;
1420
- this.id = id || `g_${this.hash().slice(0, 10)}`;
1421
- this.name = name || this.id;
1422
- }
1423
- addRule(rule) {
1424
- this.rules.push(rule);
1425
- return this;
1426
- }
1427
- addRules(rules) {
1428
- rules.forEach(rule => this.addRule(rule));
1429
- return this;
1430
- }
1431
- check(resources, environment) {
1432
- this.state = AbilityMatch.mismatch;
1433
- if (this.disabled) {
1434
- this.state = AbilityMatch.disabled;
1435
- return this.state;
1436
- }
1437
- if (!this.rules.length) {
1438
- return this.state;
1439
- }
1440
- const ruleCheckStates = [];
1441
- for (const rule of this.rules) {
1442
- if (rule.disabled) {
1443
- continue;
1444
- }
1445
- const state = rule.check(resources, environment);
1446
- ruleCheckStates.push(state);
1447
- if (AbilityCompare.and === this.compareMethod && AbilityMatch.mismatch === state) {
1448
- return this.state; // mismatch
1449
- }
1450
- if (AbilityCompare.or === this.compareMethod && AbilityMatch.match === state) {
1451
- this.state = AbilityMatch.match;
1452
- return this.state;
1453
- }
1454
- }
1455
- if (AbilityCompare.and === this.compareMethod) {
1456
- if (ruleCheckStates.every(s => AbilityMatch.match === s)) {
1457
- this.state = AbilityMatch.match;
1458
- }
1459
- }
1460
- if (AbilityCompare.or === this.compareMethod) {
1461
- if (ruleCheckStates.some(s => AbilityMatch.match === s)) {
1462
- this.state = AbilityMatch.match;
1463
- }
1464
- }
1465
- return this.state;
1466
- }
1467
- toString() {
1468
- return `AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod}, rules: ${this.rules.map(rule => rule.toString()).join('\n')}`;
1469
- }
1470
- copyWith(props) {
1471
- const next = new AbilityRuleSet({
1472
- id: props.id ?? this.id,
1473
- name: props.name ?? this.name,
1474
- description: props.description ?? this.description,
1475
- compareMethod: props.compareMethod ?? this.compareMethod,
1476
- });
1477
- const nextRules = props.rules ?? this.rules;
1478
- for (const rule of nextRules) {
1479
- next.addRule(rule);
1480
- }
1481
- return next;
1482
- }
1483
- hash() {
1484
- const ruleHashes = this.rules.map(r => r.hash()).sort();
1485
- const parts = [
1486
- `compareMethod:${this.compareMethod}`,
1487
- `isExcept:${this.isExcept}`,
1488
- `disabled:${this.disabled}`,
1489
- `rules:${ruleHashes.join('|')}`,
1490
- ];
1491
- return AbilityHash.sha1(parts.join(';'));
1492
- }
1493
- static and(rules) {
1494
- return new AbilityRuleSet({
1495
- compareMethod: AbilityCompare.and,
1496
- }).addRules(rules);
1497
- }
1498
- static or(rules) {
1499
- return new AbilityRuleSet({
1500
- compareMethod: AbilityCompare.or,
1501
- }).addRules(rules);
1502
- }
1503
- }
1504
-
1505
- class AbilityJSONParser {
1506
- /**
1507
- * Parses an array of policy configurations into an array of AbilityPolicy instances.
1508
- * @param configs - Array of policy configurations
1509
- * @returns Array of AbilityPolicy instances
1510
- */
1511
- static parse(configs) {
1512
- return configs.map(config => AbilityJSONParser.parsePolicy(config));
1513
- }
1514
- static parsePolicy(config) {
1515
- const { id, name, ruleSet, compareMethod, permission, effect, priority, disabled, tags } = config;
1516
- // Create the empty policy
1517
- const policy = new AbilityPolicy({
1518
- name,
1519
- id,
1520
- permission: permission,
1521
- priority: priority,
1522
- effect: effect,
1523
- disabled,
1524
- tags,
1525
- });
1526
- policy.compareMethod = compareMethod;
1527
- ruleSet.forEach(ruleSetConfig => {
1528
- policy.addRuleSet(AbilityJSONParser.parseRuleSet(ruleSetConfig));
1529
- });
1530
- return policy;
1531
- }
1532
- static parseRule(config) {
1533
- const { id, name, subject, resource, condition, disabled } = config;
1534
- return new AbilityRule({
1535
- id,
1536
- name,
1537
- subject,
1538
- resource,
1539
- disabled,
1540
- condition,
1541
- });
1542
- }
1543
- /**
1544
- * Parse the config JSON format to Group class instance
1545
- */
1546
- static parseRuleSet(config) {
1547
- const { id, name, rules, compareMethod, disabled } = config;
1548
- const ruleSet = new AbilityRuleSet({
1549
- disabled,
1550
- compareMethod: compareMethod,
1551
- name,
1552
- id,
1553
- });
1554
- // Adding rules if exists
1555
- if (rules && rules.length > 0) {
1556
- const abilityRules = rules.map(ruleConfig => AbilityJSONParser.parseRule(ruleConfig));
1557
- ruleSet.addRules(abilityRules);
1558
- }
1559
- return ruleSet;
1560
- }
1561
- static ruleToJSON(rule) {
1562
- return {
1563
- id: rule.id,
1564
- name: rule.name,
1565
- disabled: rule.disabled,
1566
- subject: rule.subject,
1567
- resource: rule.resource,
1568
- condition: rule.condition,
1569
- };
1570
- }
1571
- static ruleSetToJSON(ruleSet) {
1572
- return {
1573
- id: ruleSet.id.toString(),
1574
- name: ruleSet.name.toString(),
1575
- disabled: ruleSet.disabled,
1576
- compareMethod: ruleSet.compareMethod,
1577
- rules: ruleSet.rules.map(rule => AbilityJSONParser.ruleToJSON(rule)),
1578
- };
1579
- }
1580
- static policyToJSON(policy) {
1581
- return {
1582
- id: policy.id.toString(),
1583
- name: policy.name.toString(),
1584
- disabled: policy.disabled,
1585
- priority: policy.priority,
1586
- permission: policy.permission,
1587
- effect: policy.effect,
1588
- compareMethod: policy.compareMethod,
1589
- tags: policy.tags,
1590
- ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
1591
- };
1592
- }
1593
- static toJSON(policies) {
1594
- return policies.map(policy => AbilityJSONParser.policyToJSON(policy));
1595
- }
1596
- }
1597
-
1598
- function brand(code) {
1599
- return code;
1600
- }
1601
- const TokenTypes = {
1602
- EFFECT: brand('EFFECT'),
1603
- IF: brand('IF'),
1604
- PERMISSION: brand('PERMISSION'),
1605
- IDENTIFIER: brand('IDENTIFIER'),
1606
- COLON: brand('COLON'),
1607
- COMMA: brand('COMMA'),
1608
- DOT: brand('DOT'),
1609
- LBRACKET: brand('LBRACKET'),
1610
- RBRACKET: brand('RBRACKET'),
1611
- ALL: brand('ALL'),
1612
- ANY: brand('ANY'),
1613
- OF: brand('OF'),
1614
- EOF: brand('EOF'),
1615
- COMMENT: brand('COMMENT'),
1616
- EQ: brand('EQ'),
1617
- CONTAINS: brand('CONTAINS'),
1618
- IN: brand('IN'),
1619
- NOT_IN: brand('NOT_IN'),
1620
- NOT_CONTAINS: brand('NOT_CONTAINS'),
1621
- GT: brand('GT'),
1622
- GTE: brand('GTE'),
1623
- LT: brand('LT'),
1624
- LTE: brand('LTE'),
1625
- NULL: brand('NULL'),
1626
- EQ_NULL: brand('EQ_NULL'),
1627
- NOT_EQ_NULL: brand('NOT_EQ_NULL'),
1628
- DEFINED: brand('DEFINED'),
1629
- NOT_EQ: brand('NOT_EQ'),
1630
- LEN_GT: brand('LEN_GT'),
1631
- LEN_LT: brand('LEN_LT'),
1632
- LEN_EQ: brand('LEN_EQ'),
1633
- ALWAYS: brand('ALWAYS'),
1634
- NEVER: brand('NEVER'),
1635
- EXCEPT: brand('EXCEPT'),
1636
- ANNOTATION: brand('ANNOTATION'),
1637
- STRING: brand('STRING'),
1638
- NUMBER: brand('NUMBER'),
1639
- BOOLEAN: brand('BOOLEAN'),
1640
- SYMBOL: brand('SYMBOL'),
1641
- KEYWORD: brand('KEYWORD'),
1642
- ALIAS: brand('ALIAS'),
1643
- UNKNOWN: brand('UNKNOWN'),
1644
- };
1645
- class AbilityDSLToken {
1646
- type;
1647
- value;
1648
- line;
1649
- column;
1650
- constructor(type, value, line, column) {
1651
- this.type = type;
1652
- this.value = value;
1653
- this.line = line;
1654
- this.column = column;
1655
- }
1656
- toString() {
1657
- return `AbilityDSLToken([${this.type}] "${this.value}" at ${this.line}:${this.column})`;
1658
- }
1659
- }
1660
-
1661
- class AbilityDSLLexer {
1662
- input;
1663
- pos = 0;
1664
- tokens = [];
1665
- line = 1;
1666
- column = 1;
1667
- keywords = new Set([
1668
- 'if',
1669
- 'all',
1670
- 'any',
1671
- 'of',
1672
- 'permit',
1673
- 'allow',
1674
- 'deny',
1675
- 'forbidden',
1676
- 'true',
1677
- 'false',
1678
- 'null',
1679
- 'defined',
1680
- 'contains',
1681
- 'includes',
1682
- 'length',
1683
- 'len',
1684
- 'has',
1685
- 'in',
1686
- 'gt',
1687
- 'lt',
1688
- 'gte',
1689
- 'lte',
1690
- 'equals',
1691
- 'greater',
1692
- 'less',
1693
- 'not',
1694
- 'is',
1695
- 'or',
1696
- 'than',
1697
- 'always',
1698
- 'never',
1699
- 'except',
1700
- 'alias',
1701
- ]);
1702
- constructor(input) {
1703
- this.input = input;
1704
- }
1705
- tokenize() {
1706
- while (!this.isAtEnd()) {
1707
- this.skipWhitespace();
1708
- if (this.isAtEnd())
1709
- break;
1710
- const char = this.peek();
1711
- if (char === '@') {
1712
- this.tokens.push(this.readAnnotation());
1713
- continue;
1714
- }
1715
- if (char === '#') {
1716
- this.tokens.push(this.readComment());
1717
- continue;
1718
- }
1719
- if (char === '"' || char === "'") {
1720
- this.tokens.push(this.readString());
1721
- continue;
1722
- }
1723
- if (this.isDigit(char)) {
1724
- this.tokens.push(this.readNumber());
1725
- continue;
1726
- }
1727
- if (this.isSymbol(char)) {
1728
- this.tokens.push(this.readSymbol());
1729
- continue;
1730
- }
1731
- if (this.isAlpha(char)) {
1732
- this.tokens.push(this.readWord());
1733
- continue;
1734
- }
1735
- throw new Error(`Unexpected character '${char}' at ${this.line}:${this.column}`);
1736
- }
1737
- this.tokens.push(new AbilityDSLToken(TokenTypes.EOF, '', this.line, this.column));
1738
- return this.tokens;
1739
- }
1740
- readComment() {
1741
- const startLine = this.line;
1742
- const startColumn = this.column;
1743
- this.advance(); // skip '#'
1744
- let value = '';
1745
- while (!this.isAtEnd() && !this.isNewline()) {
1746
- value += this.advance();
1747
- }
1748
- return new AbilityDSLToken(TokenTypes.COMMENT, value.trim(), startLine, startColumn);
1749
- }
1750
- // private readAlias(): AbilityDSLToken {
1751
- // const startLine = this.line;
1752
- // const startColumn = this.column;
1753
- //
1754
- // this.advance(); // skip "alias" keyword
1755
- //
1756
- // // Read colon
1757
- // this.readSymbol();
1758
- //
1759
- // let value = '';
1760
- // while (!this.isAtEnd() && !this.isNewline()) {
1761
- // value += this.advance();
1762
- // }
1763
- // return new AbilityDSLToken(TokenTypes.ALIAS, value.trim(), startLine, startColumn);
1764
- // }
1765
- readAnnotation() {
1766
- const startLine = this.line;
1767
- const startColumn = this.column;
1768
- let raw = '';
1769
- // читаем всю строку после @
1770
- while (!this.isAtEnd() && !this.isNewline()) {
1771
- raw += this.advance();
1772
- }
1773
- raw = raw.trim();
1774
- // parse literals
1775
- let result = '';
1776
- let i = 0;
1777
- let inString = false;
1778
- let quote = null;
1779
- let escaped = false;
1780
- while (i < raw.length) {
1781
- const ch = raw[i];
1782
- if (inString) {
1783
- if (escaped) {
1784
- result += ch;
1785
- escaped = false;
1786
- }
1787
- else if (ch === '\\') {
1788
- escaped = true;
1789
- }
1790
- else if (ch === quote) {
1791
- inString = false;
1792
- quote = null;
1793
- }
1794
- else {
1795
- result += ch;
1796
- }
1797
- i++;
1798
- continue;
1799
- }
1800
- // start of string
1801
- if (ch === '"' || ch === "'") {
1802
- inString = true;
1803
- quote = ch;
1804
- i++;
1805
- continue;
1806
- }
1807
- result += ch;
1808
- i++;
1809
- }
1810
- return new AbilityDSLToken(TokenTypes.ANNOTATION, result.trim(), startLine, startColumn);
1811
- }
1812
- readString() {
1813
- const startLine = this.line;
1814
- const startColumn = this.column;
1815
- const quote = this.advance();
1816
- let value = '';
1817
- let escaped = false;
1818
- while (!this.isAtEnd()) {
1819
- const char = this.advance();
1820
- if (escaped) {
1821
- value += char;
1822
- escaped = false;
1823
- continue;
1824
- }
1825
- if (char === '\\') {
1826
- escaped = true;
1827
- continue;
1828
- }
1829
- if (char === quote) {
1830
- return new AbilityDSLToken(TokenTypes.STRING, value, startLine, startColumn);
1831
- }
1832
- value += char;
1833
- }
1834
- throw new Error(`Unterminated string at ${startLine}:${startColumn}`);
1835
- }
1836
- readNumber() {
1837
- const startLine = this.line;
1838
- const startColumn = this.column;
1839
- const start = this.pos;
1840
- while (!this.isAtEnd() && this.isDigit(this.peek())) {
1841
- this.advance();
1842
- }
1843
- const value = this.input.slice(start, this.pos);
1844
- return new AbilityDSLToken(TokenTypes.NUMBER, value, startLine, startColumn);
1845
- }
1846
- readSymbol() {
1847
- const startLine = this.line;
1848
- const startColumn = this.column;
1849
- const char = this.advance();
1850
- switch (char) {
1851
- case '.':
1852
- return new AbilityDSLToken(TokenTypes.DOT, char, startLine, startColumn);
1853
- case ':':
1854
- return new AbilityDSLToken(TokenTypes.COLON, char, startLine, startColumn);
1855
- case ',':
1856
- return new AbilityDSLToken(TokenTypes.COMMA, char, startLine, startColumn);
1857
- case '[':
1858
- return new AbilityDSLToken(TokenTypes.LBRACKET, char, startLine, startColumn);
1859
- case ']':
1860
- return new AbilityDSLToken(TokenTypes.RBRACKET, char, startLine, startColumn);
1861
- case '>':
1862
- if (this.peek() === '=') {
1863
- this.advance();
1864
- return new AbilityDSLToken(TokenTypes.SYMBOL, '>=', startLine, startColumn);
1865
- }
1866
- return new AbilityDSLToken(TokenTypes.SYMBOL, '>', startLine, startColumn);
1867
- case '<':
1868
- if (this.peek() === '=') {
1869
- this.advance();
1870
- return new AbilityDSLToken(TokenTypes.SYMBOL, '<=', startLine, startColumn);
1871
- }
1872
- if (this.peek() === '>') {
1873
- this.advance();
1874
- return new AbilityDSLToken(TokenTypes.SYMBOL, '<>', startLine, startColumn);
1875
- }
1876
- return new AbilityDSLToken(TokenTypes.SYMBOL, '<', startLine, startColumn);
1877
- case '=':
1878
- if (this.peek() === '=') {
1879
- this.advance();
1880
- return new AbilityDSLToken(TokenTypes.SYMBOL, '==', startLine, startColumn);
1881
- }
1882
- return new AbilityDSLToken(TokenTypes.SYMBOL, '=', startLine, startColumn);
1883
- case '!':
1884
- if (this.peek() === '=') {
1885
- this.advance();
1886
- return new AbilityDSLToken(TokenTypes.SYMBOL, '!=', startLine, startColumn);
1887
- }
1888
- throw new Error(`Unexpected symbol '!' at ${this.line}:${this.column}`);
1889
- default:
1890
- throw new Error(`Unknown symbol '${char}' at ${this.line}:${this.column}`);
1891
- }
1892
- }
1893
- readWord() {
1894
- const startLine = this.line;
1895
- const startColumn = this.column;
1896
- const start = this.pos;
1897
- // First segment
1898
- while (!this.isAtEnd() && /[a-zA-Z0-9_*]/.test(this.peek())) {
1899
- this.advance();
1900
- }
1901
- // dots segments
1902
- while (!this.isAtEnd() && this.peek() === '.') {
1903
- this.advance(); // dot
1904
- if (!/[a-zA-Z_*]/.test(this.peek())) {
1905
- break;
1906
- }
1907
- while (!this.isAtEnd() && /[a-zA-Z0-9_*]/.test(this.peek())) {
1908
- this.advance();
1909
- }
1910
- }
1911
- const word = this.input.slice(start, this.pos);
1912
- if (word === 'always') {
1913
- return new AbilityDSLToken(TokenTypes.ALWAYS, word, startLine, startColumn);
1914
- }
1915
- if (word === 'never') {
1916
- return new AbilityDSLToken(TokenTypes.NEVER, word, startLine, startColumn);
1917
- }
1918
- // (identifier or permission)
1919
- if (word.includes('.')) {
1920
- const last = this.tokens[this.tokens.length - 1];
1921
- if (last?.type === TokenTypes.EFFECT) {
1922
- if (word.startsWith('permission.')) {
1923
- return new AbilityDSLToken(TokenTypes.PERMISSION, word, startLine, startColumn);
1924
- }
1925
- }
1926
- return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
1927
- }
1928
- if (this.keywords.has(word)) {
1929
- if (word === 'permit' || word === 'allow') {
1930
- return new AbilityDSLToken(TokenTypes.EFFECT, 'permit', startLine, startColumn);
1931
- }
1932
- if (word === 'deny' || word === 'forbidden') {
1933
- return new AbilityDSLToken(TokenTypes.EFFECT, 'deny', startLine, startColumn);
1934
- }
1935
- if (word === 'all') {
1936
- return new AbilityDSLToken(TokenTypes.ALL, word, startLine, startColumn);
1937
- }
1938
- if (word === 'any') {
1939
- return new AbilityDSLToken(TokenTypes.ANY, word, startLine, startColumn);
1940
- }
1941
- if (word === 'of') {
1942
- return new AbilityDSLToken(TokenTypes.OF, word, startLine, startColumn);
1943
- }
1944
- if (word === 'if') {
1945
- return new AbilityDSLToken(TokenTypes.IF, word, startLine, startColumn);
1946
- }
1947
- if (word === 'true' || word === 'false') {
1948
- return new AbilityDSLToken(TokenTypes.BOOLEAN, word, startLine, startColumn);
1949
- }
1950
- if (word === 'null') {
1951
- return new AbilityDSLToken(TokenTypes.NULL, word, startLine, startColumn);
1952
- }
1953
- if (word === 'defined') {
1954
- return new AbilityDSLToken(TokenTypes.DEFINED, word, startLine, startColumn);
1955
- }
1956
- if (word === 'except') {
1957
- return new AbilityDSLToken(TokenTypes.EXCEPT, word, startLine, startColumn);
1958
- }
1959
- if (word === 'alias') {
1960
- return new AbilityDSLToken(TokenTypes.ALIAS, word, startLine, startColumn);
1961
- }
1962
- // Остальные ключевые слова (contains, in, equals, greater, less, not, is, or, than, equal)
1963
- return new AbilityDSLToken(TokenTypes.KEYWORD, word, startLine, startColumn);
1964
- }
1965
- // Если после EFFECT и нет точки — действие (например, "create")
1966
- const lastToken = this.tokens[this.tokens.length - 1];
1967
- if (lastToken?.type === TokenTypes.EFFECT) {
1968
- return new AbilityDSLToken(TokenTypes.PERMISSION, word, startLine, startColumn);
1969
- }
1970
- // Обычный идентификатор
1971
- return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
1972
- }
1973
- skipWhitespace() {
1974
- while (!this.isAtEnd() && /\s/.test(this.peek())) {
1975
- this.advance();
1976
- }
1977
- }
1978
- isDigit(char) {
1979
- return char >= '0' && char <= '9';
1980
- }
1981
- isAlpha(char) {
1982
- return /[a-zA-Z_]/.test(char);
1983
- }
1984
- isSymbol(char) {
1985
- return ['.', ':', ',', '[', ']', '>', '<', '=', '!'].includes(char);
1986
- }
1987
- isNewline() {
1988
- return this.peek() === '\n';
1989
- }
1990
- peek() {
1991
- return this.input[this.pos];
1992
- }
1993
- advance() {
1994
- const ch = this.input[this.pos++];
1995
- if (ch === '\n') {
1996
- this.line++;
1997
- this.column = 1;
1998
- }
1999
- else {
2000
- this.column++;
2001
- }
2002
- return ch;
2003
- }
2004
- isAtEnd() {
2005
- return this.pos >= this.input.length;
2006
- }
2007
- }
2008
-
2009
- class AbilityDSLSyntaxError extends Error {
2010
- line;
2011
- column;
2012
- context;
2013
- details;
2014
- _formattedMessage;
2015
- _originalStack;
2016
- constructor(line, column, context, // строка DSL + ^ + соседние строки
2017
- details) {
2018
- super(details.split('\n')[0]); // message = только первая строка
2019
- this.line = line;
2020
- this.column = column;
2021
- this.context = context;
2022
- this.details = details;
2023
- this.name = 'AbilityDSLSyntaxError';
2024
- if (Error.captureStackTrace) {
2025
- Error.captureStackTrace(this, AbilityDSLSyntaxError);
2026
- }
2027
- this._originalStack = this.stack;
2028
- this._formattedMessage = this.formatMessage();
2029
- Object.defineProperty(this, 'stack', {
2030
- get: () => this._formattedMessage,
2031
- configurable: true,
2032
- });
2033
- }
2034
- static supportsColor() {
2035
- return typeof process !== 'undefined' && process.stdout?.isTTY;
2036
- }
2037
- formatMessage() {
2038
- const useColor = AbilityDSLSyntaxError.supportsColor();
2039
- const BOLD = useColor ? '\x1b[1m' : '';
2040
- const RED = useColor ? '\x1b[31m' : '';
2041
- const ORANGE = useColor ? '\x1b[33;1m' : '';
2042
- const GRAY = useColor ? '\x1b[90m' : '';
2043
- const RESET = useColor ? '\x1b[0m' : '';
2044
- const lines = this.context.split('\n');
2045
- // Find line with ^
2046
- const pointerIndex = lines.findIndex(l => l.includes('^') || l.includes('~'));
2047
- const commentIndex = lines.findIndex(l => l.trim().includes('#'));
2048
- const formattedLines = lines.map((line, idx) => {
2049
- if (idx === pointerIndex - 1) {
2050
- // Error line
2051
- return `${BOLD}${ORANGE}${line}${RESET}`;
2052
- }
2053
- if (idx === pointerIndex) {
2054
- // Error with ~~~~~
2055
- return `${RED}${line}${RESET}`;
2056
- }
2057
- // Comments # ...
2058
- if (idx === commentIndex) {
2059
- return `${GRAY}${line}${RESET}`;
2060
- }
2061
- return line;
2062
- });
2063
- const contextBlock = formattedLines.join('\n');
2064
- return `${BOLD}${RED}${this.name}: ${this.details}${RESET}\n\n` + contextBlock;
2065
- }
2066
- toString() {
2067
- return this._formattedMessage;
2068
- }
2069
- }
2070
-
2071
- class AbilityDSLTokenStream {
2072
- tokens;
2073
- pos = 0;
2074
- dsl;
2075
- marks = [];
2076
- lastToken = null;
2077
- next() {
2078
- const token = this.tokens[this.pos++];
2079
- this.lastToken = token;
2080
- return token;
2081
- }
2082
- prev() {
2083
- if (this.pos === 0) {
2084
- return null;
2085
- }
2086
- const token = this.tokens[this.pos--];
2087
- this.lastToken = token;
2088
- return token;
2089
- }
2090
- lookPrev() {
2091
- return this.lastToken;
2092
- }
2093
- constructor(tokens, dsl) {
2094
- this.tokens = tokens;
2095
- this.dsl = dsl;
2096
- }
2097
- peek() {
2098
- return this.tokens[this.pos];
2099
- }
2100
- eof() {
2101
- return this.peek().type === TokenTypes.EOF;
2102
- }
2103
- check(type) {
2104
- if (this.eof()) {
2105
- return false;
2106
- }
2107
- const p = this.peek().type;
2108
- return p === type;
2109
- }
2110
- match(type) {
2111
- if (this.check(type)) {
2112
- return this.next();
2113
- }
2114
- return null;
2115
- }
2116
- expect(type, message) {
2117
- const token = this.peek();
2118
- if (token && token.type === type) {
2119
- return this.next();
2120
- }
2121
- this.syntaxError(message, token, [type]);
2122
- }
2123
- expectOneOf(types, message) {
2124
- const token = this.peek();
2125
- for (const t of types) {
2126
- if (token && token.type === t) {
2127
- return this.next();
2128
- }
2129
- }
2130
- this.syntaxError(message, token, types);
2131
- }
2132
- mark() {
2133
- this.marks.push(this.pos);
2134
- }
2135
- reset() {
2136
- const pos = this.marks.pop();
2137
- if (pos !== undefined) {
2138
- this.pos = pos;
2139
- }
2140
- }
2141
- commit() {
2142
- this.marks.pop();
2143
- }
2144
- syntaxError(details, token, expected) {
2145
- const lines = this.dsl.split(/\r?\n/);
2146
- const lineIdx = token.line - 1;
2147
- const lineBefore = lineIdx > 0 ? lines[lineIdx - 1] : '';
2148
- const current = lines[lineIdx];
2149
- const linesAfter = lineIdx + 1 < lines.length ? lines[lineIdx + 1] : '';
2150
- const wave = ' '.repeat(Math.max(0, token.column - 1)) + '~'.repeat(token.value.length);
2151
- const lineNumWidth = String(token.line + 1).length;
2152
- const num = (n) => String(n).padStart(lineNumWidth, ' ');
2153
- let context = '';
2154
- if (lineBefore.trim() !== '') {
2155
- context += `${num(token.line - 1)} | ${lineBefore}\n`;
2156
- }
2157
- context += `${num(token.line)} | ${current}\n`;
2158
- context += `${' '.repeat(lineNumWidth)} | ${wave}\n`;
2159
- if (linesAfter.trim() !== '') {
2160
- context += `${num(token.line + 1)} | ${linesAfter}`;
2161
- }
2162
- let finalDetails = details;
2163
- if (expected && expected?.length > 0) {
2164
- const actual = token.value;
2165
- const suggestion = this.suggest(actual, expected);
2166
- const detailsMsg = `${details}\nDetails: Unexpected value token \`${actual}\``;
2167
- finalDetails = suggestion ? `${detailsMsg} Did you mean \`${suggestion}\`?` : detailsMsg;
2168
- }
2169
- throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
2170
- }
2171
- suggest(actual, expectedTypes) {
2172
- const candidates = [];
2173
- for (const type of expectedTypes) {
2174
- candidates.push(type);
2175
- }
2176
- const uniqueCandidates = [...new Set(candidates)];
2177
- let best = null;
2178
- let bestDist = 3;
2179
- for (const candidate of uniqueCandidates) {
2180
- const d = this.levenshteinDistance(actual.toLowerCase(), candidate.toLowerCase());
2181
- if (d < bestDist) {
2182
- bestDist = d;
2183
- best = candidate;
2184
- }
2185
- }
2186
- return best;
2187
- }
2188
- levenshteinDistance(a, b) {
2189
- const matrix = Array.from({ length: b.length + 1 }, () => Array.from({ length: a.length + 1 }, () => 0));
2190
- for (let i = 0; i <= a.length; i++)
2191
- matrix[0][i] = i;
2192
- for (let j = 0; j <= b.length; j++)
2193
- matrix[j][0] = j;
2194
- for (let j = 1; j <= b.length; j++) {
2195
- for (let i = 1; i <= a.length; i++) {
2196
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2197
- matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + cost);
2198
- }
2199
- }
2200
- return matrix[b.length][a.length];
2201
- }
2202
- }
2203
-
2204
- class AbilityDSLAnnotations {
2205
- store = {
2206
- id: undefined,
2207
- name: undefined,
2208
- priority: undefined,
2209
- description: undefined,
2210
- disabled: undefined,
2211
- tags: undefined,
2212
- };
2213
- get(key) {
2214
- return this.store[key] ?? null;
2215
- }
2216
- set(key, value, token) {
2217
- if (value === null) {
2218
- this.store[key] = undefined;
2219
- }
2220
- else {
2221
- this.store[key] = {
2222
- key,
2223
- value,
2224
- token,
2225
- };
2226
- }
2227
- return this;
2228
- }
2229
- clear() {
2230
- for (const key of Object.keys(this.store)) {
2231
- this.store[key] = undefined;
2232
- }
2233
- }
2234
- clone() {
2235
- const cloned = new AbilityDSLAnnotations();
2236
- for (const key of Object.keys(this.store)) {
2237
- const entry = this.store[key];
2238
- cloned.store[key] = entry
2239
- ? { ...entry }
2240
- : undefined;
2241
- }
2242
- return cloned;
2243
- }
2244
- // convenience getters
2245
- get id() { return this.get('id'); }
2246
- get name() { return this.get('name'); }
2247
- get description() { return this.get('description'); }
2248
- get priority() { return this.get('priority'); }
2249
- get disabled() { return this.get('disabled'); }
2250
- get tags() { return this.get('tags'); }
2251
- // convenience setters
2252
- setID(v, t) { return this.set('id', v, t); }
2253
- setName(v, t) { return this.set('name', v, t); }
2254
- setDescription(v, t) { return this.set('description', v, t); }
2255
- setPriority(v, t) { return this.set('priority', v, t); }
2256
- setDisabled(v, t) { return this.set('disabled', v, t); }
2257
- setTags(v, t) { return this.set('tags', v, t); }
2258
- }
2259
-
2260
- const AnnotationAllowed = {
2261
- policy: new Set(["id", "name", "description", "priority", "disabled", "tags"]),
2262
- ruleSet: new Set(["id", "name", "description", "disabled"]),
2263
- rule: new Set(["id", "name", "disabled"]),
2264
- alias: new Set(["name", "disabled"]),
2265
- };
2266
-
2267
- class AbilityDSLAliases {
2268
- store = new Map();
2269
- get(alias) {
2270
- return this.store.get(alias) || null;
2271
- }
2272
- set(alias, rule) {
2273
- this.store.set(alias, rule);
2274
- return this;
2275
- }
2276
- has(alias) {
2277
- return this.store.has(alias);
2278
- }
2279
- }
2280
-
2281
- /**
2282
- * Parser for the Ability DSL.
2283
- *
2284
- * Converts a DSL string into one or more AbilityPolicy instances.
2285
- * The grammar follows the structure:
2286
- *
2287
- * <effect> <permission> if <group> [ <group> ... ]
2288
- *
2289
- * where <group> is either "all of:" or "any of:", followed by a colon,
2290
- * and then a list of rules (one per line).
2291
- *
2292
- * Each rule is: <identifier> <operator> <value>
2293
- *
2294
- * Operators can be simple (equals, contains, in) or
2295
- * composed (is null, is not null, greater than, less than or equal, etc.).
2296
- */
2297
- class AbilityDSLParser {
2298
- dsl;
2299
- stream;
2300
- annBuffer = new AbilityDSLAnnotations();
2301
- aliasBuffer = new AbilityDSLAliases();
2302
- constructor(dsl) {
2303
- this.dsl = dsl;
2304
- }
2305
- /**
2306
- * Main entry point: tokenize the input and parse all policies.
2307
- * @returns Array of AbilityPolicy instances.
2308
- */
2309
- parse() {
2310
- this.annBuffer.clear();
2311
- // 1. Лексер → токены
2312
- const tokens = new AbilityDSLLexer(this.dsl).tokenize();
2313
- // 2. Создаём TokenStream
2314
- this.stream = new AbilityDSLTokenStream(tokens, this.dsl);
2315
- const policies = [];
2316
- while (!this.stream.eof()) {
2317
- this.consumeLeadingComments();
2318
- this.consumeLeadingAnnotations();
2319
- this.consumeLeadingAliases();
2320
- if (!this.isStartOfPolicy()) {
2321
- const token = this.stream.peek();
2322
- this.stream.syntaxError(`Expected policy, got ${token.type}.`, token, [TokenTypes.EFFECT]);
2323
- }
2324
- policies.push(this.parsePolicy());
2325
- }
2326
- return policies;
2327
- }
2328
- // -------------------------------------------------------------------------
2329
- // #region Policy parsing
2330
- // -------------------------------------------------------------------------
2331
- /**
2332
- * Parses a single policy from the current token position.
2333
- *
2334
- * Grammar:
2335
- * policy = EFFECT PERMISSION IF (ALL | ANY) COLON ruleSets
2336
- */
2337
- parsePolicy() {
2338
- this.consumeLeadingComments();
2339
- this.consumeLeadingAnnotations();
2340
- this.consumeLeadingAliases();
2341
- const annotations = this.takeAnnotations('policy');
2342
- // Effect: "permit" or "deny"
2343
- const effectToken = this.stream.expect(TokenTypes.EFFECT, 'Expected effect');
2344
- const effect = effectToken.value;
2345
- // Permission: e.g. "order.update"
2346
- const permissionToken = this.stream.expect(TokenTypes.PERMISSION, 'Expected permission');
2347
- const permission = permissionToken.value;
2348
- if (!permission.startsWith('permission.')) {
2349
- return this.stream.syntaxError(`Unexpected token. The permission key, must be starts with prefix \`permission.\`, but got \`${permission}\`.\nDid you mean \`permission.${permission}\`?`, permissionToken);
2350
- }
2351
- // "if" keyword
2352
- this.stream.expect(TokenTypes.IF, 'Expected "if"');
2353
- // Group selector: "all" or "any" – determines how the top‑level rule sets are combined.
2354
- const compareToken = this.stream.expectOneOf([TokenTypes.ALL, TokenTypes.ANY], 'Expected "all" or "any"');
2355
- const compareMethod = compareToken.type === TokenTypes.ALL ? AbilityCompare.and : AbilityCompare.or;
2356
- // Colon after the group keyword
2357
- this.stream.expect(TokenTypes.COLON, 'Expected ":"');
2358
- // Parse the list of rule sets (each "all of:" or "any of:" block)
2359
- const ruleSets = this.parseRuleSets(compareMethod);
2360
- // Construct the policy instance.
2361
- return new AbilityPolicy({
2362
- id: annotations.id?.value || null,
2363
- name: annotations.name?.value || null,
2364
- description: annotations.description?.value || null,
2365
- priority: annotations.priority?.value || null,
2366
- permission: permission.replace(/^permission\./, ''),
2367
- effect: effect === 'permit' ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny,
2368
- disabled: annotations.disabled?.value ?? undefined,
2369
- tags: annotations.tags?.value ?? undefined,
2370
- compareMethod,
2371
- }).addRuleSets(ruleSets);
2372
- }
2373
- // -------------------------------------------------------------------------
2374
- // #region Rule set parsing (groups of rules)
2375
- // -------------------------------------------------------------------------
2376
- /**
2377
- * Parses a sequence of rule sets (groups) until a new policy starts or EOF.
2378
- */
2379
- parseRuleSets(policyCompareMethod) {
2380
- const sets = [];
2381
- this.consumeLeadingComments();
2382
- this.consumeLeadingAnnotations();
2383
- while (!this.stream.eof() && !this.isStartOfPolicy()) {
2384
- // maybe except ruleSet
2385
- if (this.isStartOfExcept()) {
2386
- sets.push(this.parseExceptGroup(policyCompareMethod));
2387
- continue;
2388
- }
2389
- // maybe ruleSet
2390
- if (this.isStartOfGroup()) {
2391
- sets.push(this.parseGroup());
2392
- continue;
2393
- }
2394
- // implicit ruleSet
2395
- // if (!this.isStartOfRule()) {
2396
- // this.consumeLeadingComments();
2397
- // this.consumeLeadingAnnotations();
2398
- // }
2399
- // is implicit group
2400
- // const annotation = this.takeAnnotations('ruleSet');
2401
- const group = new AbilityRuleSet({
2402
- // id: annotation.id?.value || null,
2403
- compareMethod: policyCompareMethod,
2404
- // name: annotation.name?.value ?? null,
2405
- // description: annotation.description?.value || null,
2406
- // disabled: annotation.disabled?.value ?? undefined,
2407
- });
2408
- // Read rules of implicit-группы
2409
- while (!this.stream.eof()) {
2410
- this.consumeLeadingComments();
2411
- this.consumeLeadingAnnotations();
2412
- if (this.isStartOfGroup() || this.isStartOfPolicy() || this.isStartOfExcept()) {
2413
- break;
2414
- }
2415
- if (this.stream.check(TokenTypes.IDENTIFIER) ||
2416
- this.stream.check(TokenTypes.ALWAYS) ||
2417
- this.stream.check(TokenTypes.NEVER)) {
2418
- group.addRule(this.parseRule());
2419
- }
2420
- else {
2421
- this.stream.syntaxError(`Unexpected token in implicit group: ${this.stream.peek().type}`, this.stream.peek());
2422
- }
2423
- }
2424
- sets.push(group);
2425
- }
2426
- return sets;
2427
- }
2428
- /**
2429
- * Parses a single group, e.g. "all of:" or "any of:", and returns a RuleSet.
2430
- */
2431
- parseGroup() {
2432
- this.consumeLeadingComments();
2433
- this.consumeLeadingAnnotations();
2434
- const annotations = this.takeAnnotations('ruleSet');
2435
- const compareToken = this.stream.expectOneOf([TokenTypes.ALL, TokenTypes.ANY, TokenTypes.ALWAYS, TokenTypes.NEVER], 'Expected "all" or "any" or "always" or "never"');
2436
- const compareMethod = compareToken.type === TokenTypes.ALL ? AbilityCompare.and : AbilityCompare.or;
2437
- if (this.stream.check(TokenTypes.OF)) {
2438
- this.stream.next();
2439
- }
2440
- this.stream.expect(TokenTypes.COLON, 'Expected ":"');
2441
- const group = new AbilityRuleSet({
2442
- id: annotations.id?.value || null,
2443
- compareMethod,
2444
- name: annotations.name?.value || null,
2445
- description: annotations.description?.value || null,
2446
- disabled: annotations.disabled?.value ?? undefined,
2447
- });
2448
- while (!this.stream.eof()) {
2449
- this.consumeLeadingComments();
2450
- this.consumeLeadingAnnotations();
2451
- if (this.isStartOfExcept()) {
2452
- break;
2453
- }
2454
- if (this.isStartOfGroup() || this.isStartOfPolicy()) {
2455
- break;
2456
- }
2457
- if (this.stream.check(TokenTypes.IDENTIFIER)) {
2458
- group.addRule(this.parseRule());
2459
- }
2460
- else {
2461
- this.stream.syntaxError(`Unexpected token in group: ${this.stream.peek().type}`, this.stream.peek());
2462
- }
2463
- }
2464
- return group;
2465
- }
2466
- // -------------------------------------------------------------------------
2467
- // #region Except RuleSet parsing
2468
- // -------------------------------------------------------------------------
2469
- parseExceptGroup(policyCompareMethod) {
2470
- this.consumeLeadingComments();
2471
- this.consumeLeadingAnnotations();
2472
- const annotations = this.takeAnnotations('ruleSet');
2473
- // consume "except"
2474
- this.stream.expect(TokenTypes.EXCEPT, 'Expected "except"');
2475
- let compareMethod = policyCompareMethod;
2476
- // optional: "all" / "any"
2477
- if (this.stream.check(TokenTypes.ALL) || this.stream.check(TokenTypes.ANY)) {
2478
- const compareToken = this.stream.next();
2479
- compareMethod = compareToken.type === TokenTypes.ALL ? AbilityCompare.and : AbilityCompare.or;
2480
- if (this.stream.check(TokenTypes.OF)) {
2481
- this.stream.next();
2482
- }
2483
- this.stream.expect(TokenTypes.COLON, 'Expected ":" after except group');
2484
- }
2485
- else {
2486
- // implicit except group — no "all/any of:"
2487
- // but still must end with colon
2488
- this.stream.expect(TokenTypes.COLON, 'Expected ":" after "except"');
2489
- }
2490
- const group = new AbilityRuleSet({
2491
- id: annotations.id?.value || null,
2492
- compareMethod,
2493
- name: annotations.name?.value || null,
2494
- description: annotations.description?.value || null,
2495
- isExcept: true,
2496
- disabled: annotations.disabled?.value ?? undefined,
2497
- });
2498
- // read rules
2499
- while (!this.stream.eof()) {
2500
- this.consumeLeadingComments();
2501
- this.consumeLeadingAnnotations();
2502
- if (this.isStartOfGroup() || this.isStartOfPolicy() || this.isStartOfExcept()) {
2503
- break;
2504
- }
2505
- if (this.stream.check(TokenTypes.IDENTIFIER)) {
2506
- group.addRule(this.parseRule());
2507
- }
2508
- else {
2509
- this.stream.syntaxError(`Unexpected token in except group: ${this.stream.peek().type}`, this.stream.peek());
2510
- }
2511
- }
2512
- return group;
2513
- }
2514
- // -------------------------------------------------------------------------
2515
- // #region Rule parsing
2516
- // -------------------------------------------------------------------------
2517
- /**
2518
- * Parses a single rule: subject operator value
2519
- */
2520
- parseRule() {
2521
- this.consumeLeadingComments();
2522
- this.consumeLeadingAnnotations();
2523
- const annotations = this.takeAnnotations('rule');
2524
- const isNeverAlways = this.stream.check(TokenTypes.ALWAYS) || this.stream.check(TokenTypes.NEVER);
2525
- if (!isNeverAlways && !this.stream.check(TokenTypes.IDENTIFIER)) {
2526
- this.stream.syntaxError(`Expected identifier, but got ${this.stream.peek().type}`, this.stream.peek());
2527
- }
2528
- // subject
2529
- const subject = isNeverAlways
2530
- ? ''
2531
- : this.stream.expect(TokenTypes.IDENTIFIER, 'Expected field').value;
2532
- // check alias
2533
- if (this.aliasBuffer.has(subject)) {
2534
- return this.aliasBuffer.get(subject);
2535
- }
2536
- // operator
2537
- const { condition, operator } = this.parseConditionOperator();
2538
- // value
2539
- let resource = null;
2540
- let valueToken = null;
2541
- const operatorConsumesValue = operator !== TokenTypes.EQ_NULL &&
2542
- operator !== TokenTypes.NOT_EQ_NULL &&
2543
- operator !== TokenTypes.NULL &&
2544
- operator !== TokenTypes.DEFINED &&
2545
- operator !== TokenTypes.ALWAYS &&
2546
- operator !== TokenTypes.NEVER;
2547
- if (operatorConsumesValue) {
2548
- this.stream.mark();
2549
- resource = this.parseValue();
2550
- valueToken = this.stream.lookPrev();
2551
- this.stream.commit();
2552
- }
2553
- this.consumeLeadingComments();
2554
- this.consumeLeadingAnnotations();
2555
- this.consumeLeadingAliases();
2556
- // validation: identifier without dot → error
2557
- if (typeof resource === 'string' &&
2558
- valueToken &&
2559
- valueToken.type === TokenTypes.IDENTIFIER &&
2560
- !valueToken.value.includes('.')) {
2561
- this.stream.syntaxError(`Expected comparison operator or value, got \`${resource}\``, valueToken, [TokenTypes.KEYWORD]);
2562
- }
2563
- return new AbilityRule({
2564
- id: annotations.id?.value || null,
2565
- subject,
2566
- resource,
2567
- condition,
2568
- name: annotations.name?.value || null,
2569
- description: annotations.description?.value || null,
2570
- disabled: annotations.disabled?.value ?? undefined,
2571
- });
2572
- }
2573
- // -------------------------------------------------------------------------
2574
- // #region Operator parsing
2575
- // -------------------------------------------------------------------------
2576
- /**
2577
- * Parses the comparison operator part of a rule.
2578
- * Returns both the resulting AbilityCondition and the token type that was consumed.
2579
- */
2580
- parseConditionOperator() {
2581
- // "always"
2582
- this.stream.mark();
2583
- if (this.matchWord('always')) {
2584
- this.stream.commit();
2585
- return { condition: AbilityCondition.always, operator: TokenTypes.ALWAYS };
2586
- }
2587
- this.stream.reset();
2588
- // "never"
2589
- this.stream.mark();
2590
- if (this.matchWord('never')) {
2591
- this.stream.commit();
2592
- return { condition: AbilityCondition.never, operator: TokenTypes.NEVER };
2593
- }
2594
- this.stream.reset();
2595
- // "length equals"
2596
- this.stream.mark();
2597
- if ((this.matchWord('length') || this.matchWord('len')) && this.matchWord('equals')) {
2598
- this.stream.commit();
2599
- return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
2600
- }
2601
- this.stream.reset();
2602
- // "length ="
2603
- this.stream.mark();
2604
- if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('=')) {
2605
- this.stream.commit();
2606
- return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
2607
- }
2608
- this.stream.reset();
2609
- // "length greater than"
2610
- this.stream.mark();
2611
- if ((this.matchWord('length') || this.matchWord('len')) &&
2612
- this.matchWord('greater') &&
2613
- this.matchWord('than')) {
2614
- this.stream.commit();
2615
- return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
2616
- }
2617
- this.stream.reset();
2618
- // "length >"
2619
- this.stream.mark();
2620
- if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('>')) {
2621
- this.stream.commit();
2622
- return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
2623
- }
2624
- this.stream.reset();
2625
- // "length less than"
2626
- this.stream.mark();
2627
- if ((this.matchWord('length') || this.matchWord('len')) &&
2628
- this.matchWord('less') &&
2629
- this.matchWord('than')) {
2630
- this.stream.commit();
2631
- return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
2632
- }
2633
- this.stream.reset();
2634
- // "length <"
2635
- this.stream.mark();
2636
- if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('<')) {
2637
- this.stream.commit();
2638
- return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
2639
- }
2640
- this.stream.reset();
2641
- // "greater than or equal"
2642
- this.stream.mark();
2643
- if (this.matchWord('greater') &&
2644
- this.matchWord('than') &&
2645
- this.matchWord('or') &&
2646
- this.matchWord('equal')) {
2647
- this.stream.commit();
2648
- return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
2649
- }
2650
- this.stream.reset();
2651
- // greater than
2652
- this.stream.mark();
2653
- if (this.matchWord('greater') && this.matchWord('than')) {
2654
- this.stream.commit();
2655
- return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
2656
- }
2657
- this.stream.reset();
2658
- // less than or equal
2659
- this.stream.mark();
2660
- if (this.matchWord('less') &&
2661
- this.matchWord('than') &&
2662
- this.matchWord('or') &&
2663
- this.matchWord('equal')) {
2664
- this.stream.commit();
2665
- return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
2666
- }
2667
- this.stream.reset();
2668
- // less than
2669
- if (this.matchWord('less') && this.matchWord('than')) {
2670
- return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
2671
- }
2672
- this.stream.reset();
2673
- // "not contains"
2674
- this.stream.mark();
2675
- if (this.matchWord('not') && this.matchWord('contains')) {
2676
- this.stream.commit();
2677
- return {
2678
- condition: AbilityCondition.not_contains,
2679
- operator: TokenTypes.NOT_CONTAINS,
2680
- };
2681
- }
2682
- this.stream.reset();
2683
- // "not includes"
2684
- this.stream.mark();
2685
- if (this.matchWord('not') && this.matchWord('includes')) {
2686
- this.stream.commit();
2687
- return {
2688
- condition: AbilityCondition.not_contains,
2689
- operator: TokenTypes.NOT_CONTAINS,
2690
- };
2691
- }
2692
- this.stream.reset();
2693
- // "not includes"
2694
- this.stream.mark();
2695
- if (this.matchWord('not') && this.matchWord('has')) {
2696
- this.stream.commit();
2697
- return {
2698
- condition: AbilityCondition.not_contains,
2699
- operator: TokenTypes.NOT_CONTAINS,
2700
- };
2701
- }
2702
- this.stream.reset();
2703
- // "is equals"
2704
- this.stream.mark();
2705
- if (this.matchWord('is') && this.matchWord('equals')) {
2706
- this.stream.commit();
2707
- return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
2708
- }
2709
- this.stream.reset();
2710
- // not equal
2711
- this.stream.mark();
2712
- if (this.matchWord('not') && this.matchWord('equals')) {
2713
- this.stream.commit();
2714
- return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
2715
- }
2716
- this.stream.reset();
2717
- // is not equals
2718
- this.stream.mark();
2719
- if (this.matchWord('is') && this.matchWord('not') && this.matchWord('equals')) {
2720
- this.stream.commit();
2721
- return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
2722
- }
2723
- this.stream.reset();
2724
- // is in
2725
- this.stream.mark();
2726
- if (this.matchWord('is') && this.matchWord('in')) {
2727
- this.stream.commit();
2728
- return { condition: AbilityCondition.in, operator: TokenTypes.IN };
2729
- }
2730
- this.stream.reset();
2731
- // not in
2732
- this.stream.mark();
2733
- if (this.matchWord('not') && this.matchWord('in')) {
2734
- this.stream.commit();
2735
- return { condition: AbilityCondition.not_in, operator: TokenTypes.NOT_IN };
2736
- }
2737
- this.stream.reset();
2738
- // is not null
2739
- this.stream.mark();
2740
- if (this.matchWord('is') && this.matchWord('not')) {
2741
- if (this.stream.check(TokenTypes.NULL)) {
2742
- this.stream.next();
2743
- this.stream.commit();
2744
- return {
2745
- condition: AbilityCondition.not_equals,
2746
- operator: TokenTypes.NOT_EQ_NULL,
2747
- };
2748
- }
2749
- }
2750
- this.stream.reset();
2751
- // is null
2752
- this.stream.mark();
2753
- if (this.matchWord('is') && this.matchWord('null')) {
2754
- if (this.stream.check(TokenTypes.NULL)) {
2755
- this.stream.commit();
2756
- this.stream.next();
2757
- return {
2758
- condition: AbilityCondition.equals,
2759
- operator: TokenTypes.EQ_NULL,
2760
- };
2761
- }
2762
- }
2763
- this.stream.reset();
2764
- // is defined
2765
- this.stream.mark();
2766
- if (this.matchWord('is') && this.matchWord('defined')) {
2767
- this.stream.commit();
2768
- return { condition: AbilityCondition.defined, operator: TokenTypes.DEFINED };
2769
- }
2770
- this.stream.reset();
2771
- // is not defined
2772
- this.stream.mark();
2773
- if (this.matchWord('is') && this.matchWord('not') && this.matchWord('defined')) {
2774
- this.stream.commit();
2775
- return { condition: AbilityCondition.not_defined, operator: TokenTypes.DEFINED };
2776
- }
2777
- this.stream.reset();
2778
- // Single token (symbol or keyword)
2779
- const token = this.stream.peek();
2780
- if (token.type !== TokenTypes.SYMBOL &&
2781
- token.type !== TokenTypes.KEYWORD &&
2782
- token.type !== TokenTypes.NULL) {
2783
- this.stream.syntaxError(`Expected comparison operator, got \`${token.value}\``, token, [
2784
- TokenTypes.SYMBOL,
2785
- TokenTypes.KEYWORD,
2786
- TokenTypes.NULL,
2787
- ]);
2788
- }
2789
- this.stream.next();
2790
- switch (token.type) {
2791
- case TokenTypes.SYMBOL:
2792
- if (token.value === '=' || token.value === '==')
2793
- return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
2794
- if (token.value === '!=' || token.value === '<>')
2795
- return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
2796
- if (token.value === '>')
2797
- return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
2798
- if (token.value === '<')
2799
- return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
2800
- if (token.value === '>=')
2801
- return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
2802
- if (token.value === '<=')
2803
- return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
2804
- break;
2805
- case TokenTypes.KEYWORD:
2806
- if (token.value === 'contains' || token.value === 'includes' || token.value === 'has')
2807
- return { condition: AbilityCondition.contains, operator: TokenTypes.CONTAINS };
2808
- if (token.value === 'in')
2809
- return { condition: AbilityCondition.in, operator: TokenTypes.IN };
2810
- if (token.value === 'equals')
2811
- return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
2812
- if (token.value === 'gte') {
2813
- return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
2814
- }
2815
- if (token.value === 'greater' || token.value === 'gt') {
2816
- // If we come here, it means "greater" without "than" – treat as '>'
2817
- return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
2818
- }
2819
- if (token.value === 'less' || token.value === 'lt') {
2820
- return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
2821
- }
2822
- if (token.value === 'lte') {
2823
- return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
2824
- }
2825
- if (token.value === 'is') {
2826
- // "is" alone -> equals
2827
- return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
2828
- }
2829
- break;
2830
- }
2831
- return this.stream.syntaxError(`Unexpected operator token \`${token.value}\``, token, [
2832
- TokenTypes.SYMBOL,
2833
- TokenTypes.KEYWORD,
2834
- ]);
2835
- }
2836
- /**
2837
- * Helper to match and consume a specific word token (KEYWORD or IDENTIFIER).
2838
- * @param word The exact string to look for.
2839
- * @returns True if the next token has that value.
2840
- */
2841
- matchWord(word) {
2842
- if (this.stream.eof()) {
2843
- return false;
2844
- }
2845
- const token = this.stream.peek();
2846
- if ((token.type === TokenTypes.KEYWORD ||
2847
- token.type === TokenTypes.IDENTIFIER ||
2848
- token.type === TokenTypes.ALWAYS ||
2849
- token.type === TokenTypes.NEVER ||
2850
- token.type === TokenTypes.DEFINED) &&
2851
- token.value === word) {
2852
- this.stream.next();
2853
- return true;
2854
- }
2855
- return false;
2856
- }
2857
- matchSymbol(symbol) {
2858
- if (this.stream.eof())
2859
- return false;
2860
- const token = this.stream.peek();
2861
- if (token.type === TokenTypes.SYMBOL && token.value === symbol) {
2862
- this.stream.next();
2863
- return true;
2864
- }
2865
- return false;
2866
- }
2867
- // -------------------------------------------------------------------------
2868
- // #region Value parsing (literals, arrays, identifiers)
2869
- // -------------------------------------------------------------------------
2870
- /**
2871
- * Parses a resource value. Can be a string literal, number, boolean,
2872
- * null, a path (identifier), or an array.
2873
- */
2874
- parseValue() {
2875
- // Arrays start with a left bracket
2876
- if (this.stream.check(TokenTypes.LBRACKET)) {
2877
- this.stream.next();
2878
- return this.parseArray();
2879
- }
2880
- // Ensure we are not about to read a structural token as a value.
2881
- const token = this.stream.peek();
2882
- if (token.type === TokenTypes.ALL ||
2883
- token.type === TokenTypes.ANY ||
2884
- token.type === TokenTypes.EFFECT) {
2885
- this.stream.syntaxError(`Unexpected ${token.type} in value position`, token);
2886
- }
2887
- this.stream.next();
2888
- // if (token.type === TokenTypes.IDENTIFIER) {
2889
- // this.stream.next();
2890
- //
2891
- // return this.parseValue();
2892
- // }
2893
- // CHECK THIS SWITCH COMPARE
2894
- switch (token.type) {
2895
- case TokenTypes.STRING:
2896
- return token.value;
2897
- case TokenTypes.NUMBER:
2898
- return Number(token.value);
2899
- case TokenTypes.BOOLEAN:
2900
- return token.value === 'true';
2901
- case TokenTypes.NULL:
2902
- return null;
2903
- case TokenTypes.DEFINED:
2904
- return typeof token.value !== 'undefined';
2905
- case TokenTypes.IDENTIFIER:
2906
- return null;
2907
- default: {
2908
- this.stream.syntaxError(`Unexpected value token "${token.value}"`, token, [
2909
- TokenTypes.KEYWORD,
2910
- ]);
2911
- }
2912
- }
2913
- }
2914
- /**
2915
- * Parses an array literal: [ <value>, <value>, ... ]
2916
- * The opening bracket has already been consumed.
2917
- */
2918
- parseArray() {
2919
- const arr = [];
2920
- // Handle empty array
2921
- if (this.stream.check(TokenTypes.RBRACKET)) {
2922
- this.stream.next();
2923
- return arr;
2924
- }
2925
- while (!this.stream.eof() && !this.stream.check(TokenTypes.RBRACKET)) {
2926
- const value = this.parseValue();
2927
- // Flatten nested arrays if they appear (though grammar doesn't currently allow nesting).
2928
- if (Array.isArray(value)) {
2929
- arr.push(...value);
2930
- }
2931
- else if (typeof value === 'string' ||
2932
- typeof value === 'number' ||
2933
- typeof value === 'boolean') {
2934
- arr.push(value);
2935
- }
2936
- else if (value === null) {
2937
- // Null is allowed in arrays? Currently, we throw.
2938
- this.stream.syntaxError('Unexpected null in array', this.stream.peek());
2939
- }
2940
- // Optional comma between elements
2941
- if (this.stream.check(TokenTypes.COMMA)) {
2942
- this.stream.next();
2943
- }
2944
- }
2945
- this.stream.expect(TokenTypes.RBRACKET, 'Expected "]"');
2946
- return arr;
2947
- }
2948
- // -------------------------------------------------------------------------
2949
- // #region comments
2950
- // -------------------------------------------------------------------------
2951
- consumeLeadingComments() {
2952
- while (this.stream.check(TokenTypes.COMMENT)) {
2953
- this.stream.next();
2954
- // this.processCommentToken(token);
2955
- }
2956
- }
2957
- // private _consumeLeadingAnnotations() {
2958
- // while (this.stream.check(TokenTypes.ANNOTATION)) {
2959
- // const token = this.stream.next();
2960
- // this.processAnnotationToken(token);
2961
- // }
2962
- // }
2963
- consumeLeadingAliases() {
2964
- while (this.stream.check(TokenTypes.ALIAS)) {
2965
- this.stream.next(); // consume "alias"
2966
- const nameToken = this.stream.expect(TokenTypes.IDENTIFIER, `Expected alias name`);
2967
- const aliasKey = nameToken.value;
2968
- this.stream.expect(TokenTypes.COLON, `Expected colon after an alias`);
2969
- const annotations = this.takeAnnotations('alias');
2970
- while (!this.stream.eof() && !this.isStartOfAlias() && !this.isStartOfPolicy()) {
2971
- const rule = this.parseRule();
2972
- rule.name = annotations.get('name')?.value || aliasKey;
2973
- rule.description = annotations.get('description')?.value;
2974
- if (annotations.get('disabled')?.value === true) {
2975
- rule.disabled = true;
2976
- }
2977
- this.aliasBuffer.set(aliasKey, rule);
2978
- }
2979
- }
2980
- }
2981
- consumeLeadingAnnotations() {
2982
- while (this.stream.check(TokenTypes.ANNOTATION)) {
2983
- const token = this.stream.next();
2984
- const text = token.value.trim();
2985
- if (text.startsWith('@id ')) {
2986
- this.annBuffer.setID(text.slice(4).trim(), token);
2987
- }
2988
- if (text.startsWith('@name ')) {
2989
- this.annBuffer.setName(text.slice(6).trim(), token);
2990
- }
2991
- if (text.startsWith('@description ')) {
2992
- this.annBuffer.setDescription(text.slice(13).trim(), token);
2993
- }
2994
- if (text.startsWith('@priority ')) {
2995
- this.annBuffer.setPriority(parseInt(text.slice(10).trim(), 10), token);
2996
- }
2997
- if (text.startsWith('@disabled')) {
2998
- const value = text.slice(9).trim();
2999
- this.annBuffer.setDisabled(value.length === 0 ? true : text.slice(9).trim() === 'true', token);
3000
- }
3001
- if (text.startsWith('@tags ')) {
3002
- const value = text
3003
- .slice(6)
3004
- .trim()
3005
- .split(',')
3006
- .map(tag => tag.trim());
3007
- this.annBuffer.setTags(value, token);
3008
- }
3009
- }
3010
- }
3011
- takeAnnotations(owner) {
3012
- const ann = this.annBuffer.clone();
3013
- this.annBuffer.clear();
3014
- const allowed = AnnotationAllowed[owner];
3015
- for (const key of Object.keys(ann['store'])) {
3016
- const entry = ann.get(key);
3017
- if (!entry)
3018
- continue;
3019
- if (!allowed.has(key)) {
3020
- this.stream.syntaxError(`Annotation @${key} is not allowed on ${owner}. Allowed: ${[...allowed]
3021
- .map(a => '@' + a)
3022
- .join(', ')}`, entry.token ?? this.stream.peek());
3023
- }
3024
- }
3025
- return ann;
3026
- }
3027
- // -------------------------------------------------------------------------
3028
- // #region Helpers
3029
- // -------------------------------------------------------------------------
3030
- isStartOfPolicy() {
3031
- return this.stream.check(TokenTypes.EFFECT);
3032
- }
3033
- isStartOfGroup() {
3034
- return this.stream.check(TokenTypes.ALL) || this.stream.check(TokenTypes.ANY);
3035
- }
3036
- isStartOfRule() {
3037
- return (this.stream.check(TokenTypes.IDENTIFIER) ||
3038
- this.stream.check(TokenTypes.ALWAYS) ||
3039
- this.stream.check(TokenTypes.NEVER));
3040
- }
3041
- isStartOfExcept() {
3042
- return this.stream.check(TokenTypes.EXCEPT);
3043
- }
3044
- isStartOfAlias() {
3045
- return this.stream.check(TokenTypes.ALIAS);
3046
- }
3047
- }
3048
-
3049
- function ability(strings, ...expr) {
3050
- const dsl = strings.reduce((acc, s, i) => acc + s + (expr[i] ?? ''), '');
3051
- return new AbilityDSLParser(dsl).parse();
3052
- }
3053
-
3054
- class AbilityStrategy {
3055
- policies;
3056
- matched;
3057
- constructor(policies) {
3058
- this.policies = policies;
3059
- this.matched = policies.filter(p => p.matchState === AbilityMatch.match);
3060
- }
3061
- matchedPolicies() {
3062
- return this.matched;
3063
- }
3064
- hasMatched() {
3065
- return this.matched.length > 0;
3066
- }
3067
- firstMatched() {
3068
- return this.matchedPolicies()[0] ?? null;
3069
- }
3070
- lastMatched() {
3071
- const list = this.matchedPolicies();
3072
- return list.length > 0 ? list[list.length - 1] : null;
3073
- }
3074
- firstDenied() {
3075
- return this.getDenyPolicies()[0] ?? null;
3076
- }
3077
- firstPermitted() {
3078
- return this.getPermitPolicies()[0] ?? null;
3079
- }
3080
- getPermitPolicies() {
3081
- return this.matched.filter(p => p.effect === AbilityPolicyEffect.permit);
3082
- }
3083
- getDenyPolicies() {
3084
- return this.matched.filter(p => p.effect === AbilityPolicyEffect.deny);
3085
- }
3086
- hasPermit() {
3087
- return this.getPermitPolicies().length > 0;
3088
- }
3089
- hasDeny() {
3090
- return this.getDenyPolicies().length > 0;
3091
- }
3092
- isAllowed() {
3093
- return this.evaluate() === AbilityPolicyEffect.permit;
3094
- }
3095
- isDenied() {
3096
- return this.evaluate() === AbilityPolicyEffect.deny;
3097
- }
3098
- }
3099
-
3100
- /**
3101
- * AllMustPermitStrategy
3102
- *
3103
- * This strategy requires *every applicable policy* to return "permit".
3104
- * If at least one policy returns "deny" or "not applicable", the final result is "deny".
3105
- *
3106
- * Use this strategy when:
3107
- * - You want strict, conservative access control.
3108
- * - All rules must explicitly allow the action.
3109
- *
3110
- * Example:
3111
- * Policies:
3112
- * P1 → permit
3113
- * P2 → permit
3114
- * P3 → deny
3115
- * Result: deny (because not all policies permitted)
3116
- */
3117
- class AllMustPermitStrategy extends AbilityStrategy {
3118
- _decisive = null;
3119
- evaluate() {
3120
- // 1. Нет совпавших политик → deny, но решающей политики нет
3121
- if (!this.hasMatched()) {
3122
- this._decisive = null;
3123
- return AbilityPolicyEffect.deny;
3124
- }
3125
- // 2. Если есть deny — она решающая
3126
- const deny = this.firstDenied();
3127
- if (deny) {
3128
- this._decisive = deny;
3129
- return AbilityPolicyEffect.deny;
3130
- }
3131
- this._decisive = this.firstPermitted();
3132
- return AbilityPolicyEffect.permit;
3133
- }
3134
- decisivePolicy() {
3135
- return this._decisive;
3136
- }
3137
- }
3138
-
3139
- /**
3140
- * AnyPermitStrategy
3141
- *
3142
- * This strategy returns "permit" as soon as *any* applicable policy permits the action.
3143
- * If no policy permits, the result is "deny".
3144
- *
3145
- * Use this strategy when:
3146
- * - You want optimistic access control.
3147
- * - A single positive rule should be enough to grant access.
3148
- *
3149
- * Example:
3150
- * Policies:
3151
- * P1 → deny
3152
- * P2 → permit
3153
- * P3 → deny
3154
- * Result: permit (because at least one policy permitted)
3155
- */
3156
- class AnyPermitStrategy extends AbilityStrategy {
3157
- _decisive = null;
3158
- evaluate() {
3159
- // 1. Если есть permit — он решающий
3160
- const permit = this.firstPermitted();
3161
- if (permit) {
3162
- this._decisive = permit;
3163
- return AbilityPolicyEffect.permit;
3164
- }
3165
- // 2. Нет permit → deny по умолчанию
3166
- this._decisive = null;
3167
- return AbilityPolicyEffect.deny;
3168
- }
3169
- decisivePolicy() {
3170
- return this._decisive;
3171
- }
3172
- }
3173
-
3174
- /**
3175
- * DenyOverridesStrategy
3176
- *
3177
- * This strategy gives absolute priority to "deny".
3178
- * If any applicable policy returns "deny", the final result is "deny".
3179
- * Otherwise, if at least one policy permits, the result is "permit".
3180
- *
3181
- * Use this strategy when:
3182
- * - Security is critical.
3183
- * - A single denial must block access.
3184
- *
3185
- * Example:
3186
- * Policies:
3187
- * P1 → permit
3188
- * P2 → deny
3189
- * P3 → permit
3190
- * Result: deny (because deny overrides everything)
3191
- */
3192
- class DenyOverridesStrategy extends AbilityStrategy {
3193
- _decisive = null;
3194
- evaluate() {
3195
- // 1. Если есть deny — он решающий
3196
- const deny = this.firstDenied();
3197
- if (deny) {
3198
- this._decisive = deny;
3199
- return AbilityPolicyEffect.deny;
3200
- }
3201
- // 2. Если есть permit — он решающий
3202
- const permit = this.firstPermitted();
3203
- if (permit) {
3204
- this._decisive = permit;
3205
- return AbilityPolicyEffect.permit;
3206
- }
3207
- // 3. Нет ни permit, ни deny → deny по умолчанию
3208
- this._decisive = null;
3209
- return AbilityPolicyEffect.deny;
3210
- }
3211
- decisivePolicy() {
3212
- return this._decisive;
3213
- }
3214
- }
3215
-
3216
- /**
3217
- * FirstMatchStrategy
3218
- *
3219
- * This strategy evaluates policies in order and returns the result of the *first applicable* policy.
3220
- * Remaining policies are ignored.
3221
- *
3222
- * Use this strategy when:
3223
- * - Policy order matters.
3224
- * - You want predictable, sequential rule evaluation.
3225
- *
3226
- * Example:
3227
- * Policies:
3228
- * P1 → not applicable
3229
- * P2 → permit
3230
- * P3 → deny
3231
- * Result: permit (P2 is the first applicable)
3232
- */
3233
- class FirstMatchStrategy extends AbilityStrategy {
3234
- _decisive = null;
3235
- evaluate() {
3236
- const first = this.firstMatched();
3237
- // Если нет совпавших политик → deny по умолчанию
3238
- if (!first) {
3239
- this._decisive = null;
3240
- return AbilityPolicyEffect.deny;
3241
- }
3242
- // Первая совпавшая политика — решающая
3243
- this._decisive = first;
3244
- return first.effect;
3245
- }
3246
- decisivePolicy() {
3247
- return this._decisive;
3248
- }
3249
- }
3250
-
3251
- /**
3252
- * OnlyOneApplicableStrategy
3253
- *
3254
- * This strategy requires that *exactly one* policy is applicable.
3255
- * If zero or more than one policy applies, the result is "deny".
3256
- *
3257
- * Use this strategy when:
3258
- * - Policies must be mutually exclusive.
3259
- * - You want to detect ambiguous or conflicting rules.
3260
- *
3261
- * Example:
3262
- * Policies:
3263
- * P1 → applicable
3264
- * P2 → applicable
3265
- * Result: deny (more than one applicable policy)
3266
- */
3267
- class OnlyOneApplicableStrategy extends AbilityStrategy {
3268
- _decisive = null;
3269
- evaluate() {
3270
- const matched = this.matchedPolicies();
3271
- // 1. Ровно одна совпавшая политика → она решающая
3272
- if (matched.length === 1) {
3273
- this._decisive = matched[0];
3274
- return matched[0].effect;
3275
- }
3276
- // 2. Иначе deny, решающей политики нет
3277
- this._decisive = null;
3278
- return AbilityPolicyEffect.deny;
3279
- }
3280
- decisivePolicy() {
3281
- return this._decisive;
3282
- }
3283
- }
3284
-
3285
- /**
3286
- * PermitOverridesStrategy
3287
- *
3288
- * This strategy gives priority to "permit".
3289
- * If any applicable policy permits, the final result is "permit".
3290
- * Deny is returned only if no policy permits.
3291
- *
3292
- * Use this strategy when:
3293
- * - You want permissive behavior.
3294
- * - A single positive rule should override denials.
3295
- *
3296
- * Example:
3297
- * Policies:
3298
- * P1 → deny
3299
- * P2 → permit
3300
- * P3 → deny
3301
- * Result: permit (permit overrides deny)
3302
- */
3303
- class PermitOverridesStrategy extends AbilityStrategy {
3304
- _decisive = null;
3305
- evaluate() {
3306
- // 1. Если есть permit — он выигрывает
3307
- const permit = this.matchedPolicies().find(p => p.effect === AbilityPolicyEffect.permit);
3308
- if (permit) {
3309
- this._decisive = permit;
3310
- return AbilityPolicyEffect.permit;
3311
- }
3312
- // 2. Если permit нет — ищем deny
3313
- const deny = this.matchedPolicies().find(p => p.effect === AbilityPolicyEffect.deny);
3314
- if (deny) {
3315
- this._decisive = deny;
3316
- return AbilityPolicyEffect.deny;
3317
- }
3318
- // 3. Нет ни permit, ни deny → deny по умолчанию
3319
- this._decisive = null;
3320
- return AbilityPolicyEffect.deny;
3321
- }
3322
- decisivePolicy() {
3323
- return this._decisive;
3324
- }
3325
- }
3326
-
3327
- /**
3328
- * SequentialLastMatchStrategy
3329
- *
3330
- * This strategy evaluates all applicable policies in order and returns the result of the *last* applicable one.
3331
- *
3332
- * Use this strategy when:
3333
- * - Later policies should override earlier ones.
3334
- * - You want a "last rule wins" behavior.
3335
- *
3336
- * Example:
3337
- * Policies:
3338
- * P1 → permit
3339
- * P2 → deny
3340
- * P3 → permit
3341
- * Result: permit (P3 is the last applicable)
3342
- */
3343
- class SequentialLastMatchStrategy extends AbilityStrategy {
3344
- _decisive = null;
3345
- evaluate() {
3346
- const last = this.lastMatched();
3347
- // Нет совпавших политик → deny по умолчанию
3348
- if (!last) {
3349
- this._decisive = null;
3350
- return AbilityPolicyEffect.deny;
3351
- }
3352
- // Последняя совпавшая политика — решающая
3353
- this._decisive = last;
3354
- return last.effect;
3355
- }
3356
- decisivePolicy() {
3357
- return this._decisive;
3358
- }
3359
- }
3360
-
3361
- /**
3362
- * PriorityStrategy
3363
- *
3364
- * This strategy evaluates policies based on their numeric priority.
3365
- * The policy with the highest priority (lowest number or highest number depending on implementation)
3366
- * determines the final result.
3367
- *
3368
- * Use this strategy when:
3369
- * - Policies have explicit priority levels.
3370
- * - You want deterministic resolution based on ranking.
3371
- *
3372
- * Example:
3373
- * Policies:
3374
- * P1 (priority 10) → deny
3375
- * P2 (priority 1) → permit
3376
- * Result: permit (P2 has higher priority)
3377
- */
3378
- class PriorityStrategy extends AbilityStrategy {
3379
- _decisive = null;
3380
- evaluate() {
3381
- const matched = this.matchedPolicies();
3382
- // 1. Нет совпавших политик → deny, решающей политики нет
3383
- if (matched.length === 0) {
3384
- this._decisive = null;
3385
- return AbilityPolicyEffect.deny;
3386
- }
3387
- // 2. Сортируем по приоритету (больший приоритет — выше)
3388
- const sorted = [...matched].sort((a, b) => b.priority - a.priority);
3389
- // 3. Самая приоритетная политика — решающая
3390
- const top = sorted[0];
3391
- this._decisive = top;
3392
- return top.effect;
3393
- }
3394
- decisivePolicy() {
3395
- return this._decisive;
3396
- }
3397
- }
3398
-
3399
- export { AbilityCompare, AbilityCondition, AbilityDSLLexer, AbilityDSLParser, AbilityDSLToken, AbilityError, AbilityExplain, AbilityExplainPolicy, AbilityExplainRule, AbilityExplainRuleSet, AbilityJSONParser, AbilityMatch, AbilityParserError, AbilityPolicy, AbilityPolicyEffect, AbilityResolver, AbilityResult, AbilityRule, AbilityRuleSet, AbilityStrategy, AbilityTypeGenerator, AllMustPermitStrategy, AnyPermitStrategy, DenyOverridesStrategy, FirstMatchStrategy, OnlyOneApplicableStrategy, PermitOverridesStrategy, PriorityStrategy, SequentialLastMatchStrategy, TokenTypes, ability, fromLiteral, isConditionEqual, isConditionNotEqual, toLiteral };
1
+ const t={or:"or",and:"and"};class e extends Error{constructor(t,e){super(t,e),this.name="AbilityError",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}}class s extends Error{constructor(t,e){super(t,e),this.name="AbilityParserError",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}}const i={equals:"=",defined:"defined",not_defined:"not_defined",not_equals:"<>",greater_than:">",less_than:"<",less_or_equal:"<=",greater_or_equal:">=",in:"in",not_in:"not in",contains:"contains",not_contains:"not contains",length_greater_than:"length greater than",length_less_than:"length less than",length_equals:"length equals",always:"always",never:"never"};function r(t){const e={equals:i.equals,not_equals:i.not_equals,greater_than:i.greater_than,less_than:i.less_than,less_or_equal:i.less_or_equal,greater_or_equal:i.greater_or_equal,in:i.in,not_in:i.not_in,contains:i.contains,not_contains:i.not_contains,length_greater_than:i.length_greater_than,length_less_than:i.length_less_than,length_equals:i.length_equals,always:i.always,never:i.never,defined:i.defined,not_defined:i.not_defined},r=e[t];if(!r){const i=Object.keys(e).join(", ");throw new s(`Literal "${t}" does not found in AbilityCondition. Expected one of: ${i}`)}return r}function n(t){switch(t){case i.equals:return"equals";case i.not_equals:return"not_equals";case i.greater_than:return"greater_than";case i.less_than:return"less_than";case i.less_or_equal:return"less_or_equal";case i.greater_or_equal:return"greater_or_equal";case i.in:return"in";case i.not_in:return"not_in";case i.contains:return"contains";case i.not_contains:return"not_contains";case i.length_greater_than:return"length_greater_than";case i.length_less_than:return"length_less_than";case i.length_equals:return"length_equals";case i.always:return"always";case i.never:return"never";case i.defined:return"defined";case i.not_defined:return"not_defined";default:return"never"}}function a(t,e){return null!==t&&null!==e&&t===e}function o(t,e){return!a(t,e)}const c={pending:"pending",match:"match",mismatch:"mismatch",exceptMismatch:"except-mismatch",disabled:"disabled"};class h{type;children;_name;match;debugInfo;constructor(t,e=[]){this.type=t.type,this.children=e,this._name=t.name,this.match=t.match,this.debugInfo=t.debugInfo}get name(){const t=this._name.substring(0,60);return`${t}${t.length<this._name.length?"...":""}`}toString(t="",e=!0){const s=this.match===c.match,i=this.match===c.mismatch,r=this.match===c.pending,n=`[${s?"MATCH ✓":i?"MISMATCH ✗":r?"PENDING …":"DISABLED ⊘"}]`.padEnd(15," "),a=("policy"===this.type?"POLICY":"ruleSet"===this.type?"RULESET":"RULE").padEnd(10," ");let o=`${t}${0===t.length?"":e?"└─ ":"├─ "}${n}${a}${this.name}`;this.debugInfo&&(o+=` (${this.debugInfo})`);const h=t+(e?" ":"│ ");return this.children.forEach((t,e)=>{o+="\n"+t.toString(h,e===this.children.length-1)}),o}}class l extends h{constructor(t){super({type:"rule",match:t.state,name:t.name,debugInfo:`${t.subject} ${t.condition} ${JSON.stringify(t.resource)}`})}}class u extends h{constructor(t){const e=t.rules.map(t=>new l(t));super({type:"ruleSet",match:t.state,name:t.name},e)}}class d extends h{constructor(t){const e=t.ruleSet.map(t=>new u(t));super({type:"policy",name:t.priority>-1?`@priority ${t.priority} <${t.effect}> ${t.name}`:`<${t.effect}> ${t.name}`,match:t.matchState},e)}}class m{effect;strategy;constructor(t,e){this.effect=t,this.strategy=e}explain(){return`${this.strategy.isDenied()?"== DENIED==":"== ALLOWED =="}\n${this.strategy.policies.map(t=>new d(t).toString()).join("\n")}\n`}decisive(){return this.strategy.decisivePolicy()}explainDecisive(){const t=this.decisive();return t?new d(t).toString():null}isAllowed=()=>this.strategy.isAllowed();isDenied=()=>this.strategy.isDenied()}const p={deny:"deny",permit:"permit"};class f{onDeny;onAllow;StrategyClass;policyEntries;constructor(t,e,s={}){const i=this.toArray(t);this.onDeny=s.onDeny,this.onAllow=s.onAllow;const r=[...s.tags?i.filter(t=>t.tags.some(t=>s.tags.includes(t))):i].sort((t,e)=>e.priority-t.priority);this.policyEntries=r.map(t=>({policy:t,normalizedPermission:f.normalizePermission(t.permission),segments:f.normalizePermission(t.permission).split(".")})),this.StrategyClass=e}resolve(t,s,i){const r=f.normalizePermission(String(t)).split("."),n=this.policyEntries.filter(t=>f.matchPermissions(t.segments,r)).map(t=>t.policy);for(const t of n)if(!t.disabled&&t.check(s,i)===c.pending)throw new e(`The policy "${t.name}" is still in a pending state. Make sure to call "check" to evaluate the policy before resolving permissions.`);const a=new this.StrategyClass(n),o=a.evaluate(),h=new m(o,a);return o===p.deny&&this.onDeny&&this.onDeny(h),o===p.permit&&this.onAllow&&this.onAllow(h),h}enforce(t,s,i,r){const n=this.resolve(t,s,i);if(n.isDenied())throw r?.onDeny&&r?.onDeny(n),new e("Permission denied")}static isInPermissionContain(t,e){const s=t.split("."),i=e.split("."),[r,n]=s.length>=i.length?[s,i]:[i,s];return n.every((t,e)=>"*"===t||"*"===r[e]||t===r[e])}toArray(t){return[...Array.isArray(t)?t:[t]]}static normalizePermission(t){return t.trim().replace(/^permission\./,"").replace(/\.+/g,".").toLowerCase()}static matchPermissions(t,e){let s=0;for(;s<t.length;s++){const i=t[s],r=e[s];if("*"===i)return!0;if(void 0===r)return!1;if(i!==r)return!1}return s===e.length}}class y{policies;policyEntries;constructor(t){this.policies=t,this.policyEntries=t.map(t=>({policy:t,normalizedPermission:f.normalizePermission(t.permission),segments:f.normalizePermission(t.permission).split(".")}))}generateTypeDefs(){const t={},e={},s=new Set;this.policies.forEach(i=>{i.tags.forEach(t=>s.add(t));const r=i.permission;t[r]||(t[r]={}),i.ruleSet.forEach(s=>{s.rules.forEach(s=>{const i=s.subject,n=this.determineTypeFromRule(s);if(n){if(i.startsWith("env.")){const t=i.replace(/^env\./,"");e[r]||(e[r]={}),e[r][t]=n}else{const e=t[r][i];t[r][i]=e&&e!==n?`${e} | ${n}`:n}if("string"==typeof s.resource&&this.isPath(s.resource)){const i=s.resource;if(i.startsWith("env.")){const t=i.replace(/^env\./,"");e[r]||(e[r]={});const s=e[r][t],a=n;e[r][t]=s&&s!==a?`${s} | ${a}`:a}else{t[r]||(t[r]={});const e=t[r][i],s=n;t[r][i]=e&&e!==s?`${e} | ${s}`:s}}}})})});const i={};Object.entries(t).forEach(([t,e])=>{t.endsWith(".*")||(i[t]=e)});const r=this.buildNestedStructure(i),n=this.buildNestedStructure(e);return this.formatTypeDefinitions(r,n,s)}isPath(t){return"string"==typeof t&&!t.startsWith('"')&&!t.startsWith("'")&&t.includes(".")}determineTypeFromRule(t){return t.condition===i.never||t.condition===i.always?null:t.condition===i.contains||t.condition===i.not_contains?this.getArrayType(t.resource):t.condition===i.length_equals||t.condition===i.length_greater_than||t.condition===i.length_less_than?"string | readonly unknown[]":t.condition===i.greater_than||t.condition===i.greater_or_equal||t.condition===i.less_than||t.condition===i.less_or_equal?"number":t.condition===i.in||t.condition===i.not_in?this.getInArrayType(t.resource):t.condition===i.equals||t.condition===i.not_equals?this.getPrimitiveType(t.resource):"any"}getArrayType(t){return`readonly ${this.getInArrayType(t)}[]`}getInArrayType(t){if(Array.isArray(t)){if(0===t.length)return"unknown";const e=new Set(t.map(t=>this.getPrimitiveType(t)));return 1===e.size?Array.from(e)[0]:`(${Array.from(e).join(" | ")})`}return this.getPrimitiveType(t)}getPrimitiveType(t){if(null===t)return"null | unknown";if(void 0===t)return"undefined";if("string"==typeof t&&this.isPath(t))return"unknown";switch(typeof t){case"string":return"string";case"number":return"number";case"boolean":return"boolean";case"object":return Array.isArray(t)?"array":"object";default:return"any"}}buildNestedStructure(t){const e={};return Object.entries(t).forEach(([t,s])=>{e[t]={},Object.entries(s).forEach(([s,i])=>{const r=s.split(".");let n=e[t];for(let t=0;t<r.length-1;t++){const e=r[t],s=n[e];if(s&&"object"==typeof s)n=s;else{const t={};n[e]=t,n=t}}n[r[r.length-1]]=i})}),e}formatTypeDefinitions(t,e,s){let i="// Automatically generated by via-profit/ability\n";i+="// Do not edit manually\n",i+="export type Resources = {\n",Object.keys(t).sort().forEach(e=>{const s=t[e],r=0===Object.keys(s).length,n=f.normalizePermission(e).split("."),a=this.policyEntries.filter(t=>f.matchPermissions(t.segments,n)).map(t=>t.policy),o=[...new Set(a.map(t=>t.effect))].sort(),c=a.sort((t,e)=>t.id.localeCompare(e.id)).map(t=>{const e=t.effect.padEnd(6," "),s=t.name===t.id?"Unnamed policy":t.name;return` * - ${e} ${t.id} "${s}"`}).join("\n");i+=`\n /**\n * Permission: ${e}\n * Effects: ${o.join(", ")}\n * Policies:\n${c}\n */\n`,r?i+=` ['${e}']: undefined;\n`:(i+=` ['${e}']: {\n`,i+=this.formatNestedObject(s,4),i+=" } | null | undefined;\n")}),i+="}\n";const r=s.size>0?Array.from(s).sort().map(t=>`'${t}'`).join(" | "):"never";return i+=`\n\nexport type PolicyTags = ${r};\n`,i+="\n\nexport type Environment = {\n",Object.entries(e).forEach(([t,e])=>{const s=0===Object.keys(e).length,r=f.normalizePermission(t).split("."),n=this.policyEntries.filter(t=>f.matchPermissions(t.segments,r)).map(t=>t.policy),a=[...new Set(n.map(t=>t.effect))].sort(),o=n.sort((t,e)=>t.id.localeCompare(e.id)).map(t=>{const e=t.effect.padEnd(6," "),s=t.name===t.id?"Unnamed policy":t.name;return` * - ${e} ${t.id} "${s}"`}).join("\n");i+=`\n /**\n * Permission: ${t}\n * Effects: ${a.join(", ")}\n * Policies:\n${o}\n */\n`,s?i+=` ['${t}']: undefined;\n`:(i+=` ['${t}']: {\n`,i+=this.formatNestedObject(e,4),i+=" } | null | undefined;\n")}),i+="}\n",i}formatNestedObject(t,e){const s=" ".repeat(e);let i="";return Object.keys(t).sort().forEach(r=>{const n=t[r];if("object"==typeof n&&null!==n)i+=`${s}readonly ${r}: {\n`,i+=this.formatNestedObject(n,e+2),i+=`${s}} | null | undefined;\n`;else{const t=[String(n)];let e=String(n);e.match(/unknown/)||(e.match(/null/)||t.push("null"),e.match(/undefined/)||t.push("undefined")),i+=`${s}readonly ${r}: ${t.join(" | ")} \n`}}),i}}class g{static sha1(t){const e=g.stringToBytes(t),s=8*e.length,i=new Uint8Array(e.length+1);i.set(e,0),i[e.length]=128;let r=(56-i.length%64+64)%64;const n=new Uint8Array(i.length+r+8);n.set(i,0);const a=Math.floor(s/4294967296),o=s>>>0;n[n.length-8]=a>>>24&255,n[n.length-7]=a>>>16&255,n[n.length-6]=a>>>8&255,n[n.length-5]=255&a,n[n.length-4]=o>>>24&255,n[n.length-3]=o>>>16&255,n[n.length-2]=o>>>8&255,n[n.length-1]=255&o;let c=1732584193,h=4023233417,l=2562383102,u=271733878,d=3285377520;const m=new Array(80);for(let t=0;t<n.length;t+=64){for(let e=0;e<16;e++){const s=t+4*e;m[e]=n[s]<<24|n[s+1]<<16|n[s+2]<<8|n[s+3]}for(let t=16;t<80;t++)m[t]=g.leftRotate(m[t-3]^m[t-8]^m[t-14]^m[t-16],1);let e=c,s=h,i=l,r=u,a=d;for(let t=0;t<80;t++){let n,o;t<20?(n=s&i|~s&r,o=1518500249):t<40?(n=s^i^r,o=1859775393):t<60?(n=s&i|s&r|i&r,o=2400959708):(n=s^i^r,o=3395469782);const c=g.leftRotate(e,5)+n+a+o+(0|m[t])|0;a=r,r=i,i=g.leftRotate(s,30),s=e,e=c}c=c+e|0,h=h+s|0,l=l+i|0,u=u+r|0,d=d+a|0}return[g.toHex32(c),g.toHex32(h),g.toHex32(l),g.toHex32(u),g.toHex32(d)].join("")}static leftRotate(t,e){return(t<<e|t>>>32-e)>>>0}static toHex32(t){return(t>>>0).toString(16).padStart(8,"0")}static stringToBytes(t){if("undefined"!=typeof TextEncoder)return(new TextEncoder).encode(t);{const e=Buffer.from(t,"utf8");return new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}}}class E{matchState=c.pending;ruleSet=[];effect;compareMethod=t.and;id;name;description;permission;priority=-1;disabled;tags;constructor(e){const{name:s,description:i,id:r,permission:n,effect:a,compareMethod:o=t.and,priority:c,disabled:h,tags:l}=e;this.permission=n,this.description=i,this.effect=a,this.compareMethod=o,this.priority="number"==typeof c?c:-1,this.disabled="boolean"==typeof h&&h,this.tags=l||[],this.id=r||`p_${this.hash().slice(0,10)}`,this.name=s||this.id}addRuleSet(t){return this.ruleSet.push(t),this}addRuleSets(t){for(const e of t)this.ruleSet.push(e);return this}extractRules(){const t=[];for(const e of this.ruleSet)for(const s of e.rules)t.push(s);return t}check(e,s){if(this.matchState=c.mismatch,this.disabled)return this.matchState=c.disabled,this.matchState;if(!this.ruleSet.length)return this.matchState;const i=this.ruleSet.filter(t=>!t.isExcept),r=this.ruleSet.filter(t=>t.isExcept),n=[];for(const r of i){if(r.disabled)continue;const i=r.check(e,s);if(n.push(i),t.and===this.compareMethod&&c.mismatch===i)return this.matchState=c.mismatch,this.matchState;if(t.or===this.compareMethod&&c.match===i){this.matchState=c.match;break}}let a=!1;if(a=t.and===this.compareMethod?n.every(t=>c.match===t):n.some(t=>c.match===t),!a)return this.matchState=c.mismatch,this.matchState;for(const t of r){if(t.disabled)continue;const i=t.check(e,s);if(c.match===i)return this.matchState=c.exceptMismatch,this.matchState}return this.matchState=c.match,this.matchState}explain(){if(this.matchState===c.pending)throw new e("First, run the check method, then explain");return new d(this)}copyWith(t){const e=new E({id:t.id??this.id,name:t.name??this.name,description:t.description??this.description,priority:void 0!==t.priority?t.priority:this.priority,permission:t.permission??this.permission,effect:t.effect??this.effect,compareMethod:t.compareMethod??this.compareMethod}),s=t.ruleSet??this.ruleSet;for(const t of s)e.addRuleSet(t);return e}hash(){const t=[`permission:${this.permission}`,`effect:${this.effect}`,`compareMethod:${this.compareMethod}`,`priority:${this.priority}`,`disabled:${this.disabled}`];if(this.tags&&this.tags.length>0&&t.push(`tags:${[...this.tags].sort().join(",")}`),this.ruleSet&&this.ruleSet.length>0){const e=this.ruleSet.map(t=>t.hash());t.push(`rules:${e.sort().join("|")}`)}const e=t.join(";");return g.sha1(e)}}class _{subject;resource;condition;name;description;id;state=c.pending;disabled;constructor(t){const{id:e,name:s,subject:i,resource:r,condition:n,disabled:a,description:o}=t;this.description=o,this.disabled="boolean"==typeof a&&a,this.subject=i,this.resource=r,this.condition=n,this.state=this.disabled?c.disabled:this.state,this.id=e||`r_${this.hash().slice(0,10)}`,this.name=s||this.id}static isPrimitive(t){return"string"==typeof t||"number"==typeof t||"boolean"==typeof t||null===t}static isNumber(t){return"number"==typeof t}static isString(t){return"string"==typeof t}static valueLen=t=>this.isString(t)||Array.isArray(t)?t.length:null;static operatorHandlers={[n(i.always)]:()=>!0,[n(i.defined)]:t=>void 0!==t,[n(i.not_defined)]:t=>void 0===t,[n(i.never)]:()=>!1,[n(i.equals)]:(t,e)=>t===e,[n(i.not_equals)]:(t,e)=>t!==e,[n(i.contains)]:(t,e)=>Array.isArray(t)&&_.isPrimitive(e)?t.includes(e):!(!Array.isArray(t)||!Array.isArray(e))&&t.some(t=>e.includes(t)),[n(i.not_contains)]:(t,e)=>Array.isArray(t)&&_.isPrimitive(e)?!t.includes(e):!(!Array.isArray(t)||!Array.isArray(e)||t.some(t=>e.includes(t))),[n(i.in)]:(t,e)=>_.isPrimitive(t)&&Array.isArray(e)?e.includes(t):!(!Array.isArray(t)||!Array.isArray(e))&&t.some(t=>e.includes(t)),[n(i.not_in)]:(t,e)=>_.isPrimitive(t)&&Array.isArray(e)?!e.includes(t):!(!Array.isArray(t)||!Array.isArray(e)||t.some(t=>e.includes(t))),[n(i.greater_than)]:(t,e)=>!(!_.isNumber(t)||!_.isNumber(e))&&t>e,[n(i.less_than)]:(t,e)=>!(!_.isNumber(t)||!_.isNumber(e))&&t<e,[n(i.greater_or_equal)]:(t,e)=>!(!_.isNumber(t)||!_.isNumber(e))&&t>=e,[n(i.less_or_equal)]:(t,e)=>!(!_.isNumber(t)||!_.isNumber(e))&&t<=e,[n(i.length_greater_than)]:(t,e)=>{const s=_.valueLen(t);if(null===s)return!1;if(_.isNumber(e))return s>e;const i=_.valueLen(e);return null!==i&&s>i},[n(i.length_less_than)]:(t,e)=>{const s=_.valueLen(t);if(null===s)return!1;if(_.isNumber(e))return s<e;const i=_.valueLen(e);return null!==i&&s<i},[n(i.length_equals)]:(t,e)=>{const s=_.valueLen(t);if(null===s)return!1;if(_.isNumber(e))return s===e;const i=_.valueLen(e);return null!==i&&s===i}};check(t,e){if(this.disabled)return this.state=c.disabled,this.state;const[s,i]=this.extractValues(t,e),r=(0,_.operatorHandlers[n(this.condition)])(s,i);return this.state=r?c.match:c.mismatch,this.state}extractValues(t,e){let s,i;return null==t&&null==e?[NaN,NaN]:(s=this.subject.includes(".")?this.subject.startsWith("env.")&&void 0!==e?this.getDotNotationValue(e,this.subject.replace(/^env\./,"")):this.getDotNotationValue(t,this.subject):this.subject,i="string"==typeof this.resource&&this.resource.includes(".")?this.resource.startsWith("env.")&&void 0!==e?this.getDotNotationValue(e,this.resource.replace(/^env\./,"")):this.getDotNotationValue(t,this.resource):this.resource,[s,i])}static _pathCache=new Map;static _parsePath(t){const e=_._pathCache.get(t);if(e)return e;const s=t.split("."),i=[];for(const t of s){const e=t.indexOf("[");if(-1!==e){const s=t.slice(0,e),r=t.slice(e+1,-1),n=Number(r);i.push({prop:s,index:n})}else i.push(t)}return _._pathCache.set(t,i),i}getDotNotationValue(t,e){if(null==t)return;const s=_._parsePath(e);let i=t;for(const t of s){if(null==i)return;if("string"==typeof t)i=i[t];else{const e=i[t.prop];i=Array.isArray(e)?e[t.index]:void 0}}return i}toString(){return`AbilityRule: ${this.name} condition: ${n(this.condition)} subject: "${this.subject?.toString()}" resource: "${this.resource?.toString()}"`}copyWith(t){return new _({id:t.id??this.id,name:t.name??this.name,description:t.description??this.description,subject:t.subject??this.subject,resource:t.resource??this.resource,condition:t.condition??this.condition})}hash(){const t=[];return t.push(`subject:${this.subject}`),t.push(`resource:${JSON.stringify(this.resource)}`),t.push(`condition:${this.condition}`),t.push(`disabled:${this.disabled}`),g.sha1(t.join(";"))}static equals(t,e){return new _({condition:i.equals,subject:t,resource:e})}static notEquals(t,e){return new _({condition:i.not_equals,subject:t,resource:e})}static contains(t,e){return new _({condition:i.contains,subject:t,resource:e})}static notContains(t,e){return new _({condition:i.not_contains,subject:t,resource:e})}static notIn(t,e){return new _({condition:i.not_in,subject:t,resource:e})}static in(t,e){return new _({condition:i.in,subject:t,resource:e})}static notEqual(t,e){return new _({condition:i.not_equals,subject:t,resource:e})}static lessThan(t,e){return new _({condition:i.less_than,subject:t,resource:e})}static lessOrEqual(t,e){return new _({condition:i.less_or_equal,subject:t,resource:e})}static moreThan(t,e){return new _({condition:i.greater_than,subject:t,resource:e})}static moreOrEqual(t,e){return new _({condition:i.greater_or_equal,subject:t,resource:e})}}class v{state=c.pending;rules=[];compareMethod=t.and;name;description;id;isExcept=!1;disabled;constructor(t){const{name:e,id:s,compareMethod:i,isExcept:r,disabled:n,description:a}=t;this.description=a,this.compareMethod=i,this.isExcept=r,this.disabled="boolean"==typeof n&&n,this.state=this.disabled?c.disabled:this.state,this.id=s||`g_${this.hash().slice(0,10)}`,this.name=e||this.id}addRule(t){return this.rules.push(t),this}addRules(t){return t.forEach(t=>this.addRule(t)),this}check(e,s){if(this.state=c.mismatch,this.disabled)return this.state=c.disabled,this.state;if(!this.rules.length)return this.state;const i=[];for(const r of this.rules){if(r.disabled)continue;const n=r.check(e,s);if(i.push(n),t.and===this.compareMethod&&c.mismatch===n)return this.state;if(t.or===this.compareMethod&&c.match===n)return this.state=c.match,this.state}return t.and===this.compareMethod&&i.every(t=>c.match===t)&&(this.state=c.match),t.or===this.compareMethod&&i.some(t=>c.match===t)&&(this.state=c.match),this.state}toString(){return`AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod}, rules: ${this.rules.map(t=>t.toString()).join("\n")}`}copyWith(t){const e=new v({id:t.id??this.id,name:t.name??this.name,description:t.description??this.description,compareMethod:t.compareMethod??this.compareMethod}),s=t.rules??this.rules;for(const t of s)e.addRule(t);return e}hash(){const t=this.rules.map(t=>t.hash()).sort(),e=[`compareMethod:${this.compareMethod}`,`isExcept:${this.isExcept}`,`disabled:${this.disabled}`,`rules:${t.join("|")}`];return g.sha1(e.join(";"))}static and(e){return new v({compareMethod:t.and}).addRules(e)}static or(e){return new v({compareMethod:t.or}).addRules(e)}}class N{static parse(t){return t.map(t=>N.parsePolicy(t))}static parsePolicy(t){const{id:e,name:s,ruleSet:i,compareMethod:r,permission:n,effect:a,priority:o,disabled:c,tags:h}=t,l=new E({name:s,id:e,permission:n,priority:o,effect:a,disabled:c,tags:h});return l.compareMethod=r,i.forEach(t=>{l.addRuleSet(N.parseRuleSet(t))}),l}static parseRule(t){const{id:e,name:s,subject:i,resource:r,condition:n,disabled:a}=t;return new _({id:e,name:s,subject:i,resource:r,disabled:a,condition:n})}static parseRuleSet(t){const{id:e,name:s,rules:i,compareMethod:r,disabled:n}=t,a=new v({disabled:n,compareMethod:r,name:s,id:e});if(i&&i.length>0){const t=i.map(t=>N.parseRule(t));a.addRules(t)}return a}static ruleToJSON(t){return{id:t.id,name:t.name,disabled:t.disabled,subject:t.subject,resource:t.resource,condition:t.condition}}static ruleSetToJSON(t){return{id:t.id.toString(),name:t.name.toString(),disabled:t.disabled,compareMethod:t.compareMethod,rules:t.rules.map(t=>N.ruleToJSON(t))}}static policyToJSON(t){return{id:t.id.toString(),name:t.name.toString(),disabled:t.disabled,priority:t.priority,permission:t.permission,effect:t.effect,compareMethod:t.compareMethod,tags:t.tags,ruleSet:t.ruleSet.map(t=>N.ruleSetToJSON(t))}}static toJSON(t){return t.map(t=>N.policyToJSON(t))}}const A={EFFECT:"EFFECT",IF:"IF",PERMISSION:"PERMISSION",IDENTIFIER:"IDENTIFIER",COLON:"COLON",COMMA:"COMMA",DOT:"DOT",LBRACKET:"LBRACKET",RBRACKET:"RBRACKET",ALL:"ALL",ANY:"ANY",OF:"OF",EOF:"EOF",COMMENT:"COMMENT",EQ:"EQ",CONTAINS:"CONTAINS",IN:"IN",NOT_IN:"NOT_IN",NOT_CONTAINS:"NOT_CONTAINS",GT:"GT",GTE:"GTE",LT:"LT",LTE:"LTE",NULL:"NULL",EQ_NULL:"EQ_NULL",NOT_EQ_NULL:"NOT_EQ_NULL",DEFINED:"DEFINED",NOT_EQ:"NOT_EQ",LEN_GT:"LEN_GT",LEN_LT:"LEN_LT",LEN_EQ:"LEN_EQ",ALWAYS:"ALWAYS",NEVER:"NEVER",EXCEPT:"EXCEPT",ANNOTATION:"ANNOTATION",STRING:"STRING",NUMBER:"NUMBER",BOOLEAN:"BOOLEAN",SYMBOL:"SYMBOL",KEYWORD:"KEYWORD",ALIAS:"ALIAS",UNKNOWN:"UNKNOWN"};class S{type;value;line;column;constructor(t,e,s,i){this.type=t,this.value=e,this.line=s,this.column=i}toString(){return`AbilityDSLToken([${this.type}] "${this.value}" at ${this.line}:${this.column})`}}class b{input;pos=0;tokens=[];line=1;column=1;keywords=new Set(["if","all","any","of","permit","allow","deny","forbidden","true","false","null","defined","contains","includes","length","len","has","in","gt","lt","gte","lte","equals","greater","less","not","is","or","than","always","never","except","alias"]);constructor(t){this.input=t}tokenize(){for(;!this.isAtEnd()&&(this.skipWhitespace(),!this.isAtEnd());){const t=this.peek();if("@"!==t)if("#"!==t)if('"'!==t&&"'"!==t)if(this.isDigit(t))this.tokens.push(this.readNumber());else if(this.isSymbol(t))this.tokens.push(this.readSymbol());else{if(!this.isAlpha(t))throw new Error(`Unexpected character '${t}' at ${this.line}:${this.column}`);this.tokens.push(this.readWord())}else this.tokens.push(this.readString());else this.tokens.push(this.readComment());else this.tokens.push(this.readAnnotation())}return this.tokens.push(new S(A.EOF,"",this.line,this.column)),this.tokens}readComment(){const t=this.line,e=this.column;this.advance();let s="";for(;!this.isAtEnd()&&!this.isNewline();)s+=this.advance();return new S(A.COMMENT,s.trim(),t,e)}readAnnotation(){const t=this.line,e=this.column;let s="";for(;!this.isAtEnd()&&!this.isNewline();)s+=this.advance();s=s.trim();let i="",r=0,n=!1,a=null,o=!1;for(;r<s.length;){const t=s[r];n?(o?(i+=t,o=!1):"\\"===t?o=!0:t===a?(n=!1,a=null):i+=t,r++):'"'!==t&&"'"!==t?(i+=t,r++):(n=!0,a=t,r++)}return new S(A.ANNOTATION,i.trim(),t,e)}readString(){const t=this.line,e=this.column,s=this.advance();let i="",r=!1;for(;!this.isAtEnd();){const n=this.advance();if(r)i+=n,r=!1;else if("\\"!==n){if(n===s)return new S(A.STRING,i,t,e);i+=n}else r=!0}throw new Error(`Unterminated string at ${t}:${e}`)}readNumber(){const t=this.line,e=this.column,s=this.pos;for(;!this.isAtEnd()&&this.isDigit(this.peek());)this.advance();const i=this.input.slice(s,this.pos);return new S(A.NUMBER,i,t,e)}readSymbol(){const t=this.line,e=this.column,s=this.advance();switch(s){case".":return new S(A.DOT,s,t,e);case":":return new S(A.COLON,s,t,e);case",":return new S(A.COMMA,s,t,e);case"[":return new S(A.LBRACKET,s,t,e);case"]":return new S(A.RBRACKET,s,t,e);case">":return"="===this.peek()?(this.advance(),new S(A.SYMBOL,">=",t,e)):new S(A.SYMBOL,">",t,e);case"<":return"="===this.peek()?(this.advance(),new S(A.SYMBOL,"<=",t,e)):">"===this.peek()?(this.advance(),new S(A.SYMBOL,"<>",t,e)):new S(A.SYMBOL,"<",t,e);case"=":return"="===this.peek()?(this.advance(),new S(A.SYMBOL,"==",t,e)):new S(A.SYMBOL,"=",t,e);case"!":if("="===this.peek())return this.advance(),new S(A.SYMBOL,"!=",t,e);throw new Error(`Unexpected symbol '!' at ${this.line}:${this.column}`);default:throw new Error(`Unknown symbol '${s}' at ${this.line}:${this.column}`)}}readWord(){const t=this.line,e=this.column,s=this.pos;for(;!this.isAtEnd()&&/[a-zA-Z0-9_*]/.test(this.peek());)this.advance();for(;!this.isAtEnd()&&"."===this.peek()&&(this.advance(),/[a-zA-Z_*]/.test(this.peek()));)for(;!this.isAtEnd()&&/[a-zA-Z0-9_*]/.test(this.peek());)this.advance();const i=this.input.slice(s,this.pos);if("always"===i)return new S(A.ALWAYS,i,t,e);if("never"===i)return new S(A.NEVER,i,t,e);if(i.includes(".")){const s=this.tokens[this.tokens.length-1];return s?.type===A.EFFECT&&i.startsWith("permission.")?new S(A.PERMISSION,i,t,e):new S(A.IDENTIFIER,i,t,e)}if(this.keywords.has(i))return"permit"===i||"allow"===i?new S(A.EFFECT,"permit",t,e):"deny"===i||"forbidden"===i?new S(A.EFFECT,"deny",t,e):new S("all"===i?A.ALL:"any"===i?A.ANY:"of"===i?A.OF:"if"===i?A.IF:"true"===i||"false"===i?A.BOOLEAN:"null"===i?A.NULL:"defined"===i?A.DEFINED:"except"===i?A.EXCEPT:"alias"===i?A.ALIAS:A.KEYWORD,i,t,e);const r=this.tokens[this.tokens.length-1];return new S(r?.type===A.EFFECT?A.PERMISSION:A.IDENTIFIER,i,t,e)}skipWhitespace(){for(;!this.isAtEnd()&&/\s/.test(this.peek());)this.advance()}isDigit(t){return t>="0"&&t<="9"}isAlpha(t){return/[a-zA-Z_]/.test(t)}isSymbol(t){return[".",":",",","[","]",">","<","=","!"].includes(t)}isNewline(){return"\n"===this.peek()}peek(){return this.input[this.pos]}advance(){const t=this.input[this.pos++];return"\n"===t?(this.line++,this.column=1):this.column++,t}isAtEnd(){return this.pos>=this.input.length}}class k extends Error{line;column;context;details;_formattedMessage;_originalStack;constructor(t,e,s,i){super(i.split("\n")[0]),this.line=t,this.column=e,this.context=s,this.details=i,this.name="AbilityDSLSyntaxError",Error.captureStackTrace&&Error.captureStackTrace(this,k),this._originalStack=this.stack,this._formattedMessage=this.formatMessage(),Object.defineProperty(this,"stack",{get:()=>this._formattedMessage,configurable:!0})}static supportsColor(){return"undefined"!=typeof process&&process.stdout?.isTTY}formatMessage(){const t=k.supportsColor(),e=t?"":"",s=t?"":"",i=t?"":"",r=t?"":"",n=t?"":"",a=this.context.split("\n"),o=a.findIndex(t=>t.includes("^")||t.includes("~")),c=a.findIndex(t=>t.trim().includes("#")),h=a.map((t,a)=>a===o-1?`${e}${i}${t}${n}`:a===o?`${s}${t}${n}`:a===c?`${r}${t}${n}`:t).join("\n");return`${e}${s}${this.name}: ${this.details}${n}\n\n`+h}toString(){return this._formattedMessage}}class L{tokens;pos=0;dsl;marks=[];lastToken=null;next(){const t=this.tokens[this.pos++];return this.lastToken=t,t}prev(){if(0===this.pos)return null;const t=this.tokens[this.pos--];return this.lastToken=t,t}lookPrev(){return this.lastToken}constructor(t,e){this.tokens=t,this.dsl=e}peek(){return this.tokens[this.pos]}eof(){return this.peek().type===A.EOF}check(t){return!this.eof()&&this.peek().type===t}match(t){return this.check(t)?this.next():null}expect(t,e){const s=this.peek();if(s&&s.type===t)return this.next();this.syntaxError(e,s,[t])}expectOneOf(t,e){const s=this.peek();for(const e of t)if(s&&s.type===e)return this.next();this.syntaxError(e,s,t)}mark(){this.marks.push(this.pos)}reset(){const t=this.marks.pop();void 0!==t&&(this.pos=t)}commit(){this.marks.pop()}syntaxError(t,e,s){const i=this.dsl.split(/\r?\n/),r=e.line-1,n=r>0?i[r-1]:"",a=i[r],o=r+1<i.length?i[r+1]:"",c=" ".repeat(Math.max(0,e.column-1))+"~".repeat(e.value.length),h=String(e.line+1).length,l=t=>String(t).padStart(h," ");let u="";""!==n.trim()&&(u+=`${l(e.line-1)} | ${n}\n`),u+=`${l(e.line)} | ${a}\n`,u+=`${" ".repeat(h)} | ${c}\n`,""!==o.trim()&&(u+=`${l(e.line+1)} | ${o}`);let d=t;if(s&&s?.length>0){const i=e.value,r=this.suggest(i,s),n=`${t}\nDetails: Unexpected value token \`${i}\``;d=r?`${n} Did you mean \`${r}\`?`:n}throw new k(e.line,e.column,u+"\n",d)}suggest(t,e){const s=[];for(const t of e)s.push(t);const i=[...new Set(s)];let r=null,n=3;for(const e of i){const s=this.levenshteinDistance(t.toLowerCase(),e.toLowerCase());s<n&&(n=s,r=e)}return r}levenshteinDistance(t,e){const s=Array.from({length:e.length+1},()=>Array.from({length:t.length+1},()=>0));for(let e=0;e<=t.length;e++)s[0][e]=e;for(let t=0;t<=e.length;t++)s[t][0]=t;for(let i=1;i<=e.length;i++)for(let r=1;r<=t.length;r++){const n=t[r-1]===e[i-1]?0:1;s[i][r]=Math.min(s[i][r-1]+1,s[i-1][r]+1,s[i-1][r-1]+n)}return s[e.length][t.length]}}class O{store={id:void 0,name:void 0,priority:void 0,description:void 0,disabled:void 0,tags:void 0};get(t){return this.store[t]??null}set(t,e,s){return this.store[t]=null===e?void 0:{key:t,value:e,token:s},this}clear(){for(const t of Object.keys(this.store))this.store[t]=void 0}clone(){const t=new O;for(const e of Object.keys(this.store)){const s=this.store[e];t.store[e]=s?{...s}:void 0}return t}get id(){return this.get("id")}get name(){return this.get("name")}get description(){return this.get("description")}get priority(){return this.get("priority")}get disabled(){return this.get("disabled")}get tags(){return this.get("tags")}setID(t,e){return this.set("id",t,e)}setName(t,e){return this.set("name",t,e)}setDescription(t,e){return this.set("description",t,e)}setPriority(t,e){return this.set("priority",t,e)}setDisabled(t,e){return this.set("disabled",t,e)}setTags(t,e){return this.set("tags",t,e)}}const T={policy:new Set(["id","name","description","priority","disabled","tags"]),ruleSet:new Set(["id","name","description","disabled"]),rule:new Set(["id","name","disabled"]),alias:new Set(["name","disabled"])};class x{store=new Map;get(t){return this.store.get(t)||null}set(t,e){return this.store.set(t,e),this}has(t){return this.store.has(t)}}class w{dsl;stream;annBuffer=new O;aliasBuffer=new x;constructor(t){this.dsl=t}parse(){this.annBuffer.clear();const t=new b(this.dsl).tokenize();this.stream=new L(t,this.dsl);const e=[];for(;!this.stream.eof();){if(this.consumeLeadingComments(),this.consumeLeadingAnnotations(),this.consumeLeadingAliases(),!this.isStartOfPolicy()){const t=this.stream.peek();this.stream.syntaxError(`Expected policy, got ${t.type}.`,t,[A.EFFECT])}e.push(this.parsePolicy())}return e}parsePolicy(){this.consumeLeadingComments(),this.consumeLeadingAnnotations(),this.consumeLeadingAliases();const e=this.takeAnnotations("policy"),s=this.stream.expect(A.EFFECT,"Expected effect").value,i=this.stream.expect(A.PERMISSION,"Expected permission"),r=i.value;if(!r.startsWith("permission."))return this.stream.syntaxError(`Unexpected token. The permission key, must be starts with prefix \`permission.\`, but got \`${r}\`.\nDid you mean \`permission.${r}\`?`,i);this.stream.expect(A.IF,'Expected "if"');const n=this.stream.expectOneOf([A.ALL,A.ANY],'Expected "all" or "any"').type===A.ALL?t.and:t.or;this.stream.expect(A.COLON,'Expected ":"');const a=this.parseRuleSets(n);return new E({id:e.id?.value||null,name:e.name?.value||null,description:e.description?.value||null,priority:e.priority?.value||null,permission:r.replace(/^permission\./,""),effect:"permit"===s?p.permit:p.deny,disabled:e.disabled?.value??void 0,tags:e.tags?.value??void 0,compareMethod:n}).addRuleSets(a)}parseRuleSets(t){const e=[];for(this.consumeLeadingComments(),this.consumeLeadingAnnotations();!this.stream.eof()&&!this.isStartOfPolicy();){if(this.isStartOfExcept()){e.push(this.parseExceptGroup(t));continue}if(this.isStartOfGroup()){e.push(this.parseGroup());continue}const s=new v({compareMethod:t});for(;!(this.stream.eof()||(this.consumeLeadingComments(),this.consumeLeadingAnnotations(),this.isStartOfGroup()||this.isStartOfPolicy()||this.isStartOfExcept()));)this.stream.check(A.IDENTIFIER)||this.stream.check(A.ALWAYS)||this.stream.check(A.NEVER)?s.addRule(this.parseRule()):this.stream.syntaxError(`Unexpected token in implicit group: ${this.stream.peek().type}`,this.stream.peek());e.push(s)}return e}parseGroup(){this.consumeLeadingComments(),this.consumeLeadingAnnotations();const e=this.takeAnnotations("ruleSet"),s=this.stream.expectOneOf([A.ALL,A.ANY,A.ALWAYS,A.NEVER],'Expected "all" or "any" or "always" or "never"').type===A.ALL?t.and:t.or;this.stream.check(A.OF)&&this.stream.next(),this.stream.expect(A.COLON,'Expected ":"');const i=new v({id:e.id?.value||null,compareMethod:s,name:e.name?.value||null,description:e.description?.value||null,disabled:e.disabled?.value??void 0});for(;!(this.stream.eof()||(this.consumeLeadingComments(),this.consumeLeadingAnnotations(),this.isStartOfExcept())||this.isStartOfGroup()||this.isStartOfPolicy());)this.stream.check(A.IDENTIFIER)?i.addRule(this.parseRule()):this.stream.syntaxError(`Unexpected token in group: ${this.stream.peek().type}`,this.stream.peek());return i}parseExceptGroup(e){this.consumeLeadingComments(),this.consumeLeadingAnnotations();const s=this.takeAnnotations("ruleSet");this.stream.expect(A.EXCEPT,'Expected "except"');let i=e;this.stream.check(A.ALL)||this.stream.check(A.ANY)?(i=this.stream.next().type===A.ALL?t.and:t.or,this.stream.check(A.OF)&&this.stream.next(),this.stream.expect(A.COLON,'Expected ":" after except group')):this.stream.expect(A.COLON,'Expected ":" after "except"');const r=new v({id:s.id?.value||null,compareMethod:i,name:s.name?.value||null,description:s.description?.value||null,isExcept:!0,disabled:s.disabled?.value??void 0});for(;!(this.stream.eof()||(this.consumeLeadingComments(),this.consumeLeadingAnnotations(),this.isStartOfGroup()||this.isStartOfPolicy()||this.isStartOfExcept()));)this.stream.check(A.IDENTIFIER)?r.addRule(this.parseRule()):this.stream.syntaxError(`Unexpected token in except group: ${this.stream.peek().type}`,this.stream.peek());return r}parseRule(){this.consumeLeadingComments(),this.consumeLeadingAnnotations();const t=this.takeAnnotations("rule"),e=this.stream.check(A.ALWAYS)||this.stream.check(A.NEVER);e||this.stream.check(A.IDENTIFIER)||this.stream.syntaxError(`Expected identifier, but got ${this.stream.peek().type}`,this.stream.peek());const s=e?"":this.stream.expect(A.IDENTIFIER,"Expected field").value;if(this.aliasBuffer.has(s))return this.aliasBuffer.get(s);const{condition:i,operator:r}=this.parseConditionOperator();let n=null,a=null;return r!==A.EQ_NULL&&r!==A.NOT_EQ_NULL&&r!==A.NULL&&r!==A.ALWAYS&&r!==A.NEVER&&r!==A.DEFINED&&(this.stream.mark(),n=this.parseValue(),a=this.stream.lookPrev(),this.stream.commit()),this.consumeLeadingComments(),this.consumeLeadingAnnotations(),this.consumeLeadingAliases(),"string"==typeof n&&a&&a.type===A.IDENTIFIER&&!a.value.includes(".")&&this.stream.syntaxError(`Expected comparison operator or value, got \`${n}\``,a,[A.KEYWORD]),new _({id:t.id?.value||null,subject:s,resource:n,condition:i,name:t.name?.value||null,description:t.description?.value||null,disabled:t.disabled?.value??void 0})}parseConditionOperator(){if(this.stream.mark(),this.matchWord("always"))return this.stream.commit(),{condition:i.always,operator:A.ALWAYS};if(this.stream.reset(),this.stream.mark(),this.matchWord("never"))return this.stream.commit(),{condition:i.never,operator:A.NEVER};if(this.stream.reset(),this.stream.mark(),(this.matchWord("length")||this.matchWord("len"))&&this.matchWord("equals"))return this.stream.commit(),{condition:i.length_equals,operator:A.LEN_EQ};if(this.stream.reset(),this.stream.mark(),(this.matchWord("length")||this.matchWord("len"))&&this.matchSymbol("="))return this.stream.commit(),{condition:i.length_equals,operator:A.LEN_EQ};if(this.stream.reset(),this.stream.mark(),(this.matchWord("length")||this.matchWord("len"))&&this.matchWord("greater")&&this.matchWord("than"))return this.stream.commit(),{condition:i.length_greater_than,operator:A.LEN_GT};if(this.stream.reset(),this.stream.mark(),(this.matchWord("length")||this.matchWord("len"))&&this.matchSymbol(">"))return this.stream.commit(),{condition:i.length_greater_than,operator:A.LEN_GT};if(this.stream.reset(),this.stream.mark(),(this.matchWord("length")||this.matchWord("len"))&&this.matchWord("less")&&this.matchWord("than"))return this.stream.commit(),{condition:i.length_less_than,operator:A.LEN_LT};if(this.stream.reset(),this.stream.mark(),(this.matchWord("length")||this.matchWord("len"))&&this.matchSymbol("<"))return this.stream.commit(),{condition:i.length_less_than,operator:A.LEN_LT};if(this.stream.reset(),this.stream.mark(),this.matchWord("greater")&&this.matchWord("than")&&this.matchWord("or")&&this.matchWord("equal"))return this.stream.commit(),{condition:i.greater_or_equal,operator:A.GTE};if(this.stream.reset(),this.stream.mark(),this.matchWord("greater")&&this.matchWord("than"))return this.stream.commit(),{condition:i.greater_than,operator:A.GT};if(this.stream.reset(),this.stream.mark(),this.matchWord("less")&&this.matchWord("than")&&this.matchWord("or")&&this.matchWord("equal"))return this.stream.commit(),{condition:i.less_or_equal,operator:A.LTE};if(this.stream.reset(),this.matchWord("less")&&this.matchWord("than"))return{condition:i.less_than,operator:A.LT};if(this.stream.reset(),this.stream.mark(),this.matchWord("not")&&this.matchWord("contains"))return this.stream.commit(),{condition:i.not_contains,operator:A.NOT_CONTAINS};if(this.stream.reset(),this.stream.mark(),this.matchWord("not")&&this.matchWord("includes"))return this.stream.commit(),{condition:i.not_contains,operator:A.NOT_CONTAINS};if(this.stream.reset(),this.stream.mark(),this.matchWord("not")&&this.matchWord("has"))return this.stream.commit(),{condition:i.not_contains,operator:A.NOT_CONTAINS};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("equals"))return this.stream.commit(),{condition:i.equals,operator:A.EQ};if(this.stream.reset(),this.stream.mark(),this.matchWord("not")&&this.matchWord("equals"))return this.stream.commit(),{condition:i.not_equals,operator:A.NOT_EQ};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("not")&&this.matchWord("equals"))return this.stream.commit(),{condition:i.not_equals,operator:A.NOT_EQ};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("in"))return this.stream.commit(),{condition:i.in,operator:A.IN};if(this.stream.reset(),this.stream.mark(),this.matchWord("not")&&this.matchWord("in"))return this.stream.commit(),{condition:i.not_in,operator:A.NOT_IN};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("not")&&this.stream.check(A.NULL))return this.stream.next(),this.stream.commit(),{condition:i.not_equals,operator:A.NOT_EQ_NULL};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("null")&&this.stream.check(A.NULL))return this.stream.commit(),this.stream.next(),{condition:i.equals,operator:A.EQ_NULL};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("defined"))return this.stream.commit(),{condition:i.defined,operator:A.DEFINED};if(this.stream.reset(),this.stream.mark(),this.matchWord("is")&&this.matchWord("not")&&this.matchWord("defined"))return this.stream.commit(),{condition:i.not_defined,operator:A.DEFINED};this.stream.reset();const t=this.stream.peek();switch(t.type!==A.SYMBOL&&t.type!==A.KEYWORD&&t.type!==A.NULL&&this.stream.syntaxError(`Expected comparison operator, got \`${t.value}\``,t,[A.SYMBOL,A.KEYWORD,A.NULL]),this.stream.next(),t.type){case A.SYMBOL:if("="===t.value||"=="===t.value)return{condition:i.equals,operator:A.EQ};if("!="===t.value||"<>"===t.value)return{condition:i.not_equals,operator:A.NOT_EQ};if(">"===t.value)return{condition:i.greater_than,operator:A.GT};if("<"===t.value)return{condition:i.less_than,operator:A.LT};if(">="===t.value)return{condition:i.greater_or_equal,operator:A.GTE};if("<="===t.value)return{condition:i.less_or_equal,operator:A.LTE};break;case A.KEYWORD:if("contains"===t.value||"includes"===t.value||"has"===t.value)return{condition:i.contains,operator:A.CONTAINS};if("in"===t.value)return{condition:i.in,operator:A.IN};if("equals"===t.value)return{condition:i.equals,operator:A.EQ};if("gte"===t.value)return{condition:i.greater_or_equal,operator:A.GTE};if("greater"===t.value||"gt"===t.value)return{condition:i.greater_than,operator:A.GT};if("less"===t.value||"lt"===t.value)return{condition:i.less_than,operator:A.LT};if("lte"===t.value)return{condition:i.less_or_equal,operator:A.LTE};if("is"===t.value)return{condition:i.equals,operator:A.EQ}}return this.stream.syntaxError(`Unexpected operator token \`${t.value}\``,t,[A.SYMBOL,A.KEYWORD])}matchWord(t){if(this.stream.eof())return!1;const e=this.stream.peek();return(e.type===A.KEYWORD||e.type===A.IDENTIFIER||e.type===A.ALWAYS||e.type===A.NEVER||e.type===A.DEFINED)&&e.value===t&&(this.stream.next(),!0)}matchSymbol(t){if(this.stream.eof())return!1;const e=this.stream.peek();return e.type===A.SYMBOL&&e.value===t&&(this.stream.next(),!0)}parseValue(){if(this.stream.check(A.LBRACKET))return this.stream.next(),this.parseArray();const t=this.stream.peek();switch(t.type!==A.ALL&&t.type!==A.ANY&&t.type!==A.EFFECT||this.stream.syntaxError(`Unexpected ${t.type} in value position`,t),this.stream.next(),t.type){case A.STRING:return t.value;case A.NUMBER:return Number(t.value);case A.BOOLEAN:return"true"===t.value;case A.NULL:return null;case A.DEFINED:return void 0!==t.value;case A.IDENTIFIER:return t.value;default:this.stream.syntaxError(`Unexpected value token "${t.value}"`,t,[A.KEYWORD])}}parseArray(){const t=[];if(this.stream.check(A.RBRACKET))return this.stream.next(),t;for(;!this.stream.eof()&&!this.stream.check(A.RBRACKET);){const e=this.parseValue();Array.isArray(e)?t.push(...e):"string"==typeof e||"number"==typeof e||"boolean"==typeof e?t.push(e):null===e&&this.stream.syntaxError("Unexpected null in array",this.stream.peek()),this.stream.check(A.COMMA)&&this.stream.next()}return this.stream.expect(A.RBRACKET,'Expected "]"'),t}consumeLeadingComments(){for(;this.stream.check(A.COMMENT);)this.stream.next()}consumeLeadingAliases(){for(;this.stream.check(A.ALIAS);){this.stream.next();const t=this.stream.expect(A.IDENTIFIER,"Expected alias name").value;this.stream.expect(A.COLON,"Expected colon after an alias");const e=this.takeAnnotations("alias");for(;!this.stream.eof()&&!this.isStartOfAlias()&&!this.isStartOfPolicy();){const s=this.parseRule();s.name=e.get("name")?.value||t,s.description=e.get("description")?.value,!0===e.get("disabled")?.value&&(s.disabled=!0),this.aliasBuffer.set(t,s)}}}consumeLeadingAnnotations(){for(;this.stream.check(A.ANNOTATION);){const t=this.stream.next(),e=t.value.trim();if(e.startsWith("@id ")&&this.annBuffer.setID(e.slice(4).trim(),t),e.startsWith("@name ")&&this.annBuffer.setName(e.slice(6).trim(),t),e.startsWith("@description ")&&this.annBuffer.setDescription(e.slice(13).trim(),t),e.startsWith("@priority ")&&this.annBuffer.setPriority(parseInt(e.slice(10).trim(),10),t),e.startsWith("@disabled")){const s=e.slice(9).trim();this.annBuffer.setDisabled(0===s.length||"true"===e.slice(9).trim(),t)}if(e.startsWith("@tags ")){const s=e.slice(6).trim().split(",").map(t=>t.trim());this.annBuffer.setTags(s,t)}}}takeAnnotations(t){const e=this.annBuffer.clone();this.annBuffer.clear();const s=T[t];for(const i of Object.keys(e.store)){const r=e.get(i);r&&(s.has(i)||this.stream.syntaxError(`Annotation @${i} is not allowed on ${t}. Allowed: ${[...s].map(t=>"@"+t).join(", ")}`,r.token??this.stream.peek()))}return e}isStartOfPolicy(){return this.stream.check(A.EFFECT)}isStartOfGroup(){return this.stream.check(A.ALL)||this.stream.check(A.ANY)}isStartOfRule(){return this.stream.check(A.IDENTIFIER)||this.stream.check(A.ALWAYS)||this.stream.check(A.NEVER)}isStartOfExcept(){return this.stream.check(A.EXCEPT)}isStartOfAlias(){return this.stream.check(A.ALIAS)}}function $(t,...e){const s=t.reduce((t,s,i)=>t+s+(e[i]??""),"");return new w(s).parse()}class I{policies;matched;constructor(t){this.policies=t,this.matched=t.filter(t=>t.matchState===c.match)}matchedPolicies(){return this.matched}hasMatched(){return this.matched.length>0}firstMatched(){return this.matchedPolicies()[0]??null}lastMatched(){const t=this.matchedPolicies();return t.length>0?t[t.length-1]:null}firstDenied(){return this.getDenyPolicies()[0]??null}firstPermitted(){return this.getPermitPolicies()[0]??null}getPermitPolicies(){return this.matched.filter(t=>t.effect===p.permit)}getDenyPolicies(){return this.matched.filter(t=>t.effect===p.deny)}hasPermit(){return this.getPermitPolicies().length>0}hasDeny(){return this.getDenyPolicies().length>0}isAllowed(){return this.evaluate()===p.permit}isDenied(){return this.evaluate()===p.deny}}class W extends I{_decisive=null;evaluate(){if(!this.hasMatched())return this._decisive=null,p.deny;const t=this.firstDenied();return t?(this._decisive=t,p.deny):(this._decisive=this.firstPermitted(),p.permit)}decisivePolicy(){return this._decisive}}class M extends I{_decisive=null;evaluate(){const t=this.firstPermitted();return t?(this._decisive=t,p.permit):(this._decisive=null,p.deny)}decisivePolicy(){return this._decisive}}class R extends I{_decisive=null;evaluate(){const t=this.firstDenied();if(t)return this._decisive=t,p.deny;const e=this.firstPermitted();return e?(this._decisive=e,p.permit):(this._decisive=null,p.deny)}decisivePolicy(){return this._decisive}}class D extends I{_decisive=null;evaluate(){const t=this.firstMatched();return t?(this._decisive=t,t.effect):(this._decisive=null,p.deny)}decisivePolicy(){return this._decisive}}class P extends I{_decisive=null;evaluate(){const t=this.matchedPolicies();return 1===t.length?(this._decisive=t[0],t[0].effect):(this._decisive=null,p.deny)}decisivePolicy(){return this._decisive}}class C extends I{_decisive=null;evaluate(){const t=this.matchedPolicies().find(t=>t.effect===p.permit);if(t)return this._decisive=t,p.permit;const e=this.matchedPolicies().find(t=>t.effect===p.deny);return e?(this._decisive=e,p.deny):(this._decisive=null,p.deny)}decisivePolicy(){return this._decisive}}class j extends I{_decisive=null;evaluate(){const t=this.lastMatched();return t?(this._decisive=t,t.effect):(this._decisive=null,p.deny)}decisivePolicy(){return this._decisive}}class q extends I{_decisive=null;evaluate(){const t=this.matchedPolicies();if(0===t.length)return this._decisive=null,p.deny;const e=[...t].sort((t,e)=>e.priority-t.priority)[0];return this._decisive=e,e.effect}decisivePolicy(){return this._decisive}}export{t as AbilityCompare,i as AbilityCondition,b as AbilityDSLLexer,w as AbilityDSLParser,S as AbilityDSLToken,e as AbilityError,h as AbilityExplain,d as AbilityExplainPolicy,l as AbilityExplainRule,u as AbilityExplainRuleSet,N as AbilityJSONParser,c as AbilityMatch,s as AbilityParserError,E as AbilityPolicy,p as AbilityPolicyEffect,f as AbilityResolver,m as AbilityResult,_ as AbilityRule,v as AbilityRuleSet,I as AbilityStrategy,y as AbilityTypeGenerator,W as AllMustPermitStrategy,M as AnyPermitStrategy,R as DenyOverridesStrategy,D as FirstMatchStrategy,P as OnlyOneApplicableStrategy,C as PermitOverridesStrategy,q as PriorityStrategy,j as SequentialLastMatchStrategy,A as TokenTypes,$ as ability,r as fromLiteral,a as isConditionEqual,o as isConditionNotEqual,n as toLiteral};