@siremzam/sentinel 0.3.0 → 0.3.2

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.
Files changed (59) hide show
  1. package/README.md +573 -176
  2. package/dist/engine-C6IASR5F.d.cts +283 -0
  3. package/dist/engine-C6IASR5F.d.ts +283 -0
  4. package/dist/index.cjs +877 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +58 -0
  7. package/dist/index.d.ts +58 -11
  8. package/dist/index.js +838 -6
  9. package/dist/index.js.map +1 -1
  10. package/dist/middleware/express.cjs +58 -0
  11. package/dist/middleware/express.cjs.map +1 -0
  12. package/dist/middleware/express.d.cts +35 -0
  13. package/dist/middleware/express.d.ts +6 -6
  14. package/dist/middleware/express.js +31 -39
  15. package/dist/middleware/express.js.map +1 -1
  16. package/dist/middleware/fastify.cjs +59 -0
  17. package/dist/middleware/fastify.cjs.map +1 -0
  18. package/dist/middleware/fastify.d.cts +29 -0
  19. package/dist/middleware/fastify.d.ts +6 -6
  20. package/dist/middleware/fastify.js +32 -39
  21. package/dist/middleware/fastify.js.map +1 -1
  22. package/dist/middleware/nestjs.cjs +84 -0
  23. package/dist/middleware/nestjs.cjs.map +1 -0
  24. package/dist/middleware/nestjs.d.cts +67 -0
  25. package/dist/middleware/nestjs.d.ts +9 -9
  26. package/dist/middleware/nestjs.js +51 -76
  27. package/dist/middleware/nestjs.js.map +1 -1
  28. package/dist/server.cjs +184 -0
  29. package/dist/server.cjs.map +1 -0
  30. package/dist/server.d.cts +54 -0
  31. package/dist/server.d.ts +10 -8
  32. package/dist/server.js +149 -153
  33. package/dist/server.js.map +1 -1
  34. package/package.json +22 -9
  35. package/dist/engine.d.ts +0 -70
  36. package/dist/engine.d.ts.map +0 -1
  37. package/dist/engine.js +0 -562
  38. package/dist/engine.js.map +0 -1
  39. package/dist/index.d.ts.map +0 -1
  40. package/dist/middleware/express.d.ts.map +0 -1
  41. package/dist/middleware/fastify.d.ts.map +0 -1
  42. package/dist/middleware/nestjs.d.ts.map +0 -1
  43. package/dist/policy-builder.d.ts +0 -39
  44. package/dist/policy-builder.d.ts.map +0 -1
  45. package/dist/policy-builder.js +0 -92
  46. package/dist/policy-builder.js.map +0 -1
  47. package/dist/role-hierarchy.d.ts +0 -42
  48. package/dist/role-hierarchy.d.ts.map +0 -1
  49. package/dist/role-hierarchy.js +0 -87
  50. package/dist/role-hierarchy.js.map +0 -1
  51. package/dist/serialization.d.ts +0 -52
  52. package/dist/serialization.d.ts.map +0 -1
  53. package/dist/serialization.js +0 -144
  54. package/dist/serialization.js.map +0 -1
  55. package/dist/server.d.ts.map +0 -1
  56. package/dist/types.d.ts +0 -137
  57. package/dist/types.d.ts.map +0 -1
  58. package/dist/types.js +0 -27
  59. package/dist/types.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,839 @@
