@cldmv/slothlet 3.2.1 → 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 (38) hide show
  1. package/README.md +24 -10
  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/handlers/version-manager.mjs +77 -4
  10. package/dist/lib/helpers/config.mjs +91 -7
  11. package/dist/lib/helpers/pattern-matcher.mjs +141 -0
  12. package/dist/lib/i18n/languages/de-de.json +21 -1
  13. package/dist/lib/i18n/languages/en-gb.json +21 -1
  14. package/dist/lib/i18n/languages/en-us.json +21 -1
  15. package/dist/lib/i18n/languages/es-mx.json +21 -1
  16. package/dist/lib/i18n/languages/fr-fr.json +21 -1
  17. package/dist/lib/i18n/languages/hi-in.json +21 -1
  18. package/dist/lib/i18n/languages/ja-jp.json +21 -1
  19. package/dist/lib/i18n/languages/ko-kr.json +21 -1
  20. package/dist/lib/i18n/languages/pt-br.json +21 -1
  21. package/dist/lib/i18n/languages/ru-ru.json +21 -1
  22. package/dist/lib/i18n/languages/zh-cn.json +21 -1
  23. package/dist/slothlet.mjs +11 -2
  24. package/package.json +4 -1
  25. package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
  26. package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
  27. package/types/dist/lib/handlers/context-async.d.mts.map +1 -1
  28. package/types/dist/lib/handlers/context-live.d.mts.map +1 -1
  29. package/types/dist/lib/handlers/hook-manager.d.mts.map +1 -1
  30. package/types/dist/lib/handlers/permission-manager.d.mts +151 -0
  31. package/types/dist/lib/handlers/permission-manager.d.mts.map +1 -0
  32. package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
  33. package/types/dist/lib/handlers/version-manager.d.mts.map +1 -1
  34. package/types/dist/lib/helpers/config.d.mts +16 -0
  35. package/types/dist/lib/helpers/config.d.mts.map +1 -1
  36. package/types/dist/lib/helpers/pattern-matcher.d.mts +44 -0
  37. package/types/dist/lib/helpers/pattern-matcher.d.mts.map +1 -0
  38. package/types/dist/slothlet.d.mts.map +1 -1
package/README.md CHANGED
@@ -55,18 +55,18 @@ Every feature has been hardened with a comprehensive test suite - over **5,300 t
55
55
 
56
56
  ## ✨ What's New
57
57
 
58
- ### Latest: v3.2.1 (April 2026)
58
+ ### Latest: v3.3.0 (April 2026)
59
59
 
60
- - **Missing `defineProperty` trap** — the version dispatcher proxy introduced in v3.2.0 had no `defineProperty` trap; calls fell through to the raw target, bypassing version routing, and non-configurable writes could trigger V8 proxy invariant `TypeError` on subsequent reads
61
- - **Pre-commit tooling** — removed duplicate `build:cleanup` step from `precommit-validation.mjs`; sequence is now `build:dev debug test:node → vitest`
62
- - [View full v3.2.1 Changelog](./docs/changelog/v3/v3.2.1.md)
60
+ - **Permission System** — path-based access control for inter-module calls with glob pattern rules, most-specific-wins evaluation, audit events, and runtime `api.slothlet.permissions.*` management API
61
+ - **Shared pattern matcher** — extracted hook system's glob compilation into a shared utility for reuse across hooks and permissions
62
+ - [View full v3.3.0 Changelog](./docs/changelog/v3/v3.3.0.md)
63
63
 
64
64
  ### Recent Releases
65
65
 
66
+ - **v3.2.3** (April 2026) — publish workflow fix ([Changelog](./docs/changelog/v3/v3.2.3.md))
67
+ - **v3.2.2** (April 2026) — missing `set` trap on version dispatchers; `util.inspect(api.auth)` now shows resolved versioned namespace ([Changelog](./docs/changelog/v3/v3.2.2.md))
68
+ - **v3.2.1** (April 2026) — version-dispatcher `defineProperty` trap fix; pre-commit validation cleanup ([Changelog](./docs/changelog/v3/v3.2.1.md))
66
69
  - **v3.2.0** (April 2026) — API Path Versioning (`versionDispatcher`, `api.slothlet.versioning.*`, version metadata, dispatcher proxy); lazy-mode shutdown race fix ([Changelog](./docs/changelog/v3/v3.2.0.md))
