@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.
- package/README.md +20 -5
- package/dist/lib/builders/api_builder.mjs +233 -3
- package/dist/lib/handlers/api-manager.mjs +23 -0
- package/dist/lib/handlers/context-async.mjs +4 -0
- package/dist/lib/handlers/context-live.mjs +5 -0
- package/dist/lib/handlers/hook-manager.mjs +5 -111
- package/dist/lib/handlers/permission-manager.mjs +408 -0
- package/dist/lib/handlers/unified-wrapper.mjs +90 -22
- package/dist/lib/helpers/config.mjs +91 -7
- package/dist/lib/helpers/pattern-matcher.mjs +141 -0
- package/dist/lib/i18n/languages/de-de.json +21 -1
- package/dist/lib/i18n/languages/en-gb.json +21 -1
- package/dist/lib/i18n/languages/en-us.json +21 -1
- package/dist/lib/i18n/languages/es-mx.json +21 -1
- package/dist/lib/i18n/languages/fr-fr.json +21 -1
- package/dist/lib/i18n/languages/hi-in.json +21 -1
- package/dist/lib/i18n/languages/ja-jp.json +21 -1
- package/dist/lib/i18n/languages/ko-kr.json +21 -1
- package/dist/lib/i18n/languages/pt-br.json +21 -1
- package/dist/lib/i18n/languages/ru-ru.json +21 -1
- package/dist/lib/i18n/languages/zh-cn.json +21 -1
- package/dist/slothlet.mjs +11 -2
- package/package.json +4 -1
- package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
- package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/context-async.d.mts.map +1 -1
- package/types/dist/lib/handlers/context-live.d.mts.map +1 -1
- package/types/dist/lib/handlers/hook-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/permission-manager.d.mts +151 -0
- package/types/dist/lib/handlers/permission-manager.d.mts.map +1 -0
- package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
- package/types/dist/lib/helpers/config.d.mts +16 -0
- package/types/dist/lib/helpers/config.d.mts.map +1 -1
- package/types/dist/lib/helpers/pattern-matcher.d.mts +44 -0
- package/types/dist/lib/helpers/pattern-matcher.d.mts.map +1 -0
- package/types/dist/slothlet.d.mts.map +1 -1
package/README.md
CHANGED
|
@@ -55,17 +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.
|
|
58
|
+
### Latest: v3.3.0 (April 2026)
|
|
59
59
|
|
|
60
|
-
- **
|
|
61
|
-
-
|
|
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)
|
|
62
63
|
|
|
63
64
|
### Recent Releases
|
|
64
65
|
|
|
66
|
+
- **v3.2.3** (April 2026) — publish workflow fix ([Changelog](./docs/changelog/v3/v3.2.3.md))
|
|
65
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))
|
|
66
68
|
- **v3.2.1** (April 2026) — version-dispatcher `defineProperty` trap fix; pre-commit validation cleanup ([Changelog](./docs/changelog/v3/v3.2.1.md))
|
|
67
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))
|
|
68
|
-
- **v3.1.0** (March 2026) — Frozen `api.slothlet.env` snapshot; `env.include` allowlist; reload immunity ([Changelog](./docs/changelog/v3/v3.1.0.md))
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
📚 **For complete version history and detailed release notes, see [docs/changelog/](./docs/changelog/) folder.**
|
|
@@ -113,6 +114,19 @@ Each hook type supports three ordered execution **subsets**: `"before"` → `"pr
|
|
|
113
114
|
|
|
114
115
|
🎣 **For complete hook system documentation, see [docs/HOOKS.md](https://github.com/CLDMV/slothlet/blob/master/docs/HOOKS.md)**
|
|
115
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
|
+
|
|
116
130
|
### 🌍 **Full Internationalization** _(new in v3)_
|
|
117
131
|
|
|
118
132
|
All error messages and debug output are translated. Supported languages:
|
|
@@ -324,8 +338,9 @@ await api.slothlet.api.reload("database.*");
|
|
|
324
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`** |
|
|
325
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 |
|
|
326
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()` |
|
|
327
|
-
| `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 |
|
|
328
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) |
|
|
329
344
|
| `i18n` | `object` | `{}` | Internationalization settings: `{ language: "en" }` - supported: `en`, `es`, `fr`, `de`, `pt`, `it`, `ja`, `zh`, `ko` |
|
|
330
345
|
|
|
331
346
|
---
|
|
@@ -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
|
-
|
|
516
|
-
|
|
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
|
|