1
- export { AccessEngine } from "./engine.js";
2
- export { RuleBuilder, allow, deny, createPolicyFactory } from "./policy-builder.js";
3
- export { RoleHierarchy } from "./role-hierarchy.js";
4
- export { ConditionRegistry, exportRules, exportRulesToJson, importRules, importRulesFromJson, } from "./serialization.js";
5
- export { createAuthServer } from "./server.js";
6
- export { toAuditEntry } from "./types.js";
1
+ // src/policy-builder.ts
2
+ var ruleCounter = 0;
3
+ function nextRuleId(prefix) {
4
+ return `${prefix}-${++ruleCounter}`;
5
+ }
6
+ var RuleBuilder = class {
7
+ _effect;
8
+ _roles = "*";
9
+ _actions = "*";
10
+ _resources = "*";
11
+ _conditions = [];
12
+ _priority = 0;
13
+ _description;
14
+ _id;
15
+ constructor(effect) {
16
+ this._effect = effect;
17
+ this._id = nextRuleId(effect);
18
+ }
19
+ id(id) {
20
+ this._id = id;
21
+ return this;
22
+ }
23
+ roles(...roles) {
24
+ this._roles = roles;
25
+ return this;
26
+ }
27
+ anyRole() {
28
+ this._roles = "*";
29
+ return this;
30
+ }
31
+ actions(...actions) {
32
+ this._actions = actions;
33
+ return this;
34
+ }
35
+ anyAction() {
36
+ this._actions = "*";
37
+ return this;
38
+ }
39
+ on(...resources) {
40
+ this._resources = resources;
41
+ return this;
42
+ }
43
+ anyResource() {
44
+ this._resources = "*";
45
+ return this;
46
+ }
47
+ when(condition) {
48
+ this._conditions.push(condition);
49
+ return this;
50
+ }
51
+ priority(p) {
52
+ this._priority = p;
53
+ return this;
54
+ }
55
+ describe(desc) {
56
+ this._description = desc;
57
+ return this;
58
+ }
59
+ build() {
60
+ return {
61
+ id: this._id,
62
+ effect: this._effect,
63
+ roles: this._roles,
64
+ actions: this._actions,
65
+ resources: this._resources,
66
+ conditions: this._conditions.length > 0 ? this._conditions : void 0,
67
+ priority: this._priority,
68
+ description: this._description
69
+ };
70
+ }
71
+ };
72
+ function allow() {
73
+ return new RuleBuilder("allow");
74
+ }
75
+ function deny() {
76
+ return new RuleBuilder("deny");
77
+ }
78
+ function createPolicyFactory() {
79
+ return {
80
+ allow: () => new RuleBuilder("allow"),
81
+ deny: () => new RuleBuilder("deny")
82
+ };
83
+ }
84
+
85
+ // src/engine.ts
86
+ function escapeRegexMeta(s) {
87
+ return s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
88
+ }
89
+ function compileActionPatterns(actions) {
90
+ if (actions === "*") return null;
91
+ const patterns = [];
92
+ for (const action of actions) {
93
+ if (action.includes("*")) {
94
+ const escaped = escapeRegexMeta(action).replace(/\*/g, "[^:]*");
95
+ patterns.push(new RegExp("^" + escaped + "$"));
96
+ }
97
+ }
98
+ return patterns.length > 0 ? patterns : null;
99
+ }
100
+ var AccessEngine = class {
101
+ compiled = [];
102
+ listeners = [];
103
+ asyncConditions;
104
+ _defaultDeny;
105
+ _strictTenancy;
106
+ hierarchy;
107
+ cache;
108
+ conditionErrorHandler;
109
+ constructor(options) {
110
+ this.asyncConditions = options.asyncConditions ?? false;
111
+ this._defaultDeny = (options.defaultEffect ?? "deny") === "deny";
112
+ this._strictTenancy = options.strictTenancy ?? false;
113
+ this.hierarchy = options.roleHierarchy;
114
+ this.conditionErrorHandler = options.onConditionError;
115
+ if (options.cacheSize && options.cacheSize > 0) {
116
+ this.cache = new LRUCache(options.cacheSize);
117
+ }
118
+ if (options.onDecision) {
119
+ this.listeners.push(options.onDecision);
120
+ }
121
+ }
122
+ // -----------------------------------------------------------------------
123
+ // Rule management
124
+ // -----------------------------------------------------------------------
125
+ addRule(rule) {
126
+ const frozen = Object.freeze({ ...rule });
127
+ this.compiled.push({
128
+ rule: frozen,
129
+ actionPatterns: compileActionPatterns(frozen.actions)
130
+ });
131
+ this.cache?.clear();
132
+ return this;
133
+ }
134
+ addRules(...rules) {
135
+ for (const rule of rules) {
136
+ const frozen = Object.freeze({ ...rule });
137
+ this.compiled.push({
138
+ rule: frozen,
139
+ actionPatterns: compileActionPatterns(frozen.actions)
140
+ });
141
+ }
142
+ this.cache?.clear();
143
+ return this;
144
+ }
145
+ removeRule(ruleId) {
146
+ const idx = this.compiled.findIndex((c) => c.rule.id === ruleId);
147
+ if (idx === -1) return false;
148
+ this.compiled.splice(idx, 1);
149
+ this.cache?.clear();
150
+ return true;
151
+ }
152
+ getRules() {
153
+ return this.compiled.map((c) => c.rule);
154
+ }
155
+ clearRules() {
156
+ this.compiled = [];
157
+ this.cache?.clear();
158
+ }
159
+ // -----------------------------------------------------------------------
160
+ // Cache control
161
+ // -----------------------------------------------------------------------
162
+ clearCache() {
163
+ this.cache?.clear();
164
+ }
165
+ get cacheStats() {
166
+ if (!this.cache) return null;
167
+ return { size: this.cache.size, maxSize: this.cache.maxSize };
168
+ }
169
+ // -----------------------------------------------------------------------
170
+ // Fluent rule builders bound to this engine's schema
171
+ // -----------------------------------------------------------------------
172
+ allow() {
173
+ return allow();
174
+ }
175
+ deny() {
176
+ return deny();
177
+ }
178
+ // -----------------------------------------------------------------------
179
+ // Observability
180
+ // -----------------------------------------------------------------------
181
+ onDecision(listener) {
182
+ this.listeners.push(listener);
183
+ return () => {
184
+ const idx = this.listeners.indexOf(listener);
185
+ if (idx !== -1) this.listeners.splice(idx, 1);
186
+ };
187
+ }
188
+ emit(decision) {
189
+ for (const listener of this.listeners) {
190
+ try {
191
+ const result = listener(decision);
192
+ if (result instanceof Promise) {
193
+ result.catch(() => {
194
+ });
195
+ }
196
+ } catch {
197
+ }
198
+ }
199
+ }
200
+ // -----------------------------------------------------------------------
201
+ // Evaluation
202
+ // -----------------------------------------------------------------------
203
+ evaluate(subject, action, resource, resourceContext = {}, tenantId) {
204
+ if (this.asyncConditions) {
205
+ throw new Error(
206
+ "Engine has asyncConditions enabled. Use evaluateAsync() instead."
207
+ );
208
+ }
209
+ this.validateInput(subject, action, resource);
210
+ this.enforceTenancy(subject, tenantId);
211
+ const cacheKey = this.cache ? buildCacheKey(subject.id, action, resource, tenantId) : void 0;
212
+ if (cacheKey) {
213
+ const cached = this.cache.get(cacheKey);
214
+ if (cached) {
215
+ this.emit(cached);
216
+ return cached;
217
+ }
218
+ }
219
+ const start = performance.now();
220
+ const ctx = this.buildContext(subject, action, resource, resourceContext, tenantId);
221
+ const candidates = this.matchRules(subject, action, resource, tenantId);
222
+ let matched = null;
223
+ let matchedHasConditions = false;
224
+ for (const compiled of candidates) {
225
+ const { rule } = compiled;
226
+ if (!rule.conditions || rule.conditions.length === 0) {
227
+ matched = compiled;
228
+ matchedHasConditions = false;
229
+ break;
230
+ }
231
+ const allMet = this.evaluateConditionsSync(rule, ctx);
232
+ if (allMet) {
233
+ matched = compiled;
234
+ matchedHasConditions = true;
235
+ break;
236
+ }
237
+ }
238
+ const decision = this.buildDecision(matched?.rule ?? null, ctx, start);
239
+ if (cacheKey && !matchedHasConditions) {
240
+ this.cache.set(cacheKey, decision);
241
+ }
242
+ this.emit(decision);
243
+ return decision;
244
+ }
245
+ async evaluateAsync(subject, action, resource, resourceContext = {}, tenantId) {
246
+ this.validateInput(subject, action, resource);
247
+ this.enforceTenancy(subject, tenantId);
248
+ const start = performance.now();
249
+ const ctx = this.buildContext(subject, action, resource, resourceContext, tenantId);
250
+ const candidates = this.matchRules(subject, action, resource, tenantId);
251
+ let matched = null;
252
+ for (const compiled of candidates) {
253
+ const { rule } = compiled;
254
+ if (!rule.conditions || rule.conditions.length === 0) {
255
+ matched = rule;
256
+ break;
257
+ }
258
+ const results = await Promise.all(
259
+ rule.conditions.map(
260
+ (c, i) => Promise.resolve().then(() => c(ctx)).catch((err) => {
261
+ this.emitConditionError(rule.id, i, err);
262
+ return false;
263
+ })
264
+ )
265
+ );
266
+ if (results.every(Boolean)) {
267
+ matched = rule;
268
+ break;
269
+ }
270
+ }
271
+ const decision = this.buildDecision(matched, ctx, start);
272
+ this.emit(decision);
273
+ return decision;
274
+ }
275
+ // -----------------------------------------------------------------------
276
+ // permitted() — list allowed actions on a resource for UI rendering
277
+ // -----------------------------------------------------------------------
278
+ permitted(subject, resource, actions, resourceContext = {}, tenantId) {
279
+ const allowed = /* @__PURE__ */ new Set();
280
+ for (const action of actions) {
281
+ const decision = this.asyncConditions ? (() => {
282
+ throw new Error("Use permittedAsync() with asyncConditions enabled.");
283
+ })() : this.evaluate(subject, action, resource, resourceContext, tenantId);
284
+ if (decision.allowed) {
285
+ allowed.add(action);
286
+ }
287
+ }
288
+ return allowed;
289
+ }
290
+ async permittedAsync(subject, resource, actions, resourceContext = {}, tenantId) {
291
+ const allowed = /* @__PURE__ */ new Set();
292
+ const results = await Promise.all(
293
+ actions.map(
294
+ (action) => this.evaluateAsync(subject, action, resource, resourceContext, tenantId)
295
+ )
296
+ );
297
+ for (let i = 0; i < actions.length; i++) {
298
+ if (results[i].allowed) {
299
+ allowed.add(actions[i]);
300
+ }
301
+ }
302
+ return allowed;
303
+ }
304
+ // -----------------------------------------------------------------------
305
+ // explain() — full evaluation trace for debugging
306
+ // -----------------------------------------------------------------------
307
+ explain(subject, action, resource, resourceContext = {}, tenantId) {
308
+ if (this.asyncConditions) {
309
+ throw new Error(
310
+ "Engine has asyncConditions enabled. Use explainAsync() instead."
311
+ );
312
+ }
313
+ this.enforceTenancy(subject, tenantId);
314
+ const start = performance.now();
315
+ const ctx = this.buildContext(subject, action, resource, resourceContext, tenantId);
316
+ const subjectRoles = this.resolveRoles(subject, tenantId);
317
+ const evaluatedRules = [];
318
+ let firstMatch = null;
319
+ const sorted = this.sortCandidates([...this.compiled]);
320
+ for (const compiled of sorted) {
321
+ const { rule } = compiled;
322
+ const roleMatched = rule.roles === "*" || rule.roles.some((r) => subjectRoles.has(r));
323
+ const actionMatched = rule.actions === "*" || this.matchesAction(compiled, action);
324
+ const resourceMatched = rule.resources === "*" || rule.resources.includes(resource);
325
+ const conditionResults = [];
326
+ let allConditionsPassed = true;
327
+ if (roleMatched && actionMatched && resourceMatched && rule.conditions) {
328
+ for (let i = 0; i < rule.conditions.length; i++) {
329
+ try {
330
+ const result = rule.conditions[i](ctx);
331
+ if (result !== true) {
332
+ conditionResults.push({ index: i, passed: false });
333
+ allConditionsPassed = false;
334
+ } else {
335
+ conditionResults.push({ index: i, passed: true });
336
+ }
337
+ } catch (err) {
338
+ conditionResults.push({
339
+ index: i,
340
+ passed: false,
341
+ error: err instanceof Error ? err.message : String(err)
342
+ });
343
+ allConditionsPassed = false;
344
+ }
345
+ }
346
+ }
347
+ const matched = roleMatched && actionMatched && resourceMatched && (!rule.conditions || rule.conditions.length === 0 || allConditionsPassed);
348
+ evaluatedRules.push({
349
+ rule,
350
+ roleMatched,
351
+ actionMatched,
352
+ resourceMatched,
353
+ conditionResults,
354
+ matched
355
+ });
356
+ if (matched && !firstMatch) {
357
+ firstMatch = rule;
358
+ }
359
+ }
360
+ const allowed = firstMatch != null ? firstMatch.effect === "allow" : !this._defaultDeny;
361
+ const effect = firstMatch?.effect ?? "default-deny";
362
+ const reason = firstMatch ? `Matched rule "${firstMatch.id}"${firstMatch.description ? `: ${firstMatch.description}` : ""}` : "No matching rule \u2014 default deny";
363
+ return {
364
+ allowed,
365
+ effect,
366
+ reason,
367
+ evaluatedRules,
368
+ durationMs: performance.now() - start
369
+ };
370
+ }
371
+ async explainAsync(subject, action, resource, resourceContext = {}, tenantId) {
372
+ this.enforceTenancy(subject, tenantId);
373
+ const start = performance.now();
374
+ const ctx = this.buildContext(subject, action, resource, resourceContext, tenantId);
375
+ const subjectRoles = this.resolveRoles(subject, tenantId);
376
+ const evaluatedRules = [];
377
+ let firstMatch = null;
378
+ const sorted = this.sortCandidates([...this.compiled]);
379
+ for (const compiled of sorted) {
380
+ const { rule } = compiled;
381
+ const roleMatched = rule.roles === "*" || rule.roles.some((r) => subjectRoles.has(r));
382
+ const actionMatched = rule.actions === "*" || this.matchesAction(compiled, action);
383
+ const resourceMatched = rule.resources === "*" || rule.resources.includes(resource);
384
+ const conditionResults = [];
385
+ let allConditionsPassed = true;
386
+ if (roleMatched && actionMatched && resourceMatched && rule.conditions) {
387
+ for (let i = 0; i < rule.conditions.length; i++) {
388
+ try {
389
+ const result = await rule.conditions[i](ctx);
390
+ if (result !== true) {
391
+ conditionResults.push({ index: i, passed: false });
392
+ allConditionsPassed = false;
393
+ } else {
394
+ conditionResults.push({ index: i, passed: true });
395
+ }
396
+ } catch (err) {
397
+ conditionResults.push({
398
+ index: i,
399
+ passed: false,
400
+ error: err instanceof Error ? err.message : String(err)
401
+ });
402
+ allConditionsPassed = false;
403
+ }
404
+ }
405
+ }
406
+ const matched = roleMatched && actionMatched && resourceMatched && (!rule.conditions || rule.conditions.length === 0 || allConditionsPassed);
407
+ evaluatedRules.push({
408
+ rule,
409
+ roleMatched,
410
+ actionMatched,
411
+ resourceMatched,
412
+ conditionResults,
413
+ matched
414
+ });
415
+ if (matched && !firstMatch) {
416
+ firstMatch = rule;
417
+ }
418
+ }
419
+ const allowed = firstMatch != null ? firstMatch.effect === "allow" : !this._defaultDeny;
420
+ const effect = firstMatch?.effect ?? "default-deny";
421
+ const reason = firstMatch ? `Matched rule "${firstMatch.id}"${firstMatch.description ? `: ${firstMatch.description}` : ""}` : "No matching rule \u2014 default deny";
422
+ return {
423
+ allowed,
424
+ effect,
425
+ reason,
426
+ evaluatedRules,
427
+ durationMs: performance.now() - start
428
+ };
429
+ }
430
+ // -----------------------------------------------------------------------
431
+ // Fluent check API: can(subject).perform(action).on(resource)
432
+ // -----------------------------------------------------------------------
433
+ can(subject) {
434
+ return new PerformStep(this, subject);
435
+ }
436
+ // -----------------------------------------------------------------------
437
+ // Internal helpers
438
+ // -----------------------------------------------------------------------
439
+ validateInput(subject, action, resource) {
440
+ if (!subject || typeof subject.id !== "string") {
441
+ throw new Error("subject must be an object with a string id");
442
+ }
443
+ if (!Array.isArray(subject.roles)) {
444
+ throw new Error("subject.roles must be an array");
445
+ }
446
+ if (!action || typeof action !== "string") {
447
+ throw new Error("action must be a non-empty string");
448
+ }
449
+ if (!resource || typeof resource !== "string") {
450
+ throw new Error("resource must be a non-empty string");
451
+ }
452
+ }
453
+ enforceTenancy(subject, tenantId) {
454
+ if (!this._strictTenancy || tenantId != null) return;
455
+ const hasTenantScoped = subject.roles.some((r) => r.tenantId != null);
456
+ if (hasTenantScoped) {
457
+ throw new Error(
458
+ "strictTenancy is enabled and subject has tenant-scoped roles, but no tenantId was provided to evaluate(). This could cause cross-tenant privilege escalation. Pass an explicit tenantId."
459
+ );
460
+ }
461
+ }
462
+ buildContext(subject, action, resource, resourceContext, tenantId) {
463
+ return { subject, action, resource, resourceContext, tenantId };
464
+ }
465
+ evaluateConditionsSync(rule, ctx) {
466
+ if (!rule.conditions) return true;
467
+ for (let i = 0; i < rule.conditions.length; i++) {
468
+ try {
469
+ if (rule.conditions[i](ctx) !== true) return false;
470
+ } catch (err) {
471
+ this.emitConditionError(rule.id, i, err);
472
+ return false;
473
+ }
474
+ }
475
+ return true;
476
+ }
477
+ emitConditionError(ruleId, conditionIndex, error) {
478
+ if (this.conditionErrorHandler) {
479
+ try {
480
+ this.conditionErrorHandler({ ruleId, conditionIndex, error });
481
+ } catch {
482
+ }
483
+ }
484
+ }
485
+ matchRules(subject, action, resource, tenantId) {
486
+ const subjectRoles = this.resolveRoles(subject, tenantId);
487
+ const matched = this.compiled.filter((compiled) => {
488
+ const { rule } = compiled;
489
+ if (rule.roles !== "*" && !rule.roles.some((r) => subjectRoles.has(r))) return false;
490
+ if (rule.actions !== "*" && !this.matchesAction(compiled, action)) return false;
491
+ if (rule.resources !== "*" && !rule.resources.includes(resource)) return false;
492
+ return true;
493
+ });
494
+ return this.sortCandidates(matched);
495
+ }
496
+ sortCandidates(candidates) {
497
+ return candidates.sort((a, b) => {
498
+ const pa = a.rule.priority ?? 0;
499
+ const pb = b.rule.priority ?? 0;
500
+ if (pb !== pa) return pb - pa;
501
+ if (a.rule.effect === "deny" && b.rule.effect === "allow") return -1;
502
+ if (a.rule.effect === "allow" && b.rule.effect === "deny") return 1;
503
+ return 0;
504
+ });
505
+ }
506
+ matchesAction(compiled, action) {
507
+ const { rule, actionPatterns } = compiled;
508
+ if (rule.actions === "*") return true;
509
+ const actionStr = action;
510
+ if (rule.actions.includes(actionStr)) return true;
511
+ if (actionPatterns) {
512
+ for (const pattern of actionPatterns) {
513
+ if (pattern.test(actionStr)) return true;
514
+ }
515
+ }
516
+ return false;
517
+ }
518
+ resolveRoles(subject, tenantId) {
519
+ const directRoles = /* @__PURE__ */ new Set();
520
+ for (const assignment of subject.roles) {
521
+ if (tenantId == null || assignment.tenantId == null || assignment.tenantId === tenantId) {
522
+ directRoles.add(assignment.role);
523
+ }
524
+ }
525
+ if (!this.hierarchy) return directRoles;
526
+ const expanded = /* @__PURE__ */ new Set();
527
+ for (const role of directRoles) {
528
+ for (const r of this.hierarchy.resolve(role)) {
529
+ expanded.add(r);
530
+ }
531
+ }
532
+ return expanded;
533
+ }
534
+ buildDecision(matched, ctx, start) {
535
+ const allowed = matched != null ? matched.effect === "allow" : !this._defaultDeny;
536
+ const effect = matched?.effect ?? "default-deny";
537
+ const reason = matched ? `Matched rule "${matched.id}"${matched.description ? `: ${matched.description}` : ""}` : "No matching rule \u2014 default deny";
538
+ return {
539
+ allowed,
540
+ effect,
541
+ matchedRule: matched,
542
+ subject: ctx.subject,
543
+ action: ctx.action,
544
+ resource: ctx.resource,
545
+ resourceContext: ctx.resourceContext,
546
+ tenantId: ctx.tenantId,
547
+ timestamp: Date.now(),
548
+ durationMs: performance.now() - start,
549
+ reason
550
+ };
551
+ }
552
+ };
553
+ var PerformStep = class {
554
+ constructor(engine, subject) {
555
+ this.engine = engine;
556
+ this.subject = subject;
557
+ }
558
+ perform(action) {
559
+ return new OnStep(this.engine, this.subject, action);
560
+ }
561
+ };
562
+ var OnStep = class {
563
+ constructor(engine, subject, action) {
564
+ this.engine = engine;
565
+ this.subject = subject;
566
+ this.action = action;
567
+ }
568
+ on(resource, resourceContext = {}, tenantId) {
569
+ return this.engine.evaluate(
570
+ this.subject,
571
+ this.action,
572
+ resource,
573
+ resourceContext,
574
+ tenantId
575
+ );
576
+ }
577
+ async onAsync(resource, resourceContext = {}, tenantId) {
578
+ return this.engine.evaluateAsync(
579
+ this.subject,
580
+ this.action,
581
+ resource,
582
+ resourceContext,
583
+ tenantId
584
+ );
585
+ }
586
+ };
587
+ function buildCacheKey(subjectId, action, resource, tenantId) {
588
+ return `${subjectId.length}:${subjectId}\0${action}\0${resource}\0${tenantId ?? ""}`;
589
+ }
590
+ var LRUCache = class {
591
+ map = /* @__PURE__ */ new Map();
592
+ maxSize;
593
+ constructor(maxSize) {
594
+ this.maxSize = maxSize;
595
+ }
596
+ get size() {
597
+ return this.map.size;
598
+ }
599
+ get(key) {
600
+ const value = this.map.get(key);
601
+ if (value !== void 0) {
602
+ this.map.delete(key);
603
+ this.map.set(key, value);
604
+ }
605
+ return value;
606
+ }
607
+ set(key, value) {
608
+ if (this.map.has(key)) {
609
+ this.map.delete(key);
610
+ } else if (this.map.size >= this.maxSize) {
611
+ const oldest = this.map.keys().next().value;
612
+ if (oldest !== void 0) this.map.delete(oldest);
613
+ }
614
+ this.map.set(key, value);
615
+ }
616
+ clear() {
617
+ this.map.clear();
618
+ }
619
+ };
620
+
621
+ // src/role-hierarchy.ts
622
+ var RoleHierarchy = class {
623
+ parents = /* @__PURE__ */ new Map();
624
+ cache = /* @__PURE__ */ new Map();
625
+ /**
626
+ * Define that `role` inherits permissions from `inheritsFrom` roles.
627
+ * Clears the resolution cache.
628
+ */
629
+ define(role, inheritsFrom) {
630
+ this.parents.set(role, inheritsFrom);
631
+ this.cache.clear();
632
+ this.detectCycle(role, /* @__PURE__ */ new Set());
633
+ return this;
634
+ }
635
+ /**
636
+ * Resolve the full set of roles a given role expands to,
637
+ * including all inherited roles (transitive).
638
+ */
639
+ resolve(role) {
640
+ const roleStr = role;
641
+ const cached = this.cache.get(roleStr);
642
+ if (cached) return cached;
643
+ const result = /* @__PURE__ */ new Set();
644
+ this.walk(roleStr, result);
645
+ this.cache.set(roleStr, result);
646
+ return result;
647
+ }
648
+ /**
649
+ * Resolve multiple roles at once, returning the merged set.
650
+ */
651
+ resolveAll(roles) {
652
+ const result = /* @__PURE__ */ new Set();
653
+ for (const role of roles) {
654
+ for (const r of this.resolve(role)) {
655
+ result.add(r);
656
+ }
657
+ }
658
+ return result;
659
+ }
660
+ /**
661
+ * Get all defined roles that have inheritance rules.
662
+ */
663
+ definedRoles() {
664
+ return [...this.parents.keys()];
665
+ }
666
+ walk(role, visited) {
667
+ if (visited.has(role)) return;
668
+ visited.add(role);
669
+ const parents = this.parents.get(role);
670
+ if (parents) {
671
+ for (const parent of parents) {
672
+ this.walk(parent, visited);
673
+ }
674
+ }
675
+ }
676
+ detectCycle(role, visiting) {
677
+ if (visiting.has(role)) {
678
+ throw new Error(
679
+ `Cycle detected in role hierarchy: ${[...visiting, role].join(" \u2192 ")}`
680
+ );
681
+ }
682
+ visiting.add(role);
683
+ const parents = this.parents.get(role);
684
+ if (parents) {
685
+ for (const parent of parents) {
686
+ this.detectCycle(parent, visiting);
687
+ }
688
+ }
689
+ visiting.delete(role);
690
+ }
691
+ };
692
+
693
+ // src/serialization.ts
694
+ var ConditionRegistry = class {
695
+ conditions = /* @__PURE__ */ new Map();
696
+ register(name, condition) {
697
+ if (!name || typeof name !== "string") {
698
+ throw new Error("Condition name must be a non-empty string");
699
+ }
700
+ if (typeof condition !== "function") {
701
+ throw new Error(`Condition "${name}" must be a function`);
702
+ }
703
+ this.conditions.set(name, condition);
704
+ return this;
705
+ }
706
+ get(name) {
707
+ return this.conditions.get(name);
708
+ }
709
+ has(name) {
710
+ return this.conditions.has(name);
711
+ }
712
+ names() {
713
+ return [...this.conditions.keys()];
714
+ }
715
+ };
716
+ function exportRules(rules, conditionNames) {
717
+ const jsonRules = rules.map((rule) => {
718
+ const jr = {
719
+ id: rule.id,
720
+ effect: rule.effect,
721
+ roles: rule.roles,
722
+ actions: rule.actions,
723
+ resources: rule.resources,
724
+ priority: rule.priority,
725
+ description: rule.description
726
+ };
727
+ if (rule.conditions && conditionNames) {
728
+ const names = [];
729
+ for (const cond of rule.conditions) {
730
+ const name = conditionNames.get(cond);
731
+ if (name) names.push(name);
732
+ }
733
+ if (names.length > 0) jr.conditions = names;
734
+ }
735
+ return jr;
736
+ });
737
+ return { version: 1, rules: jsonRules };
738
+ }
739
+ function exportRulesToJson(rules, conditionNames) {
740
+ return JSON.stringify(exportRules(rules, conditionNames), null, 2);
741
+ }
742
+ function importRules(doc, registry) {
743
+ if (!doc || typeof doc !== "object") {
744
+ throw new Error("Policy document must be a non-null object");
745
+ }
746
+ if (doc.version !== 1) {
747
+ throw new Error(`Unsupported policy document version: ${doc.version}`);
748
+ }
749
+ if (!Array.isArray(doc.rules)) {
750
+ throw new Error("Policy document must have a 'rules' array");
751
+ }
752
+ return doc.rules.map((jr, index) => {
753
+ if (!jr || typeof jr !== "object") {
754
+ throw new Error(`Rule at index ${index} must be a non-null object`);
755
+ }
756
+ if (!jr.id || typeof jr.id !== "string") {
757
+ throw new Error(`Rule at index ${index} is missing a valid "id" field.`);
758
+ }
759
+ if (jr.effect !== "allow" && jr.effect !== "deny") {
760
+ throw new Error(
761
+ `Invalid effect "${jr.effect}" in rule "${jr.id}". Must be "allow" or "deny".`
762
+ );
763
+ }
764
+ if (jr.roles !== "*" && !Array.isArray(jr.roles)) {
765
+ throw new Error(`Rule "${jr.id}": roles must be "*" or an array of strings`);
766
+ }
767
+ if (jr.actions !== "*" && !Array.isArray(jr.actions)) {
768
+ throw new Error(`Rule "${jr.id}": actions must be "*" or an array of strings`);
769
+ }
770
+ if (jr.resources !== "*" && !Array.isArray(jr.resources)) {
771
+ throw new Error(`Rule "${jr.id}": resources must be "*" or an array of strings`);
772
+ }
773
+ const conditions = [];
774
+ if (jr.conditions && registry) {
775
+ for (const name of jr.conditions) {
776
+ const cond = registry.get(name);
777
+ if (!cond) {
778
+ throw new Error(
779
+ `Unknown condition "${name}" in rule "${jr.id}". Registered conditions: ${registry.names().join(", ") || "(none)"}`
780
+ );
781
+ }
782
+ conditions.push(cond);
783
+ }
784
+ }
785
+ return {
786
+ id: jr.id,
787
+ effect: jr.effect,
788
+ roles: jr.roles,
789
+ actions: jr.actions,
790
+ resources: jr.resources,
791
+ conditions: conditions.length > 0 ? conditions : void 0,
792
+ priority: jr.priority,
793
+ description: jr.description
794
+ };
795
+ });
796
+ }
797
+ function importRulesFromJson(json, registry) {
798
+ let doc;
799
+ try {
800
+ doc = JSON.parse(json);
801
+ } catch (err) {
802
+ throw new Error(
803
+ `Failed to parse policy JSON: ${err instanceof Error ? err.message : String(err)}`
804
+ );
805
+ }
806
+ return importRules(doc, registry);
807
+ }
808
+
809
+ // src/types.ts
810
+ function toAuditEntry(decision) {
811
+ return {
812
+ allowed: decision.allowed,
813
+ effect: decision.effect,
814
+ matchedRuleId: decision.matchedRule?.id ?? null,
815
+ matchedRuleDescription: decision.matchedRule?.description ?? null,
816
+ subjectId: decision.subject.id,
817
+ action: decision.action,
818
+ resource: decision.resource,
819
+ tenantId: decision.tenantId,
820
+ timestamp: decision.timestamp,
821
+ durationMs: decision.durationMs,
822
+ reason: decision.reason
823
+ };
824
+ }
825
+ export {
826
+ AccessEngine,
827
+ ConditionRegistry,
828
+ RoleHierarchy,
829
+ RuleBuilder,
830
+ allow,
831
+ createPolicyFactory,
832
+ deny,
833
+ exportRules,
834
+ exportRulesToJson,
835
+ importRules,
836
+ importRulesFromJson,
837
+ toAuditEntry
838
+ };
7
839
  //# sourceMappingURL=index.js.map