67
- - **v3.1.0** (March 2026) — Frozen `api.slothlet.env` snapshot; `env.include` allowlist; reload immunity ([Changelog](./docs/changelog/v3/v3.1.0.md))
68
- - **v3.0.1** (March 2026) — Resolver fix for user `index.mjs` mis-classified as internal; CI `slothlet-dev` stripping hardening; respawn race fix; resilient `build:dist` script ([Changelog](./docs/changelog/v3/v3.0.1.md))
69
- - **v3.0.0** (February 2026) — Unified Wrapper architecture, redesigned hook system, full i18n, background materialization, lifecycle events, collision modes, mutation controls ([Changelog](./docs/changelog/v3.0.md))
70
70
 
71
71
 
72
72
  📚 **For complete version history and detailed release notes, see [docs/changelog/](./docs/changelog/) folder.**
@@ -114,6 +114,19 @@ Each hook type supports three ordered execution **subsets**: `"before"` → `"pr
114
114
 
115
115
  🎣 **For complete hook system documentation, see [docs/HOOKS.md](https://github.com/CLDMV/slothlet/blob/master/docs/HOOKS.md)**
116
116
 
117
+ ### 🔐 **Permission System** _(new in v3.3)_
118
+
119
+ Path-based access control for inter-module API calls:
120
+
121
+ - **Glob pattern rules** — same `*`, `**`, `?`, `{a,b}` syntax as hooks
122
+ - **Most-specific-wins** — exact patterns override broad globs; tiebreak by registration order
123
+ - **Self-call bypass** — calls within the same source file always succeed
124
+ - **Enforcement before hooks** — denied calls never trigger `before:` hooks or function execution
125
+ - **Audit events** — `permission:denied`, `permission:allowed`, `permission:default`, `permission:self-bypass`
126
+ - **Runtime management** — `api.slothlet.permissions.addRule()`, `.removeRule()`, `.self.*`, `.global.*`, `.control.*`
127
+
128
+ 🔐 **For complete permission system documentation, see [docs/PERMISSIONS.md](https://github.com/CLDMV/slothlet/blob/master/docs/PERMISSIONS.md)**
129
+
117
130
  ### 🌍 **Full Internationalization** _(new in v3)_
118
131
 
119
132
  All error messages and debug output are translated. Supported languages:
@@ -154,7 +167,7 @@ Automatic context preservation across all asynchronous boundaries:
154
167
  - **TypeScript-Friendly**: Comprehensive JSDoc annotations with auto-generated declarations
155
168
  - **Configurable Debug**: Detailed logging via CLI flags or environment variables
156
169
  - **Multiple Instances**: Parameter-based isolation for complex applications
157
- - **Inspectable APIs**: `console.log(api.math)` now shows real module contents (v3+)
170
+ - **Inspectable APIs**: `console.log(api.math)` and logical versioned paths like `console.log(api.auth)` show real module contents instead of proxy internals (v3+)
158
171
  - **Development Checks**: Built-in environment detection with silent production behavior
159
172
 
160
173
  ---
@@ -325,8 +338,9 @@ await api.slothlet.api.reload("database.*");
325
338
  | `hook` | `mixed` | `false` | Enable hook system: `true` (enable all), `"pattern"` (enable with pattern), or object with `enabled`, `pattern`, `suppressErrors` options - **note: `hook` singular, not `hooks`** |
326
339
  | `backgroundMaterialize` | `boolean` | `false` | In lazy mode: start background pre-loading of all modules immediately after init; automatically enables materialization tracking and the `materialized:complete` lifecycle event |
327
340
  | `api.collision` | `mixed` | `"merge"` | Collision mode for API namespace conflicts: `"merge"`, `"skip"`, `"overwrite"`, `"throw"` - or `{ initial: "merge", api: "skip" }` to set independently for load vs runtime `add()` |
328
- | `api.mutations` | `object` | all `true` | Per-operation mutation controls: `{ add: true, remove: true, reload: true }` - set any to `false` to disable |
341
+ | `api.mutations` | `object` | all `true` | Per-operation mutation controls: `{ add: true, remove: true, reload: true, permissions: true }` - set any to `false` to disable |
329
342
  | `versionDispatcher` | `mixed` | `undefined` | Version routing discriminator: `"version"` (or any string key) looks up that key in the caller's version metadata; a function receives `(allVersions, caller)` and returns a tag or `null`; `undefined` behaves like `"version"` |
343
+ | `permissions` | `object` | `undefined` | Permission system config: `{ defaultPolicy: "allow"\|"deny", enabled: true, audit: "default"\|"verbose", rules: [...] }` — see [PERMISSIONS.md](./docs/PERMISSIONS.md) |
330
344
  | `i18n` | `object` | `{}` | Internationalization settings: `{ language: "en" }` - supported: `en`, `es`, `fr`, `de`, `pt`, `it`, `ja`, `zh`, `ko` |
331
345
 
332
346
  ---
@@ -969,7 +983,7 @@ try {
969
983
  - **Debug Mode**: Comprehensive i18n-translated logging via `--slothletdebug` flag or `SLOTHLET_DEBUG=true`
970
984
  - **Development Check**: `devcheck.mjs` for environment validation
971
985
  - **Source Detection**: Automatic `src/` vs `dist/` mode detection
972
- - **API Inspection**: `console.log(api.math)` shows real module contents (v3+)
986
+ - **API Inspection**: `console.log(api.math)` and versioned dispatcher paths like `console.log(api.auth)` show real module contents (v3+)
973
987
 
974
988
  ---
975
989
 
@@ -582,9 +582,7 @@ export class ApiBuilder extends ComponentBase {
582
582
  setForVersion: function slothlet_metadata_setForVersion(logicalPath, versionTag, keyOrObj, value) {
583
583
  if (!slothlet.handlers?.metadata) {
584
584
  throw new slothlet.SlothletError("METADATA_NOT_AVAILABLE", {
585
- handlersKeys: slothlet.handlers
586
- ? Object.keys(slothlet.handlers).join(", ")
587
- : "undefined",
585
+ handlersKeys: slothlet.handlers ? Object.keys(slothlet.handlers).join(", ") : "undefined",
588
586
  validationError: true
589
587
  });
590
588
  }
@@ -734,6 +732,238 @@ export class ApiBuilder extends ComponentBase {
734
732
  if (!slothlet.handlers?.versionManager) return;
735
733
  return slothlet.handlers.versionManager.setVersionMetadataByPath(logicalPath, versionTag, patch);
736
734
  }
735
+ },
736
+
737
+
738
+ permissions: {
739
+
740
+ addRule: function slothlet_permissions_addRule(rule) {
741
+
742
+ if (!config.api?.mutations?.permissions) {
743
+ throw new slothlet.SlothletError("INVALID_CONFIG_MUTATIONS_DISABLED", {
744
+ operation: "api.slothlet.permissions.addRule",
745
+ validationError: true
746
+ });
747
+ }
748
+
749
+
750
+ const permissionManager = slothlet.handlers?.permissionManager;
751
+
752
+ if (!permissionManager?.addRule) {
753
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
754
+ validationError: true
755
+ });
756
+ }
757
+
758
+
759
+ const ruleId = permissionManager.addRule(rule, null);
760
+
761
+
762
+ if (slothlet.handlers?.apiManager?.state?.operationHistory) {
763
+ slothlet.handlers.apiManager.state.operationHistory.push({
764
+ type: "addPermissionRule",
765
+ rule,
766
+ ownerModuleID: null,
767
+ ruleId,
768
+ timestamp: Date.now()
769
+ });
770
+ }
771
+
772
+ return ruleId;
773
+ },
774
+
775
+
776
+ removeRule: function slothlet_permissions_removeRule(ruleId) {
777
+
778
+ if (!config.api?.mutations?.permissions) {
779
+ throw new slothlet.SlothletError("INVALID_CONFIG_MUTATIONS_DISABLED", {
780
+ operation: "api.slothlet.permissions.removeRule",
781
+ validationError: true
782
+ });
783
+ }
784
+
785
+
786
+ const permissionManager = slothlet.handlers?.permissionManager;
787
+
788
+ if (!permissionManager?.removeRule) {
789
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
790
+ validationError: true
791
+ });
792
+ }
793
+
794
+
795
+ const ctx = slothlet.contextManager?.tryGetContext?.();
796
+ const currentWrapper = ctx?.currentWrapper;
797
+ const callerModuleID = currentWrapper?.____slothletInternal?.moduleID ?? null;
798
+ const result = slothlet.handlers.permissionManager.removeRule(ruleId, callerModuleID);
799
+
800
+
801
+ if (result && slothlet.handlers?.apiManager?.state?.operationHistory) {
802
+ slothlet.handlers.apiManager.state.operationHistory.push({
803
+ type: "removePermissionRule",
804
+ ruleId,
805
+ callerModuleID,
806
+ timestamp: Date.now()
807
+ });
808
+ }
809
+
810
+ return result;
811
+ },
812
+
813
+
814
+ self: {
815
+
816
+ access: function slothlet_permissions_self_access(target) {
817
+
818
+ const permissionManager = slothlet.handlers?.permissionManager;
819
+
820
+ if (!permissionManager?.checkAccess) {
821
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
822
+ validationError: true
823
+ });
824
+ }
825
+
826
+
827
+ const currentWrapper = slothlet.contextManager?.tryGetContext?.()?.currentWrapper;
828
+ const callerPath = currentWrapper?.____slothletInternal?.apiPath ?? "";
829
+ const callerFilePath = currentWrapper?.____slothletInternal?.filePath ?? null;
830
+ return slothlet.handlers.permissionManager.checkAccess(callerPath, target, callerFilePath, null);
831
+ },
832
+
833
+
834
+ rules: function slothlet_permissions_self_rules() {
835
+
836
+ const permissionManager = slothlet.handlers?.permissionManager;
837
+
838
+ if (!permissionManager?.getRulesForCaller) {
839
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
840
+ validationError: true
841
+ });
842
+ }
843
+
844
+
845
+ const currentWrapper = slothlet.contextManager?.tryGetContext?.()?.currentWrapper;
846
+ const callerPath = currentWrapper?.____slothletInternal?.apiPath ?? "";
847
+ return slothlet.handlers.permissionManager.getRulesForCaller(callerPath);
848
+ }
849
+ },
850
+
851
+
852
+ global: {
853
+
854
+ checkAccess: function slothlet_permissions_global_checkAccess(caller, target) {
855
+
856
+ const permissionManager = slothlet.handlers?.permissionManager;
857
+
858
+ if (!permissionManager?.checkAccess) {
859
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
860
+ validationError: true
861
+ });
862
+ }
863
+
864
+
865
+ return slothlet.handlers.permissionManager.checkAccess(caller, target);
866
+ },
867
+
868
+
869
+ rulesForPath: function slothlet_permissions_global_rulesForPath(path) {
870
+
871
+ const permissionManager = slothlet.handlers?.permissionManager;
872
+
873
+ if (!permissionManager?.getRulesForPath) {
874
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
875
+ validationError: true
876
+ });
877
+ }
878
+
879
+
880
+ return slothlet.handlers.permissionManager.getRulesForPath(path);
881
+ },
882
+
883
+
884
+ rulesByModule: function slothlet_permissions_global_rulesByModule(moduleID) {
885
+
886
+ const permissionManager = slothlet.handlers?.permissionManager;
887
+
888
+ if (!permissionManager?.getRulesByModule) {
889
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
890
+ validationError: true
891
+ });
892
+ }
893
+
894
+
895
+ return slothlet.handlers.permissionManager.getRulesByModule(moduleID);
896
+ }
897
+ },
898
+
899
+
900
+ control: {
901
+
902
+ enable: function slothlet_permissions_control_enable() {
903
+
904
+ const permissionManager = slothlet.handlers?.permissionManager;
905
+
906
+ if (!permissionManager?.enable) {
907
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
908
+ validationError: true
909
+ });
910
+ }
911
+
912
+
913
+
914
+
915
+ const ctx = slothlet.contextManager?.tryGetContext?.();
916
+ const callerWrapper = ctx?.currentWrapper;
917
+ if (callerWrapper) {
918
+
919
+
920
+ const callerPath = callerWrapper.____slothletInternal?.apiPath ?? "";
921
+ const callerFilePath = callerWrapper.____slothletInternal?.filePath ?? null;
922
+
923
+ if (!permissionManager.checkAccess(callerPath, "slothlet.permissions.control.enable", callerFilePath, null)) {
924
+ throw new slothlet.SlothletError("PERMISSION_DENIED", {
925
+ caller: callerPath,
926
+ target: "slothlet.permissions.control.enable"
927
+ });
928
+ }
929
+ }
930
+
931
+ slothlet.handlers.permissionManager.enable();
932
+ },
933
+
934
+
935
+ disable: function slothlet_permissions_control_disable() {
936
+
937
+ const permissionManager = slothlet.handlers?.permissionManager;
938
+
939
+ if (!permissionManager?.disable) {
940
+ throw new slothlet.SlothletError("PERMISSION_MANAGER_NOT_AVAILABLE", {
941
+ validationError: true
942
+ });
943
+ }
944
+
945
+
946
+
947
+
948
+ const ctx = slothlet.contextManager?.tryGetContext?.();
949
+ const callerWrapper = ctx?.currentWrapper;
950
+ if (callerWrapper) {
951
+
952
+
953
+ const callerPath = callerWrapper.____slothletInternal?.apiPath ?? "";
954
+ const callerFilePath = callerWrapper.____slothletInternal?.filePath ?? null;
955
+
956
+ if (!permissionManager.checkAccess(callerPath, "slothlet.permissions.control.disable", callerFilePath, null)) {
957
+ throw new slothlet.SlothletError("PERMISSION_DENIED", {
958
+ caller: callerPath,
959
+ target: "slothlet.permissions.control.disable"
960
+ });
961
+ }
962
+ }
963
+
964
+ slothlet.handlers.permissionManager.disable();
965
+ }
966
+ }
737
967
  }
738
968
  };
739
969
 
@@ -1380,6 +1380,29 @@ export class ApiManager extends ComponentBase {
1380
1380
  }
1381
1381
  }
1382
1382
 
1383
+
1384
+ if (restOptions.permissions && this.slothlet.handlers?.permissionManager) {
1385
+ const perms = restOptions.permissions;
1386
+ const callerPattern = `${normalizedPath}.**`;
1387
+
1388
+ if (Array.isArray(perms.deny)) {
1389
+ for (const target of perms.deny) {
1390
+ this.slothlet.handlers.permissionManager.addRule(
1391
+ { caller: callerPattern, target, effect: "deny" },
1392
+ moduleID
1393
+ );
1394
+ }
1395
+ }
1396
+ if (Array.isArray(perms.allow)) {
1397
+ for (const target of perms.allow) {
1398
+ this.slothlet.handlers.permissionManager.addRule(
1399
+ { caller: callerPattern, target, effect: "allow" },
1400
+ moduleID
1401
+ );
1402
+ }
1403
+ }
1404
+ }
1405
+
1383
1406
  return moduleID;
1384
1407
  }
1385
1408
 
@@ -98,6 +98,8 @@ export class AsyncContextManager {
98
98
  }
99
99
  return result;
100
100
  } catch (error) {
101
+
102
+ if (error instanceof SlothletError) throw error;
101
103
  throw new SlothletError(
102
104
  "CONTEXT_EXECUTION_FAILED",
103
105
  {
@@ -120,6 +122,8 @@ export class AsyncContextManager {
120
122
  }
121
123
  return result;
122
124
  } catch (error) {
125
+
126
+ if (error instanceof SlothletError) throw error;
123
127
  throw new SlothletError(
124
128
  "CONTEXT_EXECUTION_FAILED",
125
129
  {
@@ -87,6 +87,9 @@ export class LiveContextManager {
87
87
  const previousCallerWrapper = store.callerWrapper;
88
88
 
89
89
  this.currentInstanceID = targetInstanceID;
90
+
91
+
92
+
90
93
  if (currentWrapper) {
91
94
  store.callerWrapper = previousWrapper;
92
95
  store.currentWrapper = currentWrapper;
@@ -95,6 +98,8 @@ export class LiveContextManager {
95
98
  try {
96
99
  return fn.apply(thisArg, args);
97
100
  } catch (error) {
101
+
102
+ if (error instanceof SlothletError) throw error;
98
103
  throw new SlothletError(
99
104
  "CONTEXT_EXECUTION_FAILED",
100
105
  {
@@ -19,6 +19,7 @@
19
19
 
20
20
 
21
21
  import { ComponentBase } from "@cldmv/slothlet/factories/component-base";
22
+ import { compilePattern } from "@cldmv/slothlet/helpers/pattern-matcher";
22
23
 
23
24
 
24
25
 
@@ -511,118 +512,11 @@ export class HookManager extends ComponentBase {
511
512
 
512
513
 
513
514
  #compilePattern(pattern) {
514
-
515
- const isNegation = pattern.startsWith("!");
516
- if (isNegation) {
517
- pattern = pattern.slice(1);
518
- const matcher = this.#compilePattern(pattern);
519
- return (path) => !matcher(path);
520
- }
521
-
522
-
523
- const expanded = this.#expandBraces(pattern);
524
- if (expanded.length > 1) {
525
-
526
- const matchers = expanded.map((p) => this.#compilePattern(p));
527
- return (path) => matchers.some((m) => m(path));
528
- }
529
-
530
-
531
- pattern = expanded[0];
532
-
533
-
534
- let regexPattern = pattern
535
- .replace(/[+^$()|[\]\\]/g, "\\$&")
536
- .replace(/\*\*/g, "__DOUBLESTAR__")
537
- .replace(/\*/g, "[^.]*")
538
- .replace(/__DOUBLESTAR__/g, ".*")
539
- .replace(/\?/g, ".");
540
-
541
- regexPattern = `^${regexPattern}$`;
542
- const regex = new RegExp(regexPattern);
543
-
544
- return (path) => regex.test(path);
545
- }
546
-
547
-
548
- #expandBraces(pattern, depth = 0, maxDepth = 10) {
549
-
550
- if (depth >= maxDepth) {
551
- throw new this.SlothletError("HOOK_BRACE_EXPANSION_MAX_DEPTH", { maxDepth }, null, { validationError: true });
552
- }
553
-
554
-
555
- const braceStart = pattern.indexOf("{");
556
- if (braceStart === -1) {
557
- return [pattern];
558
- }
559
-
560
-
561
- let braceEnd = -1;
562
- let depth_count = 1;
563
- for (let i = braceStart + 1; i < pattern.length; i++) {
564
- if (pattern[i] === "{") depth_count++;
565
- if (pattern[i] === "}") {
566
- depth_count--;
567
- if (depth_count === 0) {
568
- braceEnd = i;
569
- break;
570
- }
571
- }
572
- }
573
-
574
- if (braceEnd === -1) {
575
- return [pattern];
576
- }
577
-
578
-
579
- const prefix = pattern.slice(0, braceStart);
580
- const braceContent = pattern.slice(braceStart + 1, braceEnd);
581
- const suffix = pattern.slice(braceEnd + 1);
582
-
583
-
584
- const alternatives = this.#splitBraceAlternatives(braceContent);
585
-
586
-
587
- const expanded = [];
588
- for (const alt of alternatives) {
589
- const combined = prefix + alt + suffix;
590
-
591
- const recursiveExpanded = this.#expandBraces(combined, depth + 1, maxDepth);
592
- expanded.push(...recursiveExpanded);
593
- }
594
-
595
- return expanded;
596
- }
597
-
598
-
599
- #splitBraceAlternatives(content) {
600
- const alternatives = [];
601
- let current = "";
602
- let depth = 0;
603
-
604
- for (let i = 0; i < content.length; i++) {
605
- const char = content[i];
606
-
607
- if (char === "{") {
608
- depth++;
609
- current += char;
610
- } else if (char === "}") {
611
- depth--;
612
- current += char;
613
- } else if (char === "," && depth === 0) {
614
- alternatives.push(current);
615
- current = "";
616
- } else {
617
- current += char;
515
+ return compilePattern(pattern, {
516
+ onMaxDepth: (maxDepth) => {
517
+ throw new this.SlothletError("HOOK_BRACE_EXPANSION_MAX_DEPTH", { maxDepth }, null, { validationError: true });
618
518
  }
619
- }
620
-
621
- if (current) {
622
- alternatives.push(current);
623
- }
624
-
625
- return alternatives;
519
+ });
626
520
  }
627
521
 
628
522