assign-gingerly 0.0.74 → 0.0.76

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 (62) hide show
  1. package/DX/installForwarding.js +1 -1
  2. package/DX/installForwarding.ts +1 -1
  3. package/README.md +2 -0
  4. package/assignFrom.js +18 -17
  5. package/assignFrom.ts +23 -23
  6. package/assignFromAsync.js +14 -13
  7. package/assignFromAsync.ts +15 -15
  8. package/assignGingerly.js +46 -42
  9. package/assignGingerly.ts +131 -124
  10. package/assignPermissions/PermissionProcessor.js +166 -0
  11. package/assignPermissions/PermissionProcessor.ts +210 -0
  12. package/assignPermissions/isAllowedImportPath.js +2 -6
  13. package/assignPermissions/isAllowedImportPath.ts +3 -5
  14. package/assignPermissions/isAllowedUrl.js +18 -0
  15. package/assignPermissions/isAllowedUrl.ts +15 -0
  16. package/assignTentatively.js +9 -11
  17. package/assignTentatively.ts +11 -13
  18. package/beVigilant.js +1 -1
  19. package/beVigilant.ts +1 -1
  20. package/buildCSSQuery.js +1 -1
  21. package/buildCSSQuery.ts +1 -1
  22. package/eachTime.js +5 -6
  23. package/eachTime.ts +12 -15
  24. package/enhanceAll.js +3 -3
  25. package/enhanceAll.ts +4 -4
  26. package/evaluatePathWithAsyncMethods.js +12 -8
  27. package/evaluatePathWithAsyncMethods.ts +129 -117
  28. package/handlers/addEventListener.js +11 -11
  29. package/handlers/addEventListener.ts +12 -13
  30. package/handlers/lazyLoad.ts +7 -7
  31. package/handlers/lazyLoadSwitch.ts +3 -3
  32. package/handlers/manageTemplateList.js +10 -10
  33. package/handlers/manageTemplateList.ts +12 -12
  34. package/handlers/rangeSelector.ts +3 -3
  35. package/index.js +2 -3
  36. package/index.ts +2 -3
  37. package/inferencer/types/assign-gingerly/types.d.ts +37 -23
  38. package/inferencer/types/el-maker/types.d.ts +33 -0
  39. package/inferencer/types/scratch-box/types.d.ts +3 -0
  40. package/inferencer/types/truth-sourcer/types.d.ts +4 -0
  41. package/package.json +25 -23
  42. package/parseWithAttrs.js +1 -1
  43. package/parseWithAttrs.ts +1 -1
  44. package/processHandlerCommands.js +5 -7
  45. package/processHandlerCommands.ts +12 -15
  46. package/{getValues.js → resolve/getValues.js} +19 -8
  47. package/{getValues.ts → resolve/getValues.ts} +331 -314
  48. package/{resolveIdRef.js → resolve/resolveIdRef.js} +1 -1
  49. package/{resolveIdRef.ts → resolve/resolveIdRef.ts} +1 -1
  50. package/{resolveValues.ts → resolve/resolveValues.ts} +1 -1
  51. package/types/assign-gingerly/types.d.ts +37 -8
  52. package/utils/findClassPrototypeInPath.js +57 -0
  53. package/utils/findClassPrototypeInPath.ts +62 -0
  54. package/utils/isAsyncSpawn.js +20 -0
  55. package/utils/isAsyncSpawn.ts +17 -0
  56. package/assignPermissions/restrictedProps.js +0 -43
  57. package/assignPermissions/restrictedProps.ts +0 -53
  58. package/resolveAndAssignFeatures.js +0 -36
  59. package/resolveAndAssignFeatures.ts +0 -43
  60. /package/{resolveTemplate.js → resolve/resolveTemplate.js} +0 -0
  61. /package/{resolveTemplate.ts → resolve/resolveTemplate.ts} +0 -0
  62. /package/{resolveValues.js → resolve/resolveValues.js} +0 -0
package/assignGingerly.ts CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  import { EnhancementConfig } from "./types/assign-gingerly/types";
4
4
  import type { AssignFromOptions, FeatureConfigsMap, IAssignGingerlyOptions } from "./types/assign-gingerly/types";
