assign-gingerly 0.0.71 → 0.0.72

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.
@@ -252,6 +252,12 @@ export interface IAssignGingerlyOptions {
252
252
  bypassChecks?: boolean;
253
253
  /**
254
254
  * Method names to call during path evaluation (e.g., for `?.method()` calls)
255
+ *
256
+ * Append `|` to a path segment (e.g., `?.deref|?.classList?.add`) to call a
257
+ * listed method with zero arguments instead of consuming the next path segment
258
+ * as its argument. On the last segment, `|` calls the method with no arguments
259
+ * and ignores the value. The `|` suffix only applies to names listed here —
260
+ * for any other segment it is treated as part of a literal property name.
255
261
  */
256
262
  withMethods?: string[] | Set<string>;
257
263
  /**
@@ -343,7 +349,7 @@ export interface AssignFromOptions {
343
349
  /** Protocol handlers (sync or async) */
344
350
  protocols?: Record<string, (key: string) => any | Promise<any>>;
345
351
 
346
- /** Method names to call during path evaluation */
352
+ /** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
347
353
  withMethods?: string[] | Set<string>;
348
354
 
349
355
  /** Alias mappings for path segments */
@@ -701,7 +707,7 @@ export interface HandlerConfig {
701
707
  * Handlers are invoked when a LHS key ends with ' =>'.
702
708
  */
703
709
  export interface AssignFromHandler {
704
- assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any): Promise<void> | void;
710
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any, permissions?: AssignPermissions): Promise<void> | void;
705
711
  }
706
712
 
707
713
  /**
@@ -879,7 +885,7 @@ export interface LazyLoadInstantiatedContext {
879
885
  export declare class LazyLoadHandler implements AssignFromHandler {
880
886
  config: any;
881
887
  constructor(config: any);
882
- assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any): Promise<void>;
888
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any, permissions?: AssignPermissions): Promise<void>;
883
889
  protected onCloneInserted(nodes: Node[], lhsTarget: Element, resolvedParams: Record<string, any>): Promise<void>;
884
890
  }
885
891
 
@@ -927,3 +933,54 @@ export interface AddEventListenerConfig extends AssignDispatchVector {
927
933
 
928
934
  }
929
935
  //#endregion
936
+
937
+
938
+ // =============================================================================
939
+ // Permissions
940
+ // =============================================================================
941
+
942
+ /**
943
+ * Phase II+: object form for a restricted property setting.
944
+ */
945
+ export interface RestrictedPropSetting {
946
+ prop: string;
947
+ useMethod?: string; // Phase II: redirect to a safe method
948
+ attr?: string; // Phase III: also watch setAttribute for this attr
949
+ allowFromSameHost?: boolean; // Phase III
950
+ allowCrossDomain?: boolean; // Phase III
951
+ }
952
+
953
+ /**
954
+ * Phase IV+: object form for a restricted method setting.
955
+ */
956
+ export interface RestrictedMethodConfig {
957
+ method: string;
958
+ addArgs?: string[]; // Phase V: append sanitizer args
959
+ }
960
+
961
+ /**
962
+ * Permissions interface for controlling security-sensitive operations.
963
+ * Only trusted script can set these — never parsed from HTML attributes.
964
+ */
965
+ export interface AssignPermissions {
966
+ /** Allow imports from cross-domain URLs (default: false) */
967
+ crossDomainImports?: boolean;
968
+
969
+ /**
970
+ * Restricted property settings.
971
+ * Phase I: string entries are property names that cannot be assigned.
972
+ * Phase II: an object with useMethod redirects ordinary assignment to that
973
+ * method; command operations remain blocked. Phase III+ adds attr support.
974
+ *
975
+ * NOTE: This is a property-assignment guard only. Method calls (setAttribute, etc.)
976
+ * are not blocked — see Phase III+. Event listeners can still be registered, but
977
+ * assignments performed by their vectors inherit these permissions.
978
+ */
979
+ restrictedPropSettings?: Array<string | RestrictedPropSetting>;
980
+
981
+ /** Sanitizer options (Phase III+) */
982
+ sanitizerOptions?: Record<string, any>;
983
+
984
+ /** Restricted method settings (Phase IV+) */
985
+ restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
986
+ }
@@ -15,6 +15,68 @@
15
15
  * isAllowedImportPath('https://evil.com/x.js'); // false
