@cldmv/slothlet 3.2.3 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +20 -5
  2. package/dist/lib/builders/api_builder.mjs +233 -3
  3. package/dist/lib/handlers/api-manager.mjs +23 -0
  4. package/dist/lib/handlers/context-async.mjs +4 -0
  5. package/dist/lib/handlers/context-live.mjs +5 -0
  6. package/dist/lib/handlers/hook-manager.mjs +5 -111
  7. package/dist/lib/handlers/permission-manager.mjs +408 -0
  8. package/dist/lib/handlers/unified-wrapper.mjs +90 -22
  9. package/dist/lib/helpers/config.mjs +91 -7
  10. package/dist/lib/helpers/pattern-matcher.mjs +141 -0
  11. package/dist/lib/i18n/languages/de-de.json +21 -1
  12. package/dist/lib/i18n/languages/en-gb.json +21 -1
  13. package/dist/lib/i18n/languages/en-us.json +21 -1
  14. package/dist/lib/i18n/languages/es-mx.json +21 -1
  15. package/dist/lib/i18n/languages/fr-fr.json +21 -1
  16. package/dist/lib/i18n/languages/hi-in.json +21 -1
  17. package/dist/lib/i18n/languages/ja-jp.json +21 -1
  18. package/dist/lib/i18n/languages/ko-kr.json +21 -1
  19. package/dist/lib/i18n/languages/pt-br.json +21 -1
  20. package/dist/lib/i18n/languages/ru-ru.json +21 -1
  21. package/dist/lib/i18n/languages/zh-cn.json +21 -1
  22. package/dist/slothlet.mjs +11 -2
  23. package/package.json +4 -1
  24. package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
  25. package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
  26. package/types/dist/lib/handlers/context-async.d.mts.map +1 -1
  27. package/types/dist/lib/handlers/context-live.d.mts.map +1 -1
  28. package/types/dist/lib/handlers/hook-manager.d.mts.map +1 -1
  29. package/types/dist/lib/handlers/permission-manager.d.mts +151 -0
  30. package/types/dist/lib/handlers/permission-manager.d.mts.map +1 -0
  31. package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
  32. package/types/dist/lib/helpers/config.d.mts +16 -0
  33. package/types/dist/lib/helpers/config.d.mts.map +1 -1
  34. package/types/dist/lib/helpers/pattern-matcher.d.mts +44 -0
  35. package/types/dist/lib/helpers/pattern-matcher.d.mts.map +1 -0
  36. package/types/dist/slothlet.d.mts.map +1 -1