5
- import type { AssignPermissions } from './types/assign-gingerly/types.js';
6
- import { buildRestrictedPropSet, checkRestrictedProp, redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
7
- import type { RestrictedPropSettingsMap } from './assignPermissions/restrictedProps.js';
8
- import { normalizeAliasOptions } from './getValues.js';
5
+ import type { PermissionProcessor } from './types/assign-gingerly/types.js';
6
+ import { normalizeAliasOptions } from './resolve/getValues.js';
9
7
 
10
8
  /**
11
9
  * Constructor signature for ItemScope Manager classes
@@ -250,30 +248,30 @@ function isIncCommand(key: string): boolean {
250
248
  /**
251
249
  * Helper function to parse an += command and extract the path
252
250
  */
253
- function parseIncCommand(key: string): string | null {
254
- if (!isIncCommand(key)) {
255
- return null;
256
- }
257
- return key.substring(0, key.length - 3); // Remove ' +=' suffix
258
- }
259
-
260
- /**
261
- * Apply the scalar and array semantics of the += command.
262
- */
263
- function addValue(lhs: any, rhs: any): any {
264
- if (Array.isArray(lhs)) {
265
- return Array.isArray(rhs) ? [...lhs, ...rhs] : [...lhs, rhs];
266
- }
267
- if (typeof lhs === 'number' && typeof rhs === 'string') {
268
- const parsed = Number(rhs);
269
- return Number.isNaN(parsed) ? lhs + rhs : lhs + parsed;
270
- }
271
- if (typeof lhs === 'string' && typeof rhs === 'number') {
272
- const parsed = Number(lhs);
273
- return Number.isNaN(parsed) ? lhs + rhs : (parsed + rhs).toString();
274
- }
275
- return lhs + rhs;
276
- }
251
+ function parseIncCommand(key: string): string | null {
252
+ if (!isIncCommand(key)) {
253
+ return null;
254
+ }
255
+ return key.substring(0, key.length - 3); // Remove ' +=' suffix
256
+ }
257
+
258
+ /**
259
+ * Apply the scalar and array semantics of the += command.
260
+ */
261
+ function addValue(lhs: any, rhs: any): any {
262
+ if (Array.isArray(lhs)) {
263
+ return Array.isArray(rhs) ? [...lhs, ...rhs] : [...lhs, rhs];
264
+ }
265
+ if (typeof lhs === 'number' && typeof rhs === 'string') {
266
+ const parsed = Number(rhs);
267
+ return Number.isNaN(parsed) ? lhs + rhs : lhs + parsed;
268
+ }
269
+ if (typeof lhs === 'string' && typeof rhs === 'number') {
270
+ const parsed = Number(lhs);
271
+ return Number.isNaN(parsed) ? lhs + rhs : (parsed + rhs).toString();
272
+ }
273
+ return lhs + rhs;
274
+ }
277
275
 
278
276
  /**
279
277
  * Helper function to check if a key represents a =! command
@@ -444,6 +442,19 @@ export function isClassInstance(value: any): boolean {
444
442
  return proto !== Object.prototype && proto !== null;
445
443
  }
446
444
 
445
+ /**
446
+ * Check whether a method name is listed in withMethods and is not restricted
447
+ * by the permission processor. Restricted methods are treated as non-method
448
+ * property names so they fall through to normal access.
449
+ */
450
+ export function isAllowedMethod(
451
+ methodName: string,
452
+ withMethods: Set<string>,
453
+ permissionProcessor?: PermissionProcessor
454
+ ): boolean {
455
+ return withMethods.has(methodName) && !permissionProcessor?.checkRestrictedMethod(methodName);
456
+ }
457
+
447
458
  /**
448
459
  * Helper function to evaluate a nested path with method calls
449
460
  * Handles chained method calls where path segments can be methods
@@ -454,7 +465,8 @@ export function evaluatePathWithMethods(
454
465
  target: any,
455
466
  pathParts: string[],
456
467
  value: any,
457
- withMethods: Set<string>
468
+ withMethods: Set<string>,
469
+ permissionProcessor?: PermissionProcessor
458
470
  ): { target: any; lastKey: string; isMethod: boolean; isZeroArg: boolean } {
459
471
  let current = target;
460
472
  let i = 0;
@@ -466,11 +478,11 @@ export function evaluatePathWithMethods(
466
478
 
467
479
  // A trailing | marks a zero-argument method call: 'deref|' calls deref()
468
480
  // without consuming the next segment. Only applies to names in withMethods.
469
- const isZeroArgMethod = part.endsWith('|') && withMethods.has(part.slice(0, -1));
481
+ const isZeroArgMethod = part.endsWith('|') && isAllowedMethod(part.slice(0, -1), withMethods, permissionProcessor);
470
482
  const nextIsMethod = withMethods.has(nextPart)
471
483
  || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1)));
472
484
 
473
- if (withMethods.has(part) || isZeroArgMethod) {
485
+ if (isAllowedMethod(part, withMethods, permissionProcessor) || isZeroArgMethod) {
474
486
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
475
487
  const method = current[methodName];
476
488
  if (typeof method === 'function') {
@@ -503,12 +515,12 @@ export function evaluatePathWithMethods(
503
515
  // Strip a trailing | from the last segment only when it names a listed method;
504
516
  // otherwise it is a literal property name (e.g. an exotic key ending in |).
505
517
  const rawLastKey = pathParts[pathParts.length - 1];
506
- const isZeroArg = rawLastKey.endsWith('|') && withMethods.has(rawLastKey.slice(0, -1));
518
+ const isZeroArg = rawLastKey.endsWith('|') && isAllowedMethod(rawLastKey.slice(0, -1), withMethods, permissionProcessor);
507
519
  const lastKey = isZeroArg ? rawLastKey.slice(0, -1) : rawLastKey;
508
520
  return {
509
521
  target: current,
510
522
  lastKey,
511
- isMethod: withMethods.has(lastKey),
523
+ isMethod: isAllowedMethod(lastKey, withMethods, permissionProcessor),
512
524
  isZeroArg
513
525
  };
514
526
  }
@@ -568,8 +580,7 @@ function applyToEach(
568
580
  withMethods: Set<string>,
569
581
  aliasMap: Map<string, string>,
570
582
  options?: IAssignGingerlyOptions,
571
- permissions?: AssignPermissions,
572
- restrictedPropSet?: RestrictedPropSettingsMap
583
+ permissionProcessor?: PermissionProcessor
573
584
  ): void {
574
585
  // Convert to array for iteration
575
586
  const items = Array.isArray(iterable) ? iterable : Array.from(iterable);
@@ -594,8 +605,8 @@ function applyToEach(
594
605
  let current = item;
595
606
  for (const part of pathToForEach) {
596
607
  // A trailing | marks a zero-argument method call (only for names in withMethods)
597
- const isZeroArgMethod = part.endsWith('|') && withMethods.has(part.slice(0, -1));
598
- if (withMethods.has(part) || isZeroArgMethod) {
608
+ const isZeroArgMethod = part.endsWith('|') && isAllowedMethod(part.slice(0, -1), withMethods, permissionProcessor);
609
+ if (isAllowedMethod(part, withMethods, permissionProcessor) || isZeroArgMethod) {
599
610
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
600
611
  const method = current[methodName];
601
612
  if (typeof method === 'function') {
@@ -627,11 +638,11 @@ function applyToEach(
627
638
 
628
639
  // Recursively apply to the nested iterable
629
640
  if (isIterable(current)) {
630
- applyToEach(current, pathAfterForEach, value, withMethods, aliasMap, options, permissions, restrictedPropSet);
641
+ applyToEach(current, pathAfterForEach, value, withMethods, aliasMap, options, permissionProcessor);
631
642
  }
632
643
  } else {
633
644
  // No nested @each, evaluate the remaining path normally
634
- const result = evaluatePathWithMethods(item, remainingPath, value, withMethods);
645
+ const result = evaluatePathWithMethods(item, remainingPath, value, withMethods, permissionProcessor);
635
646
 
636
647
  if (result.isMethod) {
637
648
  // Last segment is a method - call it
@@ -651,7 +662,7 @@ function applyToEach(
651
662
  const lastKey = result.lastKey;
652
663
  const parent = result.target;
653
664
 
654
- if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
665
+ if (permissionProcessor?.redirectRestrictedProp(parent, lastKey, value)) {
655
666
  // skip
656
667
  } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
657
668
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
@@ -659,7 +670,7 @@ function applyToEach(
659
670
  if (typeof currentValue !== 'object' || currentValue === null) {
660
671
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
661
672
  }
662
- assignGingerly(currentValue, value, options, permissions);
673
+ assignGingerly(currentValue, value, options, permissionProcessor);
663
674
  } else {
664
675
  parent[lastKey] = value;
665
676
  }
@@ -702,7 +713,7 @@ export function assignGingerly(
702
713
  target: any,
703
714
  source: Record<string | symbol, any>,
704
715
  options?: IAssignGingerlyOptions,
705
- permissions?: AssignPermissions
716
+ permissionProcessor?: PermissionProcessor
706
717
  ): any {
707
718
  if (!target || typeof target !== 'object') {
708
719
  return target;
@@ -710,9 +721,6 @@ export function assignGingerly(
710
721
 
711
722
  const { aliasMap, withMethods: withMethodsSet } = normalizeAliasOptions(options);
712
723
 
713
- // Normalize restrictedPropSettings once per top-level call
714
- const restrictedPropSet = buildRestrictedPropSet(permissions);
715
-
716
724
  // Convert withAsyncMethods array to Set for O(1) lookup
717
725
  const withAsyncMethodsSet = options?.withAsyncMethods
718
726
  ? options.withAsyncMethods instanceof Set
@@ -800,7 +808,7 @@ export function assignGingerly(
800
808
  let lhsParent: any;
801
809
  let lhsKey: string;
802
810
  if (withMethodsSet) {
803
- const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet);
811
+ const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet, permissionProcessor);
804
812
  lhsParent = result.target;
805
813
  lhsKey = result.lastKey;
806
814
  lhsValue = lhsParent[lhsKey];
@@ -811,49 +819,49 @@ export function assignGingerly(
811
819
  }
812
820
 
813
821
  //TODO: this logic seems to occur twice at least. Maybe make it a method?
814
- if (checkRestrictedProp(restrictedPropSet, lhsKey)) {
815
- continue;
816
- }
817
-
818
- // Event handler: Element LHS + object RHS with 'on' property
822
+ if (permissionProcessor?.checkRestrictedProp(lhsKey)) {
823
+ continue;
824
+ }
825
+
826
+ // Event handler: Element LHS + object RHS with 'on' property
819
827
  if (lhsValue instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
820
828
  const capturedLhs = lhsValue;
821
829
  const capturedValue = value;
822
830
  const capturedTarget = target;
823
- const capturedOptions = options as AssignFromOptions | undefined;
824
- import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
825
- attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissions);
831
+ const capturedOptions = options as AssignFromOptions | undefined;
832
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
833
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissionProcessor);
826
834
  });
827
835
  continue;
828
836
  }
829
837
 
830
- if (!(lhsKey in lhsParent)) {
831
- lhsParent[lhsKey] = value;
832
- } else {
833
- lhsParent[lhsKey] = addValue(lhsValue, value);
838
+ if (!(lhsKey in lhsParent)) {
839
+ lhsParent[lhsKey] = value;
840
+ } else {
841
+ lhsParent[lhsKey] = addValue(lhsValue, value);
834
842
  }
835
843
  } else {
836
- // Plain key - direct operation on target
837
- if (checkRestrictedProp(restrictedPropSet, path)) {
838
- continue;
839
- }
840
-
841
- // Event handler: Element LHS + object RHS with 'on' property
844
+ // Plain key - direct operation on target
845
+ if (permissionProcessor?.checkRestrictedProp(path)) {
846
+ continue;
847
+ }
848
+
849
+ // Event handler: Element LHS + object RHS with 'on' property
842
850
  if (target[path] instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
843
851
  const capturedLhs = target[path];
844
852
  const capturedValue = value;
845
853
  const capturedTarget = target;
846
- const capturedOptions = options as AssignFromOptions | undefined;
847
- import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
848
- attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissions);
854
+ const capturedOptions = options as AssignFromOptions | undefined;
855
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
856
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {}, permissionProcessor);
849
857
  });
850
858
  continue;
851
859
  }
852
860
 
853
- if (!(path in target)) {
854
- target[path] = value;
855
- } else {
856
- target[path] = addValue(target[path], value);
861
+ if (!(path in target)) {
862
+ target[path] = value;
863
+ } else {
864
+ target[path] = addValue(target[path], value);
857
865
  }
858
866
  }
859
867
  }
@@ -909,7 +917,7 @@ export function assignGingerly(
909
917
  }
910
918
 
911
919
  // Apply negation to LHS — check restriction first
912
- if (!checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
920
+ if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
913
921
  lhsParent[lhsLastKey] = !valueToNegate;
914
922
  }
915
923
  }
@@ -961,19 +969,19 @@ export function assignGingerly(
961
969
  }
962
970
 
963
971
  // Handle Y= merge commands (recursive assignGingerly into sub-object)
964
- if (isMergeCommand(key)) {
965
- const path = parseMergeCommand(key);
966
- if (path) {
967
- const pathParts = isNestedPath(path) ? parsePath(path) : [path];
968
- const lastKey = pathParts[pathParts.length - 1];
969
- if (checkRestrictedProp(restrictedPropSet, lastKey)) {
970
- continue;
971
- }
972
- // Navigate to the target sub-object
973
- let mergeTarget: any;
972
+ if (isMergeCommand(key)) {
973
+ const path = parseMergeCommand(key);
974
+ if (path) {
975
+ const pathParts = isNestedPath(path) ? parsePath(path) : [path];
976
+ const lastKey = pathParts[pathParts.length - 1];
977
+ if (permissionProcessor?.checkRestrictedProp(lastKey)) {
978
+ continue;
979
+ }
980
+ // Navigate to the target sub-object
981
+ let mergeTarget: any;
974
982
  if (isNestedPath(path)) {
975
983
  if (withMethodsSet) {
976
- const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
984
+ const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet, permissionProcessor);
977
985
  mergeTarget = result.target[result.lastKey];
978
986
  } else {
979
987
  const pathParts = parsePath(path);
@@ -993,7 +1001,7 @@ export function assignGingerly(
993
1001
 
994
1002
  // Recursively merge if target is a valid object
995
1003
  if (mergeTarget && typeof mergeTarget === 'object') {
996
- assignGingerly(mergeTarget, value, options, permissions);
1004
+ assignGingerly(mergeTarget, value, options, permissionProcessor);
997
1005
  }
998
1006
  }
999
1007
  continue;
@@ -1021,11 +1029,10 @@ export function assignGingerly(
1021
1029
  pathParts,
1022
1030
  forEachIndex,
1023
1031
  value,
1024
- withMethodsSet,
1025
- aliasMap,
1026
- options,
1027
- permissions,
1028
- restrictedPropSet
1032
+ withMethodsSet,
1033
+ aliasMap,
1034
+ options,
1035
+ permissionProcessor
1029
1036
  );
1030
1037
  } catch (error) {
1031
1038
  console.error('Error in @eachTime:', error);
@@ -1042,7 +1049,7 @@ export function assignGingerly(
1042
1049
  let current = target;
1043
1050
  if (pathToForEach.length > 0) {
1044
1051
  if (withMethodsSet) {
1045
- const result = evaluatePathWithMethods(target, pathToForEach, value, withMethodsSet);
1052
+ const result = evaluatePathWithMethods(target, pathToForEach, value, withMethodsSet, permissionProcessor);
1046
1053
  // The result.target is the current position after evaluating the path
1047
1054
  // This is already the iterable we want
1048
1055
  current = result.target;
@@ -1055,7 +1062,7 @@ export function assignGingerly(
1055
1062
 
1056
1063
  // Apply to each item in the iterable
1057
1064
  if (isIterable(current)) {
1058
- applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissions, restrictedPropSet);
1065
+ applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissionProcessor);
1059
1066
  }
1060
1067
  // If not iterable, let JavaScript throw error naturally when trying to iterate
1061
1068
 
@@ -1069,15 +1076,15 @@ export function assignGingerly(
1069
1076
  const capturedTarget = target;
1070
1077
  const capturedPathParts = pathParts;
1071
1078
  const capturedValue = value;
1072
- const capturedWithMethodsSet = withMethodsSet || new Set<string>();
1073
- const capturedOptions = options;
1074
- const capturedPermissions = permissions;
1075
- const capturedRestrictedPropSet = restrictedPropSet;
1079
+ const capturedWithMethodsSet = withMethodsSet || new Set<string>();
1080
+ const capturedOptions = options;
1081
+ const capturedPermissionProcessor = permissionProcessor;
1076
1082
  (async () => {
1077
1083
  const { evaluatePathWithAsyncMethods } = await import('./evaluatePathWithAsyncMethods.js');
1078
1084
  const result = await evaluatePathWithAsyncMethods(
1079
1085
  capturedTarget, capturedPathParts, capturedValue,
1080
- capturedWithMethodsSet, withAsyncMethodsSet
1086
+ capturedWithMethodsSet, withAsyncMethodsSet,
1087
+ capturedPermissionProcessor
1081
1088
  );
1082
1089
 
1083
1090
  if (result.isMethod || result.isAsyncMethod) {
@@ -1096,16 +1103,16 @@ export function assignGingerly(
1096
1103
  } else {
1097
1104
  // Not a method — assign the value
1098
1105
  const lastKey = result.lastKey;
1099
- const parent = result.target;
1100
- if (redirectRestrictedProp(capturedRestrictedPropSet, parent, lastKey, capturedValue)) {
1101
- // skip
1102
- } else if (typeof capturedValue === 'object' && capturedValue !== null && !Array.isArray(capturedValue)) {
1106
+ const parent = result.target;
1107
+ if (capturedPermissionProcessor?.redirectRestrictedProp(parent, lastKey, capturedValue)) {
1108
+ // skip
1109
+ } else if (typeof capturedValue === 'object' && capturedValue !== null && !Array.isArray(capturedValue)) {
1103
1110
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
1104
1111
  const currentValue = parent[lastKey];
1105
1112
  if (typeof currentValue !== 'object' || currentValue === null) {
1106
1113
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1107
1114
  }
1108
- assignGingerly(currentValue, capturedValue, capturedOptions, capturedPermissions);
1115
+ assignGingerly(currentValue, capturedValue, capturedOptions, capturedPermissionProcessor);
1109
1116
  } else {
1110
1117
  parent[lastKey] = capturedValue;
1111
1118
  }
@@ -1119,7 +1126,7 @@ export function assignGingerly(
1119
1126
 
1120
1127
  // Check if we need to handle methods
1121
1128
  if (withMethodsSet) {
1122
- const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet);
1129
+ const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet, permissionProcessor);
1123
1130
 
1124
1131
  if (result.isMethod) {
1125
1132
  // Last segment is a method - call it
@@ -1142,19 +1149,19 @@ export function assignGingerly(
1142
1149
  const lastKey = result.lastKey;
1143
1150
  const parent = result.target;
1144
1151
 
1145
- if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
1146
- // skip
1147
- // Check for static assignTo protocol
1148
- } else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1149
- continue;
1150
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1152
+ if (permissionProcessor?.redirectRestrictedProp(parent, lastKey, value)) {
1153
+ // skip
1154
+ // Check for static assignTo protocol
1155
+ } else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1156
+ continue;
1157
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1151
1158
  // Check if property exists and is readonly
1152
1159
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
1153
1160
  const currentValue = parent[lastKey];
1154
1161
  if (typeof currentValue !== 'object' || currentValue === null) {
1155
1162
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1156
1163
  }
1157
- assignGingerly(currentValue, value, options, permissions);
1164
+ assignGingerly(currentValue, value, options, permissionProcessor);
1158
1165
  } else {
1159
1166
  // Property is writable - replace it
1160
1167
  parent[lastKey] = value;
@@ -1167,12 +1174,12 @@ export function assignGingerly(
1167
1174
  const lastKey = pathParts[pathParts.length - 1];
1168
1175
  const parent = ensureNestedPath(target, pathParts);
1169
1176
 
1170
- if (redirectRestrictedProp(restrictedPropSet, parent, lastKey, value)) {
1171
- // skip
1172
- // Check for static assignTo protocol
1173
- } else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1174
- continue;
1175
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1177
+ if (permissionProcessor?.redirectRestrictedProp(parent, lastKey, value)) {
1178
+ // skip
1179
+ // Check for static assignTo protocol
1180
+ } else if (lastKey in parent && tryAssignTo(parent[lastKey], value, parent, lastKey)) {
1181
+ continue;
1182
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1176
1183
  // Check if property exists and is readonly
1177
1184
  if (lastKey in parent && isReadonlyProperty(parent, lastKey)) {
1178
1185
  // Property is readonly - check if current value is an object
@@ -1181,7 +1188,7 @@ export function assignGingerly(
1181
1188
  throw new Error(`Cannot merge object into readonly primitive property '${String(lastKey)}'`);
1182
1189
  }
1183
1190
  // Recursively apply assignGingerly to the readonly object
1184
- assignGingerly(currentValue, value, options, permissions);
1191
+ assignGingerly(currentValue, value, options, permissionProcessor);
1185
1192
  } else {
1186
1193
  // Property is writable - replace it
1187
1194
  parent[lastKey] = value;
@@ -1212,12 +1219,12 @@ export function assignGingerly(
1212
1219
  continue;
1213
1220
  }
1214
1221
 
1215
- if (redirectRestrictedProp(restrictedPropSet, target, key, value)) {
1216
- // skip
1217
- // Check for static assignTo protocol
1218
- } else if (key in target && tryAssignTo(target[key], value, target, key)) {
1219
- continue;
1220
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1222
+ if (permissionProcessor?.redirectRestrictedProp(target, key, value)) {
1223
+ // skip
1224
+ // Check for static assignTo protocol
1225
+ } else if (key in target && tryAssignTo(target[key], value, target, key)) {
1226
+ continue;
1227
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1221
1228
  // Check if property exists and is readonly
1222
1229
  if (key in target && isReadonlyProperty(target, key)) {
1223
1230
  // Property is readonly - check if current value is an object
@@ -1226,7 +1233,7 @@ export function assignGingerly(
1226
1233
  throw new Error(`Cannot merge object into readonly primitive property '${String(key)}'`);
1227
1234
  }
1228
1235
  // Recursively apply assignGingerly to the readonly object
1229
- assignGingerly(currentValue, value, options, permissions);
1236
+ assignGingerly(currentValue, value, options, permissionProcessor);
1230
1237
  } else {
1231
1238
  // Property is writable - replace it
1232
1239
  target[key] = value;
@@ -1370,7 +1377,7 @@ export function assignGingerly(
1370
1377
  // Fire-and-forget bulk enhancements (async, non-blocking)
1371
1378
  if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
1372
1379
  import('./enhanceAll.js').then(({ enhanceAll }) => {
1373
- enhanceAll(target, options!.enhance!, permissions);
1380
+ enhanceAll(target, options!.enhance!, permissionProcessor);
1374
1381
  });
1375
1382
  }
1376
1383
 
@@ -0,0 +1,166 @@
1
+ import { isAllowedUrl } from './isAllowedUrl.js';
2
+ export class PermissionProcessor {
3
+ constructor(permissions) {
4
+ this.permissions = permissions;
5
+ const { props, attrs } = buildMaps(permissions);
6
+ this.props = props;
7
+ this.attrs = attrs;
8
+ this.methods = buildMethodSet(permissions);
9
+ this.warned = new Set();
10
+ this.warnedMethods = new Set();
11
+ }
12
+ get crossDomainImports() {
13
+ return !!this.permissions?.crossDomainImports;
14
+ }
15
+ get hasProps() {
16
+ return this.props.size > 0;
17
+ }
18
+ get hasAttrs() {
19
+ return this.attrs.size > 0;
20
+ }
21
+ checkRestrictedProp(key) {
22
+ if (!this.props.has(key))
23
+ return false;
24
+ this.warnRestricted(key);
25
+ return true;
26
+ }
27
+ checkRestrictedMethod(methodName) {
28
+ if (!this.methods.has(methodName))
29
+ return false;
30
+ this.warnRestrictedMethod(methodName);
31
+ return true;
32
+ }
33
+ redirectRestrictedProp(target, key, value) {
34
+ if (!this.props.has(key))
35
+ return false;
36
+ const setting = this.props.get(key);
37
+ const { allowCrossDomain, allowFromSameDomain } = setting || {};
38
+ if (allowCrossDomain) {
39
+ return this.applyAllowedSetting(setting, target, key, value);
40
+ }
41
+ if (allowFromSameDomain) {
42
+ if (isAllowedUrl(value)) {
43
+ return this.applyAllowedSetting(setting, target, key, value);
44
+ }
45
+ this.warnRestricted(key);
46
+ return true;
47
+ }
48
+ if (setting?.useMethod) {
49
+ const method = target?.[setting.useMethod];
50
+ if (typeof method === 'function') {
51
+ method.call(target, value);
52
+ return true;
53
+ }
54
+ }
55
+ this.warnRestricted(key);
56
+ return true;
57
+ }
58
+ checkRestrictedAttributeCall(methodName, args) {
59
+ if (methodName !== 'setAttribute' || args.length === 0 || this.attrs.size === 0) {
60
+ return { blocked: false };
61
+ }
62
+ const attrName = String(args[0]);
63
+ if (!this.attrs.has(attrName)) {
64
+ return { blocked: false };
65
+ }
66
+ const setting = this.attrs.get(attrName);
67
+ const value = args[1];
68
+ const { allowCrossDomain, allowFromSameDomain } = setting || {};
69
+ if (allowCrossDomain) {
70
+ return { blocked: false, attrName };
71
+ }
72
+ if (allowFromSameDomain) {
73
+ if (isAllowedUrl(value)) {
74
+ return { blocked: false, attrName };
75
+ }
76
+ this.warnRestricted(attrName);
77
+ return { blocked: true, attrName };
78
+ }
79
+ this.warnRestricted(attrName);
80
+ return { blocked: true, attrName };
81
+ }
82
+ applyAllowedSetting(setting, target, key, value) {
83
+ if (setting?.useMethod) {
84
+ const method = target?.[setting.useMethod];
85
+ if (typeof method === 'function') {
86
+ method.call(target, value);
87
+ return true;
88
+ }
89
+ this.warnRestricted(key);
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+ warnRestricted(key) {
95
+ if (!this.warned.has(key)) {
96
+ this.warned.add(key);
97
+ console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
98
+ }
99
+ }
100
+ warnRestrictedMethod(methodName) {
101
+ if (!this.warnedMethods.has(methodName)) {
102
+ this.warnedMethods.add(methodName);
103
+ console.warn(`assignGingerly: method '${methodName}' is in restrictedMethodSettings — method call skipped.`);
104
+ }
105
+ }
106
+ }
107
+ function buildMaps(permissions) {
108
+ const settings = permissions?.restrictedPropSettings;
109
+ const props = new Map();
110
+ const attrs = new Map();
111
+ if (!settings || settings.length === 0) {
112
+ return { props, attrs };
113
+ }
114
+ for (const setting of settings) {
115
+ if (typeof setting === 'string') {
116
+ if (props.has(setting)) {
117
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${setting}'.`);
118
+ }
119
+ props.set(setting, undefined);
120
+ continue;
121
+ }
122
+ const propNames = normalizeStrings(setting.props);
123
+ if (propNames.length === 0) {
124
+ throw new Error('assignGingerly: restrictedPropSettings object entry must have a props value.');
125
+ }
126
+ for (const prop of propNames) {
127
+ if (props.has(prop)) {
128
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
129
+ }
130
+ props.set(prop, setting);
131
+ }
132
+ const attrNames = normalizeAttrNames(setting.attr, propNames);
133
+ for (const attr of attrNames) {
134
+ if (attrs.has(attr)) {
135
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings attr entry for '${attr}'.`);
136
+ }
137
+ attrs.set(attr, setting);
138
+ }
139
+ }
140
+ return { props, attrs };
141
+ }
142
+ function normalizeStrings(value) {
143
+ if (value === undefined)
144
+ return [];
145
+ return Array.isArray(value) ? value : [value];
146
+ }
147
+ function normalizeAttrNames(attr, propNames) {
148
+ if (attr === undefined || attr === false)
149
+ return [];
150
+ if (attr === true)
151
+ return propNames;
152
+ return normalizeStrings(attr);
153
+ }
154
+ function buildMethodSet(permissions) {
155
+ const methodSettings = permissions?.restrictedMethodSettings;
156
+ const methods = new Set();
157
+ if (!methodSettings || methodSettings.length === 0) {
158
+ return methods;
159
+ }
160
+ for (const setting of methodSettings) {
161
+ if (typeof setting === 'string') {
162
+ methods.add(setting);
163
+ }
164
+ }
165
+ return methods;
166
+ }