16
16
  * isAllowedImportPath('//cdn.example.com/x.js'); // false
17
17
  */
18
+ /**
19
+ * Module-level warn dedup — one warning per property key per process lifetime.
20
+ */
21
+ const warnedOnce = new Set();
22
+ /**
23
+ * Warn once per key that a restricted property assignment was skipped.
24
+ */
25
+ function warnRestricted(key) {
26
+ if (!warnedOnce.has(key)) {
27
+ warnedOnce.add(key);
28
+ console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
29
+ }
30
+ }
31
+ /**
32
+ * Normalize the restrictedPropSettings from permissions into a fast-lookup Set.
33
+ * Phase I: extracts string entries only (object entries are Phase II+).
34
+ * Returns undefined when nothing is restricted (fast-bail in callers).
35
+ */
36
+ export function buildRestrictedPropSet(permissions) {
37
+ const settings = permissions?.restrictedPropSettings;
38
+ if (!settings || settings.length === 0)
39
+ return undefined;
40
+ const restrictedPropSet = new Map();
41
+ for (const setting of settings) {
42
+ const prop = typeof setting === 'string' ? setting : setting.prop;
43
+ if (restrictedPropSet.has(prop)) {
44
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
45
+ }
46
+ restrictedPropSet.set(prop, typeof setting === 'string' ? undefined : setting);
47
+ }
48
+ return restrictedPropSet;
49
+ }
50
+ /**
51
+ * Check if a property key is restricted. If so, warn (once) and return true.
52
+ * Call sites should `continue` or skip the assignment when this returns true.
53
+ */
54
+ export function checkRestrictedProp(restrictedPropSet, key) {
55
+ if (!restrictedPropSet || !restrictedPropSet.has(key))
56
+ return false;
57
+ warnRestricted(key);
58
+ return true;
59
+ }
60
+ /**
61
+ * Redirect an ordinary assignment through its configured safe method.
62
+ * Returns true when the property is restricted, whether redirected or skipped.
63
+ */
64
+ export function redirectRestrictedProp(restrictedPropSet, target, key, value) {
65
+ if (!restrictedPropSet || !restrictedPropSet.has(key))
66
+ return false;
67
+ const setting = restrictedPropSet.get(key);
68
+ if (!setting?.useMethod) {
69
+ warnRestricted(key);
70
+ return true;
71
+ }
72
+ const method = target?.[setting.useMethod];
73
+ if (typeof method !== 'function') {
74
+ warnRestricted(key);
75
+ return true;
76
+ }
77
+ method.call(target, value);
78
+ return true;
79
+ }
18
80
  /**
19
81
  * Check if an import path is allowed (non-cross-domain).
20
82
  *
@@ -16,16 +16,81 @@
16
16
  * isAllowedImportPath('//cdn.example.com/x.js'); // false
17
17
  */
18
18
 