@@ -0,0 +1,408 @@
1
+ /*
2
+ Copyright 2026 CLDMV/Shinrai
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+
18
+
19
+
20
+
21
+ import { ComponentBase } from "@cldmv/slothlet/factories/component-base";
22
+ import { compilePattern } from "@cldmv/slothlet/helpers/pattern-matcher";
23
+ import { translate } from "@cldmv/slothlet/i18n";
24
+
25
+
26
+ let ruleIdCounter = 0;
27
+
28
+
29
+ export class PermissionManager extends ComponentBase {
30
+
31
+ static slothletProperty = "permissionManager";
32
+
33
+
34
+ #rules = new Map();
35
+
36
+
37
+ #defaultPolicy = "allow";
38
+
39
+
40
+ #enabled = false;
41
+
42
+
43
+ #audit = "default";
44
+
45
+
46
+ #resolvedCache = new Map();
47
+
48
+
49
+ #compiledCache = new Map();
50
+
51
+
52
+ constructor(slothlet) {
53
+ super(slothlet);
54
+
55
+ const permConfig = slothlet.config?.permissions;
56
+ if (permConfig) {
57
+
58
+
59
+ this.#defaultPolicy = permConfig.defaultPolicy || "allow";
60
+ this.#enabled = permConfig.enabled !== false;
61
+ this.#audit = permConfig.audit || "default";
62
+
63
+
64
+ if (Array.isArray(permConfig.rules)) {
65
+ for (const rule of permConfig.rules) {
66
+ this.addRule(rule, null);
67
+ }
68
+ }
69
+ }
70
+
71
+
72
+ this.addRule({ caller: "**", target: "slothlet.permissions.control.**", effect: "deny" }, "__builtin__");
73
+ }
74
+
75
+
76
+ addRule(rule, ownerModuleID = null, ruleId = null) {
77
+ this.#validateRule(rule);
78
+
79
+ const id = ruleId || `perm-${++ruleIdCounter}`;
80
+ const entry = {
81
+ id,
82
+ caller: rule.caller,
83
+ target: rule.target,
84
+ effect: rule.effect,
85
+ ownerModuleID: ownerModuleID,
86
+ registeredAt: Date.now()
87
+ };
88
+
89
+ this.#rules.set(id, entry);
90
+ this.#clearCache();
91
+
92
+ this.debug("permissions", {
93
+ key: "DEBUG_PERMISSION_RULE_ADDED",
94
+ ruleId: id,
95
+ caller: rule.caller,
96
+ target: rule.target,
97
+ effect: rule.effect,
98
+ ownerModuleID
99
+ });
100
+
101
+ return id;
102
+ }
103
+
104
+
105
+ removeRule(ruleId, callerModuleID = null) {
106
+ const entry = this.#rules.get(ruleId);
107
+ if (!entry) return false;
108
+
109
+
110
+
111
+
112
+
113
+
114
+ if (callerModuleID && entry.ownerModuleID && callerModuleID === entry.ownerModuleID) {
115
+ throw new this.SlothletError("PERMISSION_SELF_MODIFY", {
116
+ ruleId,
117
+ moduleID: callerModuleID
118
+ });
119
+ }
120
+
121
+ this.#rules.delete(ruleId);
122
+ this.#clearCache();
123
+
124
+ this.debug("permissions", {
125
+ key: "DEBUG_PERMISSION_RULE_REMOVED",
126
+ ruleId,
127
+ caller: entry.caller,
128
+ target: entry.target,
129
+ effect: entry.effect
130
+ });
131
+
132
+ return true;
133
+ }
134
+
135
+
136
+ checkAccess(callerPath, targetPath, callerFilePath = null, targetFilePath = null) {
137
+
138
+
139
+
140
+
141
+
142
+
143
+ const isControlTarget = targetPath?.startsWith("slothlet.permissions.control.");
144
+ if (!this.#enabled && !isControlTarget) return true;
145
+
146
+
147
+ if (callerFilePath && targetFilePath && callerFilePath === targetFilePath) {
148
+ this.#emitAuditEvent("permission:self-bypass", {
149
+ caller: callerPath,
150
+ target: targetPath,
151
+ filePath: callerFilePath
152
+ });
153
+ return true;
154
+ }
155
+
156
+
157
+ const cacheKey = `${callerPath}::${targetPath}`;
158
+ if (this.#resolvedCache.has(cacheKey)) {
159
+ const cached = this.#resolvedCache.get(cacheKey);
160
+ this.#emitAuditEvent(cached.event, cached.payload);
161
+ return cached.allowed;
162
+ }
163
+
164
+
165
+ const entry = this.#evaluate(callerPath, targetPath);
166
+ this.#resolvedCache.set(cacheKey, entry);
167
+ this.#emitAuditEvent(entry.event, entry.payload);
168
+ return entry.allowed;
169
+ }
170
+
171
+
172
+ getRulesForPath(targetPath) {
173
+ const matching = [];
174
+ for (const entry of this.#rules.values()) {
175
+ const targetMatcher = this.#getCompiledPattern(entry.target);
176
+ if (targetMatcher(targetPath)) {
177
+ matching.push(this.#serializeRule(entry));
178
+ }
179
+ }
180
+ return matching;
181
+ }
182
+
183
+
184
+ getRulesByModule(moduleID) {
185
+ const matching = [];
186
+ for (const entry of this.#rules.values()) {
187
+ if (entry.ownerModuleID === moduleID) {
188
+ matching.push(this.#serializeRule(entry));
189
+ }
190
+ }
191
+ return matching;
192
+ }
193
+
194
+
195
+ getRulesForCaller(callerPath) {
196
+ const matching = [];
197
+ for (const entry of this.#rules.values()) {
198
+ const callerMatcher = this.#getCompiledPattern(entry.caller);
199
+ if (callerMatcher(callerPath)) {
200
+ matching.push(this.#serializeRule(entry));
201
+ }
202
+ }
203
+ return matching;
204
+ }
205
+
206
+
207
+ enable() {
208
+ this.#enabled = true;
209
+ this.#clearCache();
210
+ }
211
+
212
+
213
+ disable() {
214
+ this.#enabled = false;
215
+ this.#clearCache();
216
+ }
217
+
218
+
219
+ isEnabled() {
220
+ return this.#enabled;
221
+ }
222
+
223
+
224
+
225
+
226
+
227
+ exportRules() {
228
+ const rules = [];
229
+ for (const entry of this.#rules.values()) {
230
+ rules.push({
231
+ rule: { caller: entry.caller, target: entry.target, effect: entry.effect },
232
+ ownerModuleID: entry.ownerModuleID
233
+ });
234
+ }
235
+ return rules;
236
+ }
237
+
238
+
239
+ importRules(registrations) {
240
+ if (!Array.isArray(registrations)) return;
241
+ for (const reg of registrations) {
242
+ this.addRule(reg.rule, reg.ownerModuleID);
243
+ }
244
+ }
245
+
246
+
247
+
248
+ async shutdown() {
249
+ this.#rules.clear();
250
+ this.#resolvedCache.clear();
251
+ this.#compiledCache.clear();
252
+ this.#enabled = false;
253
+ this.#defaultPolicy = "allow";
254
+ this.#audit = "default";
255
+ }
256
+
257
+
258
+
259
+
260
+ #validateRule(rule) {
261
+ if (!rule || typeof rule !== "object") {
262
+ throw new this.SlothletError("INVALID_PERMISSION_RULE", {
263
+ reason: translate("PERM_RULE_NOT_OBJECT"),
264
+ received: typeof rule
265
+ });
266
+ }
267
+ if (typeof rule.caller !== "string" || !rule.caller) {
268
+ throw new this.SlothletError("INVALID_PERMISSION_RULE", {
269
+ reason: translate("PERM_RULE_CALLER_REQUIRED"),
270
+ received: typeof rule.caller
271
+ });
272
+ }
273
+ if (typeof rule.target !== "string" || !rule.target) {
274
+ throw new this.SlothletError("INVALID_PERMISSION_RULE", {
275
+ reason: translate("PERM_RULE_TARGET_REQUIRED"),
276
+ received: typeof rule.target
277
+ });
278
+ }
279
+ if (rule.effect !== "allow" && rule.effect !== "deny") {
280
+ throw new this.SlothletError("INVALID_PERMISSION_RULE", {
281
+ reason: translate("PERM_RULE_EFFECT_INVALID"),
282
+ received: rule.effect
283
+ });
284
+ }
285
+ }
286
+
287
+
288
+ #evaluate(callerPath, targetPath) {
289
+
290
+ const matches = [];
291
+
292
+ for (const entry of this.#rules.values()) {
293
+ const callerMatcher = this.#getCompiledPattern(entry.caller);
294
+ const targetMatcher = this.#getCompiledPattern(entry.target);
295
+
296
+ if (callerMatcher(callerPath) && targetMatcher(targetPath)) {
297
+ matches.push(entry);
298
+ }
299
+ }
300
+
301
+
302
+ if (matches.length === 0) {
303
+ const allowed = this.#defaultPolicy === "allow";
304
+ return {
305
+ allowed,
306
+ event: "permission:default",
307
+ payload: { caller: callerPath, target: targetPath, policy: this.#defaultPolicy }
308
+ };
309
+ }
310
+
311
+
312
+ matches.sort((a, b) => {
313
+ const specA = this.#computeSpecificity(a, callerPath, targetPath);
314
+ const specB = this.#computeSpecificity(b, callerPath, targetPath);
315
+ if (specA !== specB) return specB - specA;
316
+ return a.registeredAt - b.registeredAt;
317
+ });
318
+
319
+
320
+ const highestSpec = this.#computeSpecificity(matches[0], callerPath, targetPath);
321
+ const topTier = matches.filter((m) => this.#computeSpecificity(m, callerPath, targetPath) === highestSpec);
322
+
323
+ const winner = topTier[topTier.length - 1];
324
+ const allowed = winner.effect === "allow";
325
+
326
+ return {
327
+ allowed,
328
+ event: allowed ? "permission:allowed" : "permission:denied",
329
+ payload: { caller: callerPath, target: targetPath, rule: this.#serializeRule(winner) }
330
+ };
331
+ }
332
+
333
+
334
+ #computeSpecificity(entry, callerPath, targetPath) {
335
+ return this.#patternSpecificity(entry.caller, callerPath) + this.#patternSpecificity(entry.target, targetPath);
336
+ }
337
+
338
+
339
+ #patternSpecificity(pattern, _path) {
340
+
341
+ if (!pattern.includes("*") && !pattern.includes("?") && !pattern.includes("{")) {
342
+ return 3;
343
+ }
344
+
345
+ if (pattern.includes("**")) {
346
+ return 1;
347
+ }
348
+
349
+ return 2;
350
+ }
351
+
352
+
353
+ #getCompiledPattern(pattern) {
354
+ let matcher = this.#compiledCache.get(pattern);
355
+ if (!matcher) {
356
+ matcher = compilePattern(pattern);
357
+ this.#compiledCache.set(pattern, matcher);
358
+ }
359
+ return matcher;
360
+ }
361
+
362
+
363
+ #clearCache() {
364
+ this.#resolvedCache.clear();
365
+ }
366
+
367
+
368
+ #emitAuditEvent(event, payload) {
369
+
370
+ this.debug("permissions", {
371
+ key:
372
+ event === "permission:denied"
373
+ ? "DEBUG_PERMISSION_DENIED"
374
+ : event === "permission:allowed"
375
+ ? "DEBUG_PERMISSION_ALLOWED"
376
+ : event === "permission:self-bypass"
377
+ ? "DEBUG_PERMISSION_SELF_BYPASS"
378
+ : "DEBUG_PERMISSION_DEFAULT",
379
+ ...payload
380
+ });
381
+
382
+
383
+ const alwaysEmit = event === "permission:denied" || event === "permission:self-bypass";
384
+ if (!alwaysEmit && this.#audit !== "verbose") return;
385
+
386
+ const lifecycle = this.slothlet.handlers?.lifecycle;
387
+ if (lifecycle) {
388
+ lifecycle.emit(event, { ...payload, timestamp: Date.now() });
389
+ }
390
+ }
391
+
392
+
393
+ #serializeRule(entry) {
394
+ return {
395
+ id: entry.id,
396
+ caller: entry.caller,
397
+ target: entry.target,
398
+ effect: entry.effect,
399
+ ownerModuleID: entry.ownerModuleID,
400
+ registeredAt: entry.registeredAt
401
+ };
402
+ }
403
+
404
+
405
+ debug(category, data) {
406
+ this.slothlet.debug(category, data);
407
+ }
408
+ }
@@ -1654,6 +1654,13 @@ export class UnifiedWrapper extends ComponentBase {
1654
1654
  },
1655
1655
 
1656
1656
  async apply(___target, ___thisArg, args) {
1657
+
1658
+
1659
+
1660
+
1661
+
1662
+ const ___capturedCallerWrapper = wrapper.slothlet.contextManager?.tryGetContext?.()?.currentWrapper ?? null;
1663
+
1657
1664
  wrapper.slothlet.debug("wrapper", {
1658
1665
  key: "DEBUG_MODE_WAITING_APPLY_ENTRY",
1659
1666
  apiPath: wrapper.____slothletInternal.apiPath,
@@ -1834,6 +1841,27 @@ export class UnifiedWrapper extends ComponentBase {
1834
1841
 
1835
1842
 
1836
1843
 
1844
+
1845
+
1846
+
1847
+
1848
+
1849
+
1850
+ if (___capturedCallerWrapper && wrapper.slothlet.contextManager) {
1851
+ const ___instanceStore = wrapper.slothlet.contextManager.instances?.get?.(wrapper.instanceID);
1852
+
1853
+
1854
+
1855
+ if (___instanceStore && !___instanceStore.currentWrapper) {
1856
+ return wrapper.slothlet.contextManager.runInContext(
1857
+ wrapper.instanceID,
1858
+ () => Reflect.apply(current, lastObject, args),
1859
+ null,
1860
+ [],
1861
+ ___capturedCallerWrapper
1862
+ );
1863
+ }
1864
+ }
1837
1865
  return Reflect.apply(current, lastObject, args);
1838
1866
  }
1839
1867
 
@@ -2540,6 +2568,35 @@ export class UnifiedWrapper extends ComponentBase {
2540
2568
  }
2541
2569
 
2542
2570
 
2571
+
2572
+
2573
+ const permissionManager = wrapper.slothlet.handlers?.permissionManager;
2574
+ if (permissionManager && permissionManager.isEnabled()) {
2575
+ const ctx = wrapper.slothlet.contextManager?.tryGetContext?.();
2576
+
2577
+
2578
+ const callerWrapper = ctx?.currentWrapper;
2579
+
2580
+
2581
+ if (callerWrapper) {
2582
+
2583
+
2584
+ const callerPath = callerWrapper.____slothletInternal?.apiPath ?? "";
2585
+ const callerFilePath = callerWrapper.____slothletInternal?.filePath ?? null;
2586
+ const targetPath = wrapper.____slothletInternal.apiPath;
2587
+ const targetFilePath = wrapper.____slothletInternal.filePath ?? null;
2588
+
2589
+
2590
+ if (!permissionManager.checkAccess(callerPath, targetPath, callerFilePath, targetFilePath)) {
2591
+ throw new wrapper.SlothletError("PERMISSION_DENIED", {
2592
+ caller: callerPath,
2593
+ target: targetPath
2594
+ });
2595
+ }
2596
+ }
2597
+ }
2598
+
2599
+
2543
2600
  const hookManager = wrapper.slothlet.handlers?.hookManager;
2544
2601
 
2545
2602
  const hasHooks = hookManager && hookManager.enabled && !wrapper.____slothletInternal.apiPath.startsWith("slothlet.hook");
@@ -2584,33 +2641,43 @@ export class UnifiedWrapper extends ComponentBase {
2584
2641
  const checkMaterialized = () => {
2585
2642
  if (wrapper.____slothletInternal.state.materialized) {
2586
2643
  const impl = wrapper.____slothletInternal.impl;
2587
- if (typeof impl === "function") {
2588
- if (wrapper.slothlet.contextManager) {
2589
- resolve(wrapper.slothlet.contextManager.runInContext(wrapper.instanceID, impl, thisArg, args, wrapper));
2590
- } else {
2591
- resolve(impl.apply(thisArg, args));
2592
- }
2593
- } else if (impl && typeof impl === "object" && typeof impl.default === "function") {
2594
- if (wrapper.contextManager) {
2595
- resolve(wrapper.contextManager.runInContext(wrapper.instanceID, impl.default, impl, args, wrapper));
2644
+ try {
2645
+ if (typeof impl === "function") {
2646
+ if (wrapper.slothlet.contextManager) {
2647
+ resolve(wrapper.slothlet.contextManager.runInContext(wrapper.instanceID, impl, thisArg, args, wrapper));
2648
+ } else {
2649
+ resolve(impl.apply(thisArg, args));
2650
+ }
2651
+ } else if (impl && typeof impl === "object" && typeof impl.default === "function") {
2652
+ if (wrapper.contextManager) {
2653
+ resolve(wrapper.contextManager.runInContext(wrapper.instanceID, impl.default, impl, args, wrapper));
2654
+ } else {
2655
+ resolve(impl.default.apply(impl, args));
2656
+ }
2596
2657
  } else {
2597
- resolve(impl.default.apply(impl, args));
2658
+ reject(
2659
+ new wrapper.slothlet.SlothletError(
2660
+ "INVALID_CONFIG_NOT_A_FUNCTION",
2661
+ {
2662
+ apiPath: wrapper.____slothletInternal.apiPath,
2663
+ actualType: typeof impl
2664
+ },
2665
+ null,
2666
+ { validationError: true }
2667
+ )
2668
+ );
2598
2669
  }
2599
- } else {
2600
- reject(
2601
- new wrapper.slothlet.SlothletError(
2602
- "INVALID_CONFIG_NOT_A_FUNCTION",
2603
- {
2604
- apiPath: wrapper.____slothletInternal.apiPath,
2605
- actualType: typeof impl
2606
- },
2607
- null,
2608
- { validationError: true }
2609
- )
2610
- );
2670
+ } catch (err) {
2671
+
2672
+
2673
+
2674
+ reject(err);
2611
2675
  }
2612
2676
  return;
2613
2677
  }
2678
+
2679
+
2680
+
2614
2681
  if (!wrapper.____slothletInternal.state.inFlight) {
2615
2682
  reject(
2616
2683
  new wrapper.slothlet.SlothletError(
@@ -2623,6 +2690,7 @@ export class UnifiedWrapper extends ComponentBase {
2623
2690
  )
2624
2691
  );
2625
2692
  return;
2693
+
2626
2694
  }
2627
2695
  setImmediate(checkMaterialized);
2628
2696
  };
@@ -18,6 +18,7 @@
18
18
 
19
19
 
20
20
  import { ComponentBase } from "@cldmv/slothlet/factories/component-base";
21
+ import { SlothletError } from "@cldmv/slothlet/errors";
21
22
 
22
23
 
23
24
  export class Config extends ComponentBase {
@@ -98,7 +99,7 @@ export class Config extends ComponentBase {
98
99
 
99
100
 
100
101
  normalizeMutations(mutations) {
101
- const defaults = { add: true, remove: true, reload: true };
102
+ const defaults = { add: true, remove: true, reload: true, permissions: true };
102
103
 
103
104
 
104
105
  if (!mutations || typeof mutations !== "object") {
@@ -109,7 +110,8 @@ export class Config extends ComponentBase {
109
110
  return {
110
111
  add: mutations.add === false ? false : true,
111
112
  remove: mutations.remove === false ? false : true,
112
- reload: mutations.reload === false ? false : true
113
+ reload: mutations.reload === false ? false : true,
114
+ permissions: mutations.permissions === false ? false : true
113
115
  };
114
116
  }
115
117
 
@@ -126,7 +128,8 @@ export class Config extends ComponentBase {
126
128
  context: false,
127
129
  initialization: false,
128
130
  materialize: false,
129
- versioning: false
131
+ versioning: false,
132
+ permissions: false
130
133
  };
131
134
  }
132
135
 
@@ -142,7 +145,8 @@ export class Config extends ComponentBase {
142
145
  context: true,
143
146
  initialization: true,
144
147
  materialize: true,
145
- versioning: true
148
+ versioning: true,
149
+ permissions: true
146
150
  };
147
151
  }
148
152
 
@@ -158,7 +162,8 @@ export class Config extends ComponentBase {
158
162
  context: debug.context || false,
159
163
  initialization: debug.initialization || false,
160
164
  materialize: debug.materialize || false,
161
- versioning: debug.versioning || false
165
+ versioning: debug.versioning || false,
166
+ permissions: debug.permissions || false
162
167
  };
163
168
  }
164
169
 
@@ -173,7 +178,8 @@ export class Config extends ComponentBase {
173
178
  context: false,
174
179
  initialization: false,
175
180
  materialize: false,
176
- versioning: false
181
+ versioning: false,
182
+ permissions: false
177
183
  };
178
184
  }
179
185
 
@@ -284,6 +290,9 @@ export class Config extends ComponentBase {
284
290
  }
285
291
 
286
292
 
293
+ const permissionsConfig = this.normalizePermissions(config.permissions);
294
+
295
+
287
296
  let i18nConfig = null;
288
297
  if (config.i18n && typeof config.i18n === "object") {
289
298
  i18nConfig = {
@@ -315,7 +324,8 @@ export class Config extends ComponentBase {
315
324
  silent: config.silent === true,
316
325
  typescript: this.normalizeTypeScript(config.typescript),
317
326
  env: this.normalizeEnv(config.env),
318
- versionDispatcher: config.versionDispatcher ?? null
327
+ versionDispatcher: config.versionDispatcher ?? null,
328
+ permissions: permissionsConfig
319
329
  };
320
330
  }
321
331
 
@@ -368,4 +378,78 @@ export class Config extends ComponentBase {
368
378
  }
369
379
  return null;
370
380
  }
381
+
382
+
383
+ normalizePermissions(permissions) {
384
+ if (!permissions || typeof permissions !== "object") {
385
+ return null;
386
+ }
387
+
388
+
389
+ let defaultPolicy;
390
+ if (permissions.defaultPolicy === "deny") {
391
+ defaultPolicy = "deny";
392
+ } else if (permissions.defaultPolicy === "allow" || permissions.defaultPolicy === undefined) {
393
+ defaultPolicy = "allow";
394
+ } else {
395
+ throw new SlothletError(
396
+ "INVALID_CONFIG",
397
+ {
398
+ option: "permissions.defaultPolicy",
399
+ value: permissions.defaultPolicy,
400
+ expected: '"allow" or "deny"',
401
+ hint: "HINT_INVALID_CONFIG"
402
+ },
403
+ null,
404
+ { validationError: true }
405
+ );
406
+ }
407
+
408
+ const enabled = permissions.enabled !== false;
409
+
410
+
411
+ let audit;
412
+ if (permissions.audit === "verbose") {
413
+ audit = "verbose";
414
+ } else if (permissions.audit === "default" || permissions.audit === undefined) {
415
+ audit = "default";
416
+ } else if (permissions.audit === true) {
417
+
418
+ audit = "default";
419
+ } else if (permissions.audit === false) {
420
+
421
+ audit = "default";
422
+ } else {
423
+ throw new SlothletError(
424
+ "INVALID_CONFIG",
425
+ {
426
+ option: "permissions.audit",
427
+ value: permissions.audit,
428
+ expected: '"default" or "verbose"',
429
+ hint: "HINT_INVALID_CONFIG"
430
+ },
431
+ null,
432
+ { validationError: true }
433
+ );
434
+ }
435
+
436
+
437
+ if (permissions.rules !== undefined && !Array.isArray(permissions.rules)) {
438
+ throw new SlothletError(
439
+ "INVALID_CONFIG",
440
+ {
441
+ option: "permissions.rules",
442
+ value: permissions.rules,
443
+ expected: "array",
444
+ hint: "HINT_INVALID_CONFIG"
445
+ },
446
+ null,
447
+ { validationError: true }
448
+ );
449
+ }
450
+
451
+ const rules = Array.isArray(permissions.rules) ? permissions.rules : [];
452
+
453
+ return { defaultPolicy, enabled, audit, rules };
454
+ }
371
455
  }