@via-profit/ability 3.6.5 → 3.7.3

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