assign-gingerly 0.0.78 → 0.0.80

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/assignGingerly.ts CHANGED
@@ -506,9 +506,10 @@ export function evaluatePathWithMethods(
506
506
  value: any,
507
507
  withMethods: Set<string>,
508
508
  permissionProcessor?: PermissionProcessor
509
- ): { target: any; lastKey: string; isMethod: boolean; isZeroArg: boolean } {
509
+ ): { target: any; lastKey: string; isMethod: boolean; isZeroArg: boolean; lastSegmentConsumed: boolean } {
510
510
  let current = target;
511
511
  let i = 0;
512
+ let lastSegmentConsumed = false;
512
513
 
513
514
  // Process all segments except the last one
514
515
  while (i < pathParts.length - 1) {
@@ -525,13 +526,17 @@ export function evaluatePathWithMethods(
525
526
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
526
527
  const method = current[methodName];
527
528
  if (typeof method === 'function') {
529
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
528
530
  if (isZeroArgMethod || nextIsMethod) {
529
531
  // Zero-arg call - next part is either a method or explicitly not an argument
530
- current = method.call(current);
532
+ current = method.call(current, ...appendArgs);
531
533
  } else {
532
534
  // Only current is method - call with next part as string arg
533
- current = method.call(current, nextPart);
535
+ current = method.call(current, nextPart, ...appendArgs);
534
536
  i++; // Skip next part since we consumed it as argument
537
+ if (i === pathParts.length - 1) {
538
+ lastSegmentConsumed = true;
539
+ }
535
540
  }
536
541
  } else {
537
542
  // Not a function - just access property (create if needed)
@@ -560,7 +565,8 @@ export function evaluatePathWithMethods(
560
565
  target: current,
561
566
  lastKey,
562
567
  isMethod: isAllowedMethod(lastKey, withMethods, permissionProcessor),
563
- isZeroArg
568
+ isZeroArg,
569
+ lastSegmentConsumed
564
570
  };
565
571
  }
566
572
 
@@ -649,8 +655,9 @@ function applyToEach(
649
655
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
650
656
  const method = current[methodName];
651
657
  if (typeof method === 'function') {
658
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
652
659
  if (isZeroArgMethod) {
653
- current = method.call(current);
660
+ current = method.call(current, ...appendArgs);
654
661
  continue;
655
662
  }
656
663
  // For methods in the middle, we need to check the next part
@@ -659,13 +666,13 @@ function applyToEach(
659
666
  const nextIsMethod = nextPart && (withMethods.has(nextPart)
660
667
  || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
661
668
  if (nextIsMethod) {
662
- current = method.call(current);
669
+ current = method.call(current, ...appendArgs);
663
670
  } else if (nextPart) {
664
- current = method.call(current, nextPart);
671
+ current = method.call(current, nextPart, ...appendArgs);
665
672
  // Skip next part
666
673
  pathToForEach.splice(nextIndex, 1);
667
674
  } else {
668
- current = method.call(current);
675
+ current = method.call(current, ...appendArgs);
669
676
  }
670
677
  } else {
671
678
  current = current[methodName];
@@ -687,13 +694,14 @@ function applyToEach(
687
694
  // Last segment is a method - call it
688
695
  const method = result.target[result.lastKey];
689
696
  if (typeof method === 'function') {
697
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
690
698
  if (result.isZeroArg) {
691
- // Trailing | marker - call with no arguments, ignoring the value
692
- method.call(result.target);
699
+ // Trailing | marker - call with appended args only, ignoring the value
700
+ method.call(result.target, ...appendArgs);
693
701
  } else if (Array.isArray(value)) {
694
- method.apply(result.target, value);
702
+ method.apply(result.target, [...value, ...appendArgs]);
695
703
  } else {
696
- method.call(result.target, value);
704
+ method.call(result.target, value, ...appendArgs);
697
705
  }
698
706
  }
699
707
  } else {
@@ -721,6 +729,110 @@ function applyToEach(
721
729
  }
722
730
  }
723
731
 
732
+ /**
733
+ * Resolve the RHS of a toggle (=!) command to the value that should be negated.
734
+ * Supports:
735
+ * - '?.path' nested path strings resolved against target (returns the resolved value, or true if missing)
736
+ * - plain key strings looked up on target (returns the value, or true if missing)
737
+ * - resolved literal values (e.g., booleans from assignFrom) returned as-is
738
+ */
739
+ function resolveValueToNegate(rhsPath: any, target: any): any {
740
+ if (typeof rhsPath === 'string' && isNestedPath(rhsPath)) {
741
+ const rhsPathParts = parsePath(rhsPath);
742
+ let current = target;
743
+ let exists = true;
744
+ for (const part of rhsPathParts) {
745
+ if (current && typeof current === 'object' && part in current) {
746
+ current = current[part];
747
+ } else {
748
+ exists = false;
749
+ break;
750
+ }
751
+ }
752
+ return exists ? current : true;
753
+ }
754
+ if (typeof rhsPath === 'string') {
755
+ return (rhsPath in target) ? target[rhsPath] : true;
756
+ }
757
+ // Non-string resolved value (e.g., boolean, number, null from assignFrom)
758
+ return rhsPath;
759
+ }
760
+
761
+ /**
762
+ * Navigate a path that leads to an iterable, returning the iterable value or undefined.
763
+ * Handles withMethods and the evaluatePathWithMethods semantics.
764
+ */
765
+ function getIterableAtPath(
766
+ target: any,
767
+ pathParts: string[],
768
+ value: any,
769
+ withMethods: Set<string> | undefined,
770
+ permissionProcessor?: PermissionProcessor
771
+ ): any {
772
+ let current = target;
773
+ if (pathParts.length > 0) {
774
+ if (withMethods && withMethods.size > 0) {
775
+ const result = evaluatePathWithMethods(target, pathParts, value, withMethods, permissionProcessor);
776
+ // evaluatePathWithMethods returns the container object + last key by default.
777
+ // If the last segment was consumed as a method argument, or the last segment
778
+ // is a zero-arg method marked with |, result.target is already the value.
779
+ if (result.lastSegmentConsumed) {
780
+ current = result.target;
781
+ } else if (result.isMethod) {
782
+ const method = result.target[result.lastKey];
783
+ if (typeof method === 'function') {
784
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
785
+ current = method.call(result.target, ...appendArgs);
786
+ } else {
787
+ current = method;
788
+ }
789
+ } else {
790
+ current = result.target[result.lastKey];
791
+ }
792
+ } else {
793
+ for (const part of pathParts) {
794
+ current = current[part];
795
+ }
796
+ }
797
+ }
798
+ return isIterable(current) ? current : undefined;
799
+ }
800
+
801
+ /**
802
+ * Apply an operator command (+=, =!, -=, Y=) to each item in an iterable.
803
+ * Detects @each in the path, navigates to the iterable, then builds a synthetic
804
+ * command key for the remaining path and delegates to assignGingerly per item.
805
+ * Nested @each is handled recursively through assignGingerly.
806
+ */
807
+ function applyCommandToEach(
808
+ target: any,
809
+ pathParts: string[],
810
+ commandSuffix: string,
811
+ value: any,
812
+ withMethods: Set<string> | undefined,
813
+ aliasMap: Map<string, string>,
814
+ options?: IAssignGingerlyOptions,
815
+ permissionProcessor?: PermissionProcessor
816
+ ): void {
817
+ const forEachIndex = pathParts.findIndex(part => isForEachSymbol(part, aliasMap));
818
+ if (forEachIndex === -1) return;
819
+
820
+ const pathToForEach = pathParts.slice(0, forEachIndex);
821
+ const pathAfterForEach = pathParts.slice(forEachIndex + 1);
822
+
823
+ const iterable = getIterableAtPath(target, pathToForEach, value, withMethods, permissionProcessor);
824
+ if (!iterable) return;
825
+
826
+ const items = Array.isArray(iterable) ? iterable : Array.from(iterable);
827
+ const syntheticKey = pathAfterForEach.length > 0
828
+ ? `?.${pathAfterForEach.join('?.')}${commandSuffix}`
829
+ : commandSuffix;
830
+
831
+ for (const item of items) {
832
+ assignGingerly(item, { [syntheticKey]: value }, options, permissionProcessor);
833
+ }
834
+ }
835
+
724
836
  /**
725
837
  * Apply alias substitutions to a key string.
726
838
  * Replaces complete tokens between `?.` delimiters with their aliased values.
@@ -842,6 +954,12 @@ export function assignGingerly(
842
954
  if (isNestedPath(path)) {
843
955
  const pathParts = parsePath(path);
844
956
 
957
+ // Check for @each in path
958
+ if (pathParts.some(part => isForEachSymbol(part, aliasMap))) {
959
+ applyCommandToEach(target, pathParts, ' +=', value, withMethodsSet, aliasMap, options, permissionProcessor);
960
+ continue;
961
+ }
962
+
845
963
  // Check for withMethods path evaluation
846
964
  let lhsValue: any;
847
965
  let lhsParent: any;
@@ -912,52 +1030,51 @@ export function assignGingerly(
912
1030
  const lhsPath = parseToggleCommand(key);
913
1031
  if (lhsPath) {
914
1032
  const rhsPath = value;
915
-
916
- // Resolve LHS
917
- let lhsParent: any;
918
- let lhsLastKey: string;
1033
+
919
1034
  if (isNestedPath(lhsPath)) {
920
1035
  const lhsPathParts = parsePath(lhsPath);
921
- lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
922
- lhsParent = ensureNestedPath(target, lhsPathParts);
923
- } else {
924
- lhsLastKey = lhsPath;
925
- lhsParent = target;
926
- }
927
1036
 
928
- // Determine what to negate
929
- let valueToNegate;
930
- if (rhsPath === '.') {
931
- // Self-reference: negate the LHS value itself (if it exists)
932
- if (lhsLastKey in lhsParent) {
933
- valueToNegate = lhsParent[lhsLastKey];
1037
+ // Check for @each in the LHS path
1038
+ if (lhsPathParts.some(part => isForEachSymbol(part, aliasMap))) {
1039
+ // Resolve non-self-referencing RHS paths against the original target
1040
+ // before iterating, so each item negates the same root value.
1041
+ const resolvedValue = (rhsPath === '.' || typeof rhsPath !== 'string')
1042
+ ? rhsPath
1043
+ : resolveValueToNegate(rhsPath, target);
1044
+ applyCommandToEach(target, lhsPathParts, ' =!', resolvedValue, withMethodsSet, aliasMap, options, permissionProcessor);
1045
+ continue;
1046
+ }
1047
+
1048
+ // No @each in path - standard toggle
1049
+ const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
1050
+ const lhsParent = ensureNestedPath(target, lhsPathParts);
1051
+
1052
+ // Determine what to negate
1053
+ let valueToNegate: any;
1054
+ if (rhsPath === '.') {
1055
+ valueToNegate = (lhsLastKey in lhsParent) ? lhsParent[lhsLastKey] : undefined;
934
1056
  } else {
935
- valueToNegate = undefined;
1057
+ valueToNegate = resolveValueToNegate(rhsPath, target);
1058
+ }
1059
+
1060
+ if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
1061
+ lhsParent[lhsLastKey] = !valueToNegate;
936
1062
  }
937
1063
  } else {
938
- // RHS path: navigate to get the value (don't create paths)
939
- if (isNestedPath(rhsPath)) {
940
- const rhsPathParts = parsePath(rhsPath);
941
- let current = target;
942
- let exists = true;
943
- for (const part of rhsPathParts) {
944
- if (current && typeof current === 'object' && part in current) {
945
- current = current[part];
946
- } else {
947
- exists = false;
948
- break;
949
- }
950
- }
951
- valueToNegate = exists ? current : true;
1064
+ // Plain key LHS
1065
+ const lhsLastKey = lhsPath;
1066
+ const lhsParent = target;
1067
+
1068
+ let valueToNegate: any;
1069
+ if (rhsPath === '.') {
1070
+ valueToNegate = (lhsLastKey in lhsParent) ? lhsParent[lhsLastKey] : undefined;
952
1071
  } else {
953
- // Plain key RHS
954
- valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
1072
+ valueToNegate = resolveValueToNegate(rhsPath, target);
1073
+ }
1074
+
1075
+ if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
1076
+ lhsParent[lhsLastKey] = !valueToNegate;
955
1077
  }
956
- }
957
-
958
- // Apply negation to LHS — check restriction first
959
- if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
960
- lhsParent[lhsLastKey] = !valueToNegate;
961
1078
  }
962
1079
  }
963
1080
  continue;
@@ -973,6 +1090,13 @@ export function assignGingerly(
973
1090
 
974
1091
  if (isNestedPath(path)) {
975
1092
  const pathParts = parsePath(path);
1093
+
1094
+ // Check for @each in path
1095
+ if (pathParts.some(part => isForEachSymbol(part, aliasMap))) {
1096
+ applyCommandToEach(target, pathParts, ' -=', value, withMethodsSet, aliasMap, options, permissionProcessor);
1097
+ continue;
1098
+ }
1099
+
976
1100
  if (pathParts.length === 0) {
977
1101
  parent = target;
978
1102
  } else {
@@ -1016,6 +1140,13 @@ export function assignGingerly(
1016
1140
  if (permissionProcessor?.checkRestrictedProp(lastKey)) {
1017
1141
  continue;
1018
1142
  }
1143
+
1144
+ // Check for @each in path
1145
+ if (isNestedPath(path) && pathParts.some(part => isForEachSymbol(part, aliasMap))) {
1146
+ applyCommandToEach(target, pathParts, ' Y=', value, withMethodsSet, aliasMap, options, permissionProcessor);
1147
+ continue;
1148
+ }
1149
+
1019
1150
  // Navigate to the target sub-object
1020
1151
  let mergeTarget: any;
1021
1152
  if (isNestedPath(path)) {
@@ -1083,24 +1214,12 @@ export function assignGingerly(
1083
1214
  // Static forEach (@each) - existing logic
1084
1215
  const pathToForEach = pathParts.slice(0, forEachIndex);
1085
1216
  const pathAfterForEach = pathParts.slice(forEachIndex + 1);
1086
-
1217
+
1087
1218
  // Navigate to the iterable
1088
- let current = target;
1089
- if (pathToForEach.length > 0) {
1090
- if (withMethodsSet) {
1091
- const result = evaluatePathWithMethods(target, pathToForEach, value, withMethodsSet, permissionProcessor);
1092
- // The result.target is the current position after evaluating the path
1093
- // This is already the iterable we want
1094
- current = result.target;
1095
- } else {
1096
- for (const part of pathToForEach) {
1097
- current = current[part];
1098
- }
1099
- }
1100
- }
1101
-
1219
+ const current = getIterableAtPath(target, pathToForEach, value, withMethodsSet, permissionProcessor);
1220
+
1102
1221
  // Apply to each item in the iterable
1103
- if (isIterable(current)) {
1222
+ if (current) {
1104
1223
  applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissionProcessor);
1105
1224
  }
1106
1225
  // If not iterable, let JavaScript throw error naturally when trying to iterate
@@ -1130,12 +1249,13 @@ export function assignGingerly(
1130
1249
  // Last segment is a method — call it
1131
1250
  const method = result.target[result.lastKey];
1132
1251
  if (typeof method === 'function') {
1133
- // Trailing | marker - call with no arguments, ignoring the value
1252
+ const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1253
+ // Trailing | marker - call with appended args only, ignoring the value
1134
1254
  const returnVal = result.isZeroArg
1135
- ? method.call(result.target)
1255
+ ? method.call(result.target, ...appendArgs)
1136
1256
  : Array.isArray(capturedValue)
1137
- ? method.apply(result.target, capturedValue)
1138
- : method.call(result.target, capturedValue);
1257
+ ? method.apply(result.target, [...capturedValue, ...appendArgs])
1258
+ : method.call(result.target, capturedValue, ...appendArgs);
1139
1259
  // If it's an async method, await it (for side effects)
1140
1260
  if (result.isAsyncMethod) await returnVal;
1141
1261
  }
@@ -1171,13 +1291,14 @@ export function assignGingerly(
1171
1291
  // Last segment is a method - call it
1172
1292
  const method = result.target[result.lastKey];
1173
1293
  if (typeof method === 'function') {
1294
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1174
1295
  if (result.isZeroArg) {
1175
- // Trailing | marker - call with no arguments, ignoring the value
1176
- method.call(result.target);
1296
+ // Trailing | marker - call with appended args only, ignoring the value
1297
+ method.call(result.target, ...appendArgs);
1177
1298
  } else if (Array.isArray(value)) {
1178
- method.apply(result.target, value);
1299
+ method.apply(result.target, [...value, ...appendArgs]);
1179
1300
  } else {
1180
- method.call(result.target, value);
1301
+ method.call(result.target, value, ...appendArgs);
1181
1302
  }
1182
1303
  }
1183
1304
  // Silently skip if not a function
@@ -1245,13 +1366,14 @@ export function assignGingerly(
1245
1366
  const methodName = isZeroArgKey ? key.slice(0, -1) : key;
1246
1367
  const method = target[methodName];
1247
1368
  if (typeof method === 'function') {
1369
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
1248
1370
  if (isZeroArgKey) {
1249
- // Trailing | marker - call with no arguments, ignoring the value
1250
- method.call(target);
1371
+ // Trailing | marker - call with appended args only, ignoring the value
1372
+ method.call(target, ...appendArgs);
1251
1373
  } else if (Array.isArray(value)) {
1252
- method.apply(target, value);
1374
+ method.apply(target, [...value, ...appendArgs]);
1253
1375
  } else {
1254
- method.call(target, value);
1376
+ method.call(target, value, ...appendArgs);
1255
1377
  }
1256
1378
  }
1257
1379
  // Silently skip if not a function
@@ -1,11 +1,14 @@
1
1
  import { isAllowedUrl } from './isAllowedUrl.js';
2
+ import { getValue } from '../resolve/getValues.js';
2
3
  export class PermissionProcessor {
3
4
  constructor(permissions) {
4
5
  this.permissions = permissions;
5
6
  const { props, attrs } = buildMaps(permissions);
6
7
  this.props = props;
7
8
  this.attrs = attrs;
8
- this.methods = buildMethodSet(permissions);
9
+ const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
10
+ this.blockedMethods = blockedMethods;
11
+ this.configuredMethods = configuredMethods;
9
12
  this.warned = new Set();
10
13
  this.warnedMethods = new Set();
11
14
  }
@@ -25,11 +28,29 @@ export class PermissionProcessor {
25
28
  return true;
26
29
  }
27
30
  checkRestrictedMethod(methodName) {
28
- if (!this.methods.has(methodName))
31
+ if (!this.blockedMethods.has(methodName))
29
32
  return false;
30
33
  this.warnRestrictedMethod(methodName);
31
34
  return true;
32
35
  }
36
+ getMethodAppendArgs(methodName) {
37
+ const config = this.configuredMethods.get(methodName);
38
+ if (!config)
39
+ return undefined;
40
+ const rawArgs = config.appendArgs ?? config.addArgs;
41
+ if (!rawArgs || rawArgs.length === 0)
42
+ return undefined;
43
+ const resolved = [];
44
+ for (const arg of rawArgs) {
45
+ if (typeof arg === 'string' && arg.startsWith('?.')) {
46
+ resolved.push(getValue(arg, this.permissions));
47
+ }
48
+ else {
49
+ resolved.push(arg);
50
+ }
51
+ }
52
+ return resolved;
53
+ }
33
54
  redirectRestrictedProp(target, key, value) {
34
55
  if (!this.props.has(key))
35
56
  return false;
@@ -151,16 +172,22 @@ function normalizeAttrNames(attr, propNames) {
151
172
  return propNames;
152
173
  return normalizeStrings(attr);
153
174
  }
154
- function buildMethodSet(permissions) {
175
+ function buildMethodMaps(permissions) {
155
176
  const methodSettings = permissions?.restrictedMethodSettings;
156
- const methods = new Set();
177
+ const blockedMethods = new Set();
178
+ const configuredMethods = new Map();
157
179
  if (!methodSettings || methodSettings.length === 0) {
158
- return methods;
180
+ return { blockedMethods, configuredMethods };
159
181
  }
160
182
  for (const setting of methodSettings) {
161
183
  if (typeof setting === 'string') {
162
- methods.add(setting);
184
+ blockedMethods.add(setting);
185
+ continue;
186
+ }
187
+ if (configuredMethods.has(setting.method)) {
188
+ throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
163
189
  }
190
+ configuredMethods.set(setting.method, setting);
164
191
  }
165
- return methods;
192
+ return { blockedMethods, configuredMethods };
166
193
  }
@@ -1,4 +1,5 @@
1
- import type { AssignPermissions, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
1
+ import type { AssignPermissions, RestrictedMethodConfig, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
2
+ import { getValue } from '../resolve/getValues.js';
2
3
  import { isAllowedUrl } from './isAllowedUrl.js';
3
4
 
4
5
  export interface RestrictedPropSettingsMap {
@@ -10,7 +11,8 @@ export class PermissionProcessor {
10
11
  private readonly permissions: AssignPermissions | undefined;
11
12
  private readonly props: Map<string, RestrictedPropSetting | undefined>;
12
13
  private readonly attrs: Map<string, RestrictedPropSetting | undefined>;
13
- private readonly methods: Set<string>;
14
+ private readonly blockedMethods: Set<string>;
15
+ private readonly configuredMethods: Map<string, RestrictedMethodConfig>;
14
16
  private readonly warned = new Set<string>();
15
17
  private readonly warnedMethods = new Set<string>();
16
18
 
@@ -19,7 +21,9 @@ export class PermissionProcessor {
19
21
  const { props, attrs } = buildMaps(permissions);
20
22
  this.props = props;
21
23
  this.attrs = attrs;
22
- this.methods = buildMethodSet(permissions);
24
+ const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
25
+ this.blockedMethods = blockedMethods;
26
+ this.configuredMethods = configuredMethods;
23
27
  }
24
28
 
25
29
  get crossDomainImports(): boolean {
@@ -41,11 +45,36 @@ export class PermissionProcessor {
41
45
  }
42
46
 
43
47
  checkRestrictedMethod(methodName: string): boolean {
44
- if (!this.methods.has(methodName)) return false;
48
+ if (!this.blockedMethods.has(methodName)) return false;
45
49
  this.warnRestrictedMethod(methodName);
46
50
  return true;
47
51
  }
48
52
 
53
+ /**
54
+ * Returns the resolved appendArgs for a configured method, if any.
55
+ * - String entries that start with `?.` are resolved against the permissions object.
56
+ * - Non-path strings are returned as-is.
57
+ * - Methods listed as plain strings in restrictedMethodSettings do not return args;
58
+ * they are fully blocked via checkRestrictedMethod.
59
+ */
60
+ getMethodAppendArgs(methodName: string): any[] | undefined {
61
+ const config = this.configuredMethods.get(methodName);
62
+ if (!config) return undefined;
63
+
64
+ const rawArgs = config.appendArgs ?? config.addArgs;
65
+ if (!rawArgs || rawArgs.length === 0) return undefined;
66
+
67
+ const resolved: any[] = [];
68
+ for (const arg of rawArgs) {
69
+ if (typeof arg === 'string' && arg.startsWith('?.')) {
70
+ resolved.push(getValue(arg, this.permissions));
71
+ } else {
72
+ resolved.push(arg);
73
+ }
74
+ }
75
+ return resolved;
76
+ }
77
+
49
78
  redirectRestrictedProp(target: any, key: string, value: any): boolean {
50
79
  if (!this.props.has(key)) return false;
51
80
  const setting = this.props.get(key);
@@ -192,19 +221,25 @@ function normalizeAttrNames(
192
221
  return normalizeStrings(attr);
193
222
  }
194
223
 
195
- function buildMethodSet(permissions: AssignPermissions | undefined): Set<string> {
224
+ function buildMethodMaps(permissions: AssignPermissions | undefined): { blockedMethods: Set<string>; configuredMethods: Map<string, RestrictedMethodConfig> } {
196
225
  const methodSettings = permissions?.restrictedMethodSettings;
197
- const methods = new Set<string>();
226
+ const blockedMethods = new Set<string>();
227
+ const configuredMethods = new Map<string, RestrictedMethodConfig>();
198
228
  if (!methodSettings || methodSettings.length === 0) {
199
- return methods;
229
+ return { blockedMethods, configuredMethods };
200
230
  }
201
231
 
202
232
  for (const setting of methodSettings) {
203
233
  if (typeof setting === 'string') {
204
- methods.add(setting);
234
+ blockedMethods.add(setting);
235
+ continue;
236
+ }
237
+
238
+ if (configuredMethods.has(setting.method)) {
239
+ throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
205
240
  }
206
- // Phase II: object-form RestrictedMethodConfig entries are ignored for now.
241
+ configuredMethods.set(setting.method, setting);
207
242
  }
208
243
 
209
- return methods;
244
+ return { blockedMethods, configuredMethods };
210
245
  }
@@ -43,13 +43,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
43
43
  // Async method — call and await
44
44
  const method = current[baseName];
45
45
  if (typeof method === 'function') {
46
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
46
47
  if (isZeroArgAsync || nextIsMethod) {
47
48
  // Zero-arg call — next is either a method or explicitly not an argument
48
- current = await method.call(current);
49
+ current = await method.call(current, ...appendArgs);
49
50
  }
50
51
  else {
51
52
  // Call with next part as string arg, then await
52
- current = await method.call(current, nextPart);
53
+ current = await method.call(current, nextPart, ...appendArgs);
53
54
  i++; // Skip next part since we consumed it as argument
54
55
  }
55
56
  }
@@ -65,13 +66,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
65
66
  // Sync method — same logic as evaluatePathWithMethods
66
67
  const method = current[baseName];
67
68
  if (typeof method === 'function') {
69
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
68
70
  if (isZeroArgSync || nextIsMethod) {
69
71
  // Zero-arg call — next is either a method or explicitly not an argument
70
- current = method.call(current);
72
+ current = method.call(current, ...appendArgs);
71
73
  }
72
74
  else {
73
75
  // Call with next part as string arg
74
- current = method.call(current, nextPart);
76
+ current = method.call(current, nextPart, ...appendArgs);
75
77
  i++; // Skip next part since we consumed it as argument
76
78
  }
77
79
  }
@@ -69,12 +69,13 @@ export async function evaluatePathWithAsyncMethods(
69
69
  // Async method — call and await
70
70
  const method = current[baseName];
71
71
  if (typeof method === 'function') {
72
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
72
73
  if (isZeroArgAsync || nextIsMethod) {
73
74
  // Zero-arg call — next is either a method or explicitly not an argument
74
- current = await method.call(current);
75
+ current = await method.call(current, ...appendArgs);
75
76
  } else {
76
77
  // Call with next part as string arg, then await
77
- current = await method.call(current, nextPart);
78
+ current = await method.call(current, nextPart, ...appendArgs);
78
79
  i++; // Skip next part since we consumed it as argument
79
80
  }
80
81
  } else {
@@ -88,12 +89,13 @@ export async function evaluatePathWithAsyncMethods(
88
89
  // Sync method — same logic as evaluatePathWithMethods
89
90
  const method = current[baseName];
90
91
  if (typeof method === 'function') {
92
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
91
93
  if (isZeroArgSync || nextIsMethod) {
92
94
  // Zero-arg call — next is either a method or explicitly not an argument
93
- current = method.call(current);
95
+ current = method.call(current, ...appendArgs);
94
96
  } else {
95
97
  // Call with next part as string arg
96
- current = method.call(current, nextPart);
98
+ current = method.call(current, nextPart, ...appendArgs);
97
99
  i++; // Skip next part since we consumed it as argument
98
100
  }
99
101
  } else {
@@ -47,7 +47,7 @@ async function defineIshProperty(element, managerName, options, assignGingerlyFn
47
47
  let config = registry.get(managerName);
48
48
  // If not registered, wait for registration
49
49
  if (!config) {
50
- const { waitForEvent } = await import('./waitForEvent.js');
50
+ const { waitForEvent } = await import('./utils/waitForEvent.js');
51
51
  await waitForEvent(registry, managerName);
52
52
  config = registry.get(managerName);
53
53
  if (!config) {
@@ -66,7 +66,7 @@ async function defineIshProperty(
66
66
 
67
67
  // If not registered, wait for registration
68
68
  if (!config) {
69
- const { waitForEvent } = await import('./waitForEvent.js');
69
+ const { waitForEvent } = await import('./utils/waitForEvent.js');
70
70
  await waitForEvent(registry, managerName);
71
71
  config = registry.get(managerName);
72
72