19
+ // Re-export AssignPermissions from canonical location for backwards compatibility
20
+ export type { AssignPermissions } from './types/assign-gingerly/types.js';
21
+ import type { AssignPermissions, RestrictedPropSetting } from './types/assign-gingerly/types.js';
22
+
23
+ export type RestrictedPropSettingsMap = Map<string, RestrictedPropSetting | undefined>;
24
+
25
+ /**
26
+ * Module-level warn dedup — one warning per property key per process lifetime.
27
+ */
28
+ const warnedOnce = new Set<string>();
29
+
30
+ /**
31
+ * Warn once per key that a restricted property assignment was skipped.
32
+ */
33
+ function warnRestricted(key: string): void {
34
+ if (!warnedOnce.has(key)) {
35
+ warnedOnce.add(key);
36
+ console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
37
+ }
38
+ }
39
+
19
40
  /**
20
- * Permissions interface for controlling security-sensitive operations.
21
- * Passed as the last parameter to assignGingerly, assignFrom, and enhanceAll.
22
- * Only trusted script can set these they are never parsed from HTML attributes.
41
+ * Normalize the restrictedPropSettings from permissions into a fast-lookup Set.
42
+ * Phase I: extracts string entries only (object entries are Phase II+).
43
+ * Returns undefined when nothing is restricted (fast-bail in callers).
23
44
  */
24
- export interface AssignPermissions {
25
- /** Allow imports from cross-domain URLs (default: false) */
26
- crossDomainImports?: boolean;
45
+ export function buildRestrictedPropSet(permissions: AssignPermissions | undefined): RestrictedPropSettingsMap | undefined {
46
+ const settings = permissions?.restrictedPropSettings;
47
+ if (!settings || settings.length === 0) return undefined;
48
+ const restrictedPropSet: RestrictedPropSettingsMap = new Map();
49
+ for (const setting of settings) {
50
+ const prop = typeof setting === 'string' ? setting : setting.prop;
51
+ if (restrictedPropSet.has(prop)) {
52
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
53
+ }
54
+ restrictedPropSet.set(prop, typeof setting === 'string' ? undefined : setting);
55
+ }
56
+ return restrictedPropSet;
27
57
  }
28
58
 
59
+ /**
60
+ * Check if a property key is restricted. If so, warn (once) and return true.
61
+ * Call sites should `continue` or skip the assignment when this returns true.
62
+ */
63
+ export function checkRestrictedProp(restrictedPropSet: RestrictedPropSettingsMap | undefined, key: string): boolean {
64
+ if (!restrictedPropSet || !restrictedPropSet.has(key)) return false;
65
+ warnRestricted(key);
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * Redirect an ordinary assignment through its configured safe method.
71
+ * Returns true when the property is restricted, whether redirected or skipped.
72
+ */
73
+ export function redirectRestrictedProp(
74
+ restrictedPropSet: RestrictedPropSettingsMap | undefined,
75
+ target: any,
76
+ key: string,
77
+ value: any
78
+ ): boolean {
79
+ if (!restrictedPropSet || !restrictedPropSet.has(key)) return false;
80
+ const setting = restrictedPropSet.get(key);
81
+ if (!setting?.useMethod) {
82
+ warnRestricted(key);
83
+ return true;
84
+ }
85
+ const method = target?.[setting.useMethod];
86
+ if (typeof method !== 'function') {
87
+ warnRestricted(key);
88
+ return true;
89
+ }
90
+ method.call(target, value);
91
+ return true;
92
+ }
93
+
29
94
  /**
30
95
  * Check if an import path is allowed (non-cross-domain).
31
96
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.71",
3
+ "version": "0.0.72",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -203,7 +203,7 @@
203
203
  "chrome": "npx playwright cr http://localhost:8000"
204
204
  },
205
205
  "devDependencies": {
206
- "@playwright/test": "1.62.0",
206
+ "@playwright/test": "1.62.1",
207
207
  "spa-ssi": "0.0.27"
208
208
  }
209
209
  }
@@ -6,7 +6,7 @@
6
6
  import { resolveValues } from './resolveValues.js';
7
7
  import { getValues } from './getValues.js';
8
8
  import { evaluatePathWithMethods } from './assignGingerly.js';
9
- import { isAllowedImportPath } from './isAllowedImportPath.js';
9
+ import { buildRestrictedPropSet, isAllowedImportPath, redirectRestrictedProp } from './isAllowedImportPath.js';
10
10
  /**
11
11
  * Map of built-in handler names to their module paths.
12
12
  * These are auto-loaded on demand — no explicit import required.
@@ -102,6 +102,7 @@ async function resolveFromHandlers(name, handlers, permissions) {
102
102
  * @param handlerRegistry - The registry of handler classes
103
103
  */
104
104
  export async function processHandlerCommands(target, handlerKeys, pattern, options, permissions) {
105
+ const restrictedPropSet = buildRestrictedPropSet(permissions);
105
106
  for (const key of handlerKeys) {
106
107
  const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
107
108
  const rhs = pattern[key];
@@ -211,11 +212,12 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
211
212
  // Instantiate and invoke the handler
212
213
  const handler = new HandlerClass(config);
213
214
  //return; //1.5ms
214
- const result = await handler.assign(lhsTarget, resolvedParams, options);
215
+ const result = await handler.assign(lhsTarget, resolvedParams, options, permissions);
215
216
  //return; //1.5ms
216
217
  // Return-value protocol: if handler returns a non-undefined value,
217
218
  // assign it back to the LHS path
218
- if (result !== undefined && lhsParent != null && lhsKey != null) {
219
+ if (result !== undefined && lhsParent != null && lhsKey != null
220
+ && !redirectRestrictedProp(restrictedPropSet, lhsParent, lhsKey, result)) {
219
221
  lhsParent[lhsKey] = result;
220
222
  }
221
223
  }
@@ -7,7 +7,7 @@
7
7
  import { resolveValues } from './resolveValues.js';
8
8
  import { getValues } from './getValues.js';
9
9
  import { evaluatePathWithMethods } from './assignGingerly.js';
10
- import { isAllowedImportPath } from './isAllowedImportPath.js';
10
+ import { buildRestrictedPropSet, isAllowedImportPath, redirectRestrictedProp } from './isAllowedImportPath.js';
11
11
  import type { AssignPermissions } from './isAllowedImportPath.js';
12
12
  import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
13
13
 
@@ -125,8 +125,9 @@ export async function processHandlerCommands(
125
125
  pattern: Record<string, any>,
126
126
  options: AssignFromOptions,
127
127
  permissions?: AssignPermissions
128
- ): Promise<void> {
129
-
128
+ ): Promise<void> {
129
+ const restrictedPropSet = buildRestrictedPropSet(permissions);
130
+
130
131
  for (const key of handlerKeys) {
131
132
  const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
132
133
  const rhs = pattern[key];
@@ -238,12 +239,13 @@ export async function processHandlerCommands(
238
239
  // Instantiate and invoke the handler
239
240
  const handler = new HandlerClass(config);
240
241
  //return; //1.5ms
241
- const result = await handler.assign(lhsTarget, resolvedParams, options);
242
+ const result = await handler.assign(lhsTarget, resolvedParams, options, permissions);
242
243
  //return; //1.5ms
243
- // Return-value protocol: if handler returns a non-undefined value,
244
- // assign it back to the LHS path
245
- if (result !== undefined && lhsParent != null && lhsKey != null) {
246
- lhsParent[lhsKey] = result;
244
+ // Return-value protocol: if handler returns a non-undefined value,
245
+ // assign it back to the LHS path
246
+ if (result !== undefined && lhsParent != null && lhsKey != null
247
+ && !redirectRestrictedProp(restrictedPropSet, lhsParent, lhsKey, result)) {
248
+ lhsParent[lhsKey] = result;
247
249
  }
248
250
  }
249
251
  }
@@ -252,6 +252,12 @@ export interface IAssignGingerlyOptions {
252
252
  bypassChecks?: boolean;
253
253
  /**
254
254
  * Method names to call during path evaluation (e.g., for `?.method()` calls)
255
+ *
256
+ * Append `|` to a path segment (e.g., `?.deref|?.classList?.add`) to call a
257
+ * listed method with zero arguments instead of consuming the next path segment
258
+ * as its argument. On the last segment, `|` calls the method with no arguments
259
+ * and ignores the value. The `|` suffix only applies to names listed here —
260
+ * for any other segment it is treated as part of a literal property name.
255
261
  */
256
262
  withMethods?: string[] | Set<string>;
257
263
  /**
@@ -343,7 +349,7 @@ export interface AssignFromOptions {
343
349
  /** Protocol handlers (sync or async) */
344
350
  protocols?: Record<string, (key: string) => any | Promise<any>>;
345
351
 
346
- /** Method names to call during path evaluation */
352
+ /** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
347
353
  withMethods?: string[] | Set<string>;
348
354
 
349
355
  /** Alias mappings for path segments */
@@ -700,8 +706,8 @@ export interface HandlerConfig {
700
706
  * Interface for assignFrom handler classes.
701
707
  * Handlers are invoked when a LHS key ends with ' =>'.
702
708
  */
703
- export interface AssignFromHandler {
704
- assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any): Promise<void> | void;
709
+ export interface AssignFromHandler {
710
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any, permissions?: AssignPermissions): Promise<void> | void;
705
711
  }
706
712
 
707
713
  /**
@@ -879,7 +885,7 @@ export interface LazyLoadInstantiatedContext {
879
885
  export declare class LazyLoadHandler implements AssignFromHandler {
880
886
  config: any;
881
887
  constructor(config: any);
882
- assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any): Promise<void>;
888
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any, permissions?: AssignPermissions): Promise<void>;
883
889
  protected onCloneInserted(nodes: Node[], lhsTarget: Element, resolvedParams: Record<string, any>): Promise<void>;
884
890
  }
885
891
 
@@ -927,3 +933,54 @@ export interface AddEventListenerConfig extends AssignDispatchVector {
927
933
 
928
934
  }
929
935
  //#endregion
936
+
937
+
938
+ // =============================================================================
939
+ // Permissions
940
+ // =============================================================================
941
+
942
+ /**
943
+ * Phase II+: object form for a restricted property setting.
944
+ */
945
+ export interface RestrictedPropSetting {
946
+ prop: string;
947
+ useMethod?: string; // Phase II: redirect to a safe method
948
+ attr?: string; // Phase III: also watch setAttribute for this attr
949
+ allowFromSameHost?: boolean; // Phase III
950
+ allowCrossDomain?: boolean; // Phase III
951
+ }
952
+
953
+ /**
954
+ * Phase IV+: object form for a restricted method setting.
955
+ */
956
+ export interface RestrictedMethodConfig {
957
+ method: string;
958
+ addArgs?: string[]; // Phase V: append sanitizer args
959
+ }
960
+
961
+ /**
962
+ * Permissions interface for controlling security-sensitive operations.
963
+ * Only trusted script can set these — never parsed from HTML attributes.
964
+ */
965
+ export interface AssignPermissions {
966
+ /** Allow imports from cross-domain URLs (default: false) */
967
+ crossDomainImports?: boolean;
968
+
969
+ /**
970
+ * Restricted property settings.
971
+ * Phase I: string entries are property names that cannot be assigned.
972
+ * Phase II: an object with useMethod redirects ordinary assignment to that
973
+ * method; command operations remain blocked. Phase III+ adds attr support.
974
+ *
975
+ * NOTE: This is a property-assignment guard only. Method calls (setAttribute, etc.)
976
+ * are not blocked — see Phase III+. Event listeners can still be registered, but
977
+ * assignments performed by their vectors inherit these permissions.
978
+ */
979
+ restrictedPropSettings?: Array<string | RestrictedPropSetting>;
980
+
981
+ /** Sanitizer options (Phase III+) */
982
+ sanitizerOptions?: Record<string, any>;
983
+
984
+ /** Restricted method settings (Phase IV+) */
985
+ restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
986
+ }