assign-gingerly 0.0.71 → 0.0.73
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/DX/emojis.js +16 -0
- package/DX/emojis.ts +16 -0
- package/README.md +30 -3
- package/assignFrom.js +6 -6
- package/assignFrom.ts +11 -10
- package/assignFromAsync.js +3 -3
- package/assignFromAsync.ts +4 -4
- package/assignGingerly.js +136 -69
- package/assignGingerly.ts +170 -103
- package/assignPermissions/isAllowedImportPath.js +41 -0
- package/assignPermissions/isAllowedImportPath.ts +38 -0
- package/assignPermissions/restrictedProps.js +43 -0
- package/assignPermissions/restrictedProps.ts +53 -0
- package/assignTentatively.js +45 -30
- package/assignTentatively.ts +85 -60
- package/defineWithFeatures.js +38 -32
- package/defineWithFeatures.ts +46 -39
- package/eachTime.js +12 -4
- package/eachTime.ts +17 -7
- package/enhanceAll.js +2 -2
- package/enhanceAll.ts +3 -3
- package/evaluatePathWithAsyncMethods.js +29 -16
- package/evaluatePathWithAsyncMethods.ts +31 -16
- package/handlers/addEventListener.js +11 -11
- package/handlers/addEventListener.ts +18 -15
- package/handlers/lazyLoad.ts +10 -7
- package/handlers/lazyLoadSwitch.ts +4 -3
- package/handlers/manageTemplateList.js +9 -9
- package/handlers/manageTemplateList.ts +11 -10
- package/handlers/rangeSelector.ts +4 -3
- package/inferencer/types/assign-gingerly/types.d.ts +63 -4
- package/inferencer/types/nested-regex-groups/types.d.ts +12 -0
- package/package.json +6 -5
- package/processHandlerCommands.js +7 -4
- package/processHandlerCommands.ts +13 -10
- package/types/assign-gingerly/types.d.ts +63 -4
- package/isAllowedImportPath.js +0 -42
- package/isAllowedImportPath.ts +0 -53
|
@@ -30,7 +30,8 @@ import type { ManageTemplateListResolvedParams } from '../types/assign-gingerly/
|
|
|
30
30
|
import { findMarkers, createMarkers, getNodesBetweenMarkers, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
|
|
31
31
|
import { resolveValue } from '../resolveValues.js';
|
|
32
32
|
import { assignFrom } from '../assignFrom.js';
|
|
33
|
-
import { processInferredAssignments } from '../inferredAssignments.js';
|
|
33
|
+
import { processInferredAssignments } from '../inferredAssignments.js';
|
|
34
|
+
import type { AssignPermissions } from '../types/assign-gingerly/types.js';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Reserved keys in fromEachItem config — not treated as shorthand patterns.
|
|
@@ -74,7 +75,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
74
75
|
this.config = config;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
async assign(lhsTarget: any, resolvedParams: ManageTemplateListResolvedParams, options?: any): Promise<void> {
|
|
78
|
+
async assign(lhsTarget: any, resolvedParams: ManageTemplateListResolvedParams, options?: any, permissions?: AssignPermissions): Promise<void> {
|
|
78
79
|
//return;
|
|
79
80
|
const {
|
|
80
81
|
forEach: items,
|
|
@@ -183,7 +184,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
183
184
|
const len = Math.min(elements.length, configs.length);
|
|
184
185
|
for (let j = 0; j < len; j++) {
|
|
185
186
|
const cfg = configs[j];
|
|
186
|
-
assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions });
|
|
187
|
+
assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions }, permissions);
|
|
187
188
|
}
|
|
188
189
|
} else {
|
|
189
190
|
const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
|
|
@@ -191,13 +192,13 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
191
192
|
if (processInferred) {
|
|
192
193
|
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
193
194
|
} else {
|
|
194
|
-
assignFrom(rootEl, toClone, { from: item, ...withOptions });
|
|
195
|
+
assignFrom(rootEl, toClone, { from: item, ...withOptions }, permissions);
|
|
195
196
|
}
|
|
196
197
|
if (hostToClone && options?.from) {
|
|
197
|
-
assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions });
|
|
198
|
+
assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions }, permissions);
|
|
198
199
|
}
|
|
199
200
|
if (targetToClone) {
|
|
200
|
-
assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions });
|
|
201
|
+
assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions }, permissions);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -228,7 +229,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
228
229
|
const len = Math.min(elements.length, configs.length);
|
|
229
230
|
for (let j = 0; j < len; j++) {
|
|
230
231
|
const cfg = configs[j];
|
|
231
|
-
assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions });
|
|
232
|
+
assignFrom(elements[j], cfg.toClone ?? {}, { from: item, ...cfg.withOptions }, permissions);
|
|
232
233
|
}
|
|
233
234
|
} else {
|
|
234
235
|
const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
|
|
@@ -239,14 +240,14 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
239
240
|
if (processInferred) {
|
|
240
241
|
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
241
242
|
} else {
|
|
242
|
-
assignFrom(rootEl, toClone, { from: item, ...withOptions });
|
|
243
|
+
assignFrom(rootEl, toClone, { from: item, ...withOptions }, permissions);
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
if (hostToClone && options?.from) {
|
|
246
|
-
assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions });
|
|
247
|
+
assignFrom(rootEl, hostToClone, { from: options.from, ...hostWithOptions }, permissions);
|
|
247
248
|
}
|
|
248
249
|
if (targetToClone) {
|
|
249
|
-
assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions });
|
|
250
|
+
assignFrom(rootEl, targetToClone, { from: lhsTarget, ...targetWithOptions }, permissions);
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
25
|
-
import assignGingerly from '../assignGingerly.js';
|
|
25
|
+
import assignGingerly from '../assignGingerly.js';
|
|
26
|
+
import type { AssignPermissions } from '../types/assign-gingerly/types.js';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Operator keys recognized in case objects.
|
|
@@ -72,7 +73,7 @@ export class RangeSelectorHandler implements AssignFromHandler {
|
|
|
72
73
|
this.config = config;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
async assign(lhsTarget: any, resolvedParams: any): Promise<void> {
|
|
76
|
+
async assign(lhsTarget: any, resolvedParams: any, _options?: any, permissions?: AssignPermissions): Promise<void> {
|
|
76
77
|
const { value, when } = resolvedParams;
|
|
77
78
|
|
|
78
79
|
if (!Array.isArray(when)) return;
|
|
@@ -81,7 +82,7 @@ export class RangeSelectorHandler implements AssignFromHandler {
|
|
|
81
82
|
for (const caseObj of when) {
|
|
82
83
|
if (caseMatches(value, caseObj)) {
|
|
83
84
|
if (caseObj.merge && typeof caseObj.merge === 'object') {
|
|
84
|
-
assignGingerly(lhsTarget, caseObj.merge);
|
|
85
|
+
assignGingerly(lhsTarget, caseObj.merge, undefined, permissions);
|
|
85
86
|
}
|
|
86
87
|
return; // First match wins
|
|
87
88
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {ParserOptions} from '../nested-regex-groups/types.js';
|
|
2
|
+
|
|
1
3
|
export type EnhKey = string | symbol;
|
|
2
4
|
|
|
3
5
|
// type NoUnderscore<T extends string> = T extends `_${string}` ? never : T;
|
|
@@ -196,7 +198,7 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
|
196
198
|
* For named parsers like 'parse-pattern-statements', this is forwarded
|
|
197
199
|
* as the options argument to the underlying parse function.
|
|
198
200
|
*/
|
|
199
|
-
parserOptions?:
|
|
201
|
+
parserOptions?: ParserOptions;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
export type AttrPatterns<T = any> = {
|
|
@@ -252,6 +254,12 @@ export interface IAssignGingerlyOptions {
|
|
|
252
254
|
bypassChecks?: boolean;
|
|
253
255
|
/**
|
|
254
256
|
* Method names to call during path evaluation (e.g., for `?.method()` calls)
|
|
257
|
+
*
|
|
258
|
+
* Append `|` to a path segment (e.g., `?.deref|?.classList?.add`) to call a
|
|
259
|
+
* listed method with zero arguments instead of consuming the next path segment
|
|
260
|
+
* as its argument. On the last segment, `|` calls the method with no arguments
|
|
261
|
+
* and ignores the value. The `|` suffix only applies to names listed here —
|
|
262
|
+
* for any other segment it is treated as part of a literal property name.
|
|
255
263
|
*/
|
|
256
264
|
withMethods?: string[] | Set<string>;
|
|
257
265
|
/**
|
|
@@ -343,7 +351,7 @@ export interface AssignFromOptions {
|
|
|
343
351
|
/** Protocol handlers (sync or async) */
|
|
344
352
|
protocols?: Record<string, (key: string) => any | Promise<any>>;
|
|
345
353
|
|
|
346
|
-
/** Method names to call during path evaluation */
|
|
354
|
+
/** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
|
|
347
355
|
withMethods?: string[] | Set<string>;
|
|
348
356
|
|
|
349
357
|
/** Alias mappings for path segments */
|
|
@@ -701,7 +709,7 @@ export interface HandlerConfig {
|
|
|
701
709
|
* Handlers are invoked when a LHS key ends with ' =>'.
|
|
702
710
|
*/
|
|
703
711
|
export interface AssignFromHandler {
|
|
704
|
-
assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any): Promise<void> | void;
|
|
712
|
+
assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any, permissions?: AssignPermissions): Promise<void> | void;
|
|
705
713
|
}
|
|
706
714
|
|
|
707
715
|
/**
|
|
@@ -879,7 +887,7 @@ export interface LazyLoadInstantiatedContext {
|
|
|
879
887
|
export declare class LazyLoadHandler implements AssignFromHandler {
|
|
880
888
|
config: any;
|
|
881
889
|
constructor(config: any);
|
|
882
|
-
assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any): Promise<void>;
|
|
890
|
+
assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any, permissions?: AssignPermissions): Promise<void>;
|
|
883
891
|
protected onCloneInserted(nodes: Node[], lhsTarget: Element, resolvedParams: Record<string, any>): Promise<void>;
|
|
884
892
|
}
|
|
885
893
|
|
|
@@ -927,3 +935,54 @@ export interface AddEventListenerConfig extends AssignDispatchVector {
|
|
|
927
935
|
|
|
928
936
|
}
|
|
929
937
|
//#endregion
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
// =============================================================================
|
|
941
|
+
// Permissions
|
|
942
|
+
// =============================================================================
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Phase II+: object form for a restricted property setting.
|
|
946
|
+
*/
|
|
947
|
+
export interface RestrictedPropSetting {
|
|
948
|
+
prop: string;
|
|
949
|
+
useMethod?: string; // Phase II: redirect to a safe method
|
|
950
|
+
attr?: string; // Phase III: also watch setAttribute for this attr
|
|
951
|
+
allowFromSameHost?: boolean; // Phase III
|
|
952
|
+
allowCrossDomain?: boolean; // Phase III
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Phase IV+: object form for a restricted method setting.
|
|
957
|
+
*/
|
|
958
|
+
export interface RestrictedMethodConfig {
|
|
959
|
+
method: string;
|
|
960
|
+
addArgs?: string[]; // Phase V: append sanitizer args
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Permissions interface for controlling security-sensitive operations.
|
|
965
|
+
* Only trusted script can set these — never parsed from HTML attributes.
|
|
966
|
+
*/
|
|
967
|
+
export interface AssignPermissions {
|
|
968
|
+
/** Allow imports from cross-domain URLs (default: false) */
|
|
969
|
+
crossDomainImports?: boolean;
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Restricted property settings.
|
|
973
|
+
* Phase I: string entries are property names that cannot be assigned.
|
|
974
|
+
* Phase II: an object with useMethod redirects ordinary assignment to that
|
|
975
|
+
* method; command operations remain blocked. Phase III+ adds attr support.
|
|
976
|
+
*
|
|
977
|
+
* NOTE: This is a property-assignment guard only. Method calls (setAttribute, etc.)
|
|
978
|
+
* are not blocked — see Phase III+. Event listeners can still be registered, but
|
|
979
|
+
* assignments performed by their vectors inherit these permissions.
|
|
980
|
+
*/
|
|
981
|
+
restrictedPropSettings?: Array<string | RestrictedPropSetting>;
|
|
982
|
+
|
|
983
|
+
/** Sanitizer options (Phase III+) */
|
|
984
|
+
sanitizerOptions?: Record<string, any>;
|
|
985
|
+
|
|
986
|
+
/** Restricted method settings (Phase IV+) */
|
|
987
|
+
restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
|
|
988
|
+
}
|
|
@@ -91,6 +91,18 @@ export interface ParserOptions {
|
|
|
91
91
|
* as statement delimiters during splitting.
|
|
92
92
|
*/
|
|
93
93
|
ignorePeriodInsideBraces?: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* When true, normalizes whitespace in the input string before parsing.
|
|
97
|
+
* This replaces multiple whitespace characters with a single space and trims the string.
|
|
98
|
+
* Default is false.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Input: " First. Second. "
|
|
102
|
+
* // With normalizeWhitespace: true -> "First. Second."
|
|
103
|
+
* // With normalizeWhitespace: false -> " First. Second. "
|
|
104
|
+
*/
|
|
105
|
+
normalizeWhitespace?: boolean;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.73",
|
|
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": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"*.js",
|
|
19
19
|
"*.ts",
|
|
20
20
|
"DX/**",
|
|
21
|
+
"assignPermissions/**",
|
|
21
22
|
"handlers/**",
|
|
22
23
|
"inferencer/**",
|
|
23
24
|
"README.md",
|
|
@@ -143,9 +144,9 @@
|
|
|
143
144
|
"default": "./enhanceAll.js",
|
|
144
145
|
"types": "./enhanceAll.ts"
|
|
145
146
|
},
|
|
146
|
-
"./isAllowedImportPath.js": {
|
|
147
|
-
"default": "./isAllowedImportPath.js",
|
|
148
|
-
"types": "./isAllowedImportPath.ts"
|
|
147
|
+
"./assignPermissions/isAllowedImportPath.js": {
|
|
148
|
+
"default": "./assignPermissions/isAllowedImportPath.js",
|
|
149
|
+
"types": "./assignPermissions/isAllowedImportPath.ts"
|
|
149
150
|
},
|
|
150
151
|
"./markerUtils.js": {
|
|
151
152
|
"default": "./markerUtils.js",
|
|
@@ -203,7 +204,7 @@
|
|
|
203
204
|
"chrome": "npx playwright cr http://localhost:8000"
|
|
204
205
|
},
|
|
205
206
|
"devDependencies": {
|
|
206
|
-
"@playwright/test": "1.62.
|
|
207
|
+
"@playwright/test": "1.62.1",
|
|
207
208
|
"spa-ssi": "0.0.27"
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
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 { isAllowedImportPath } from './assignPermissions/isAllowedImportPath.js';
|
|
10
|
+
import { buildRestrictedPropSet, redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
10
11
|
/**
|
|
11
12
|
* Map of built-in handler names to their module paths.
|
|
12
13
|
* These are auto-loaded on demand — no explicit import required.
|
|
@@ -80,7 +81,7 @@ async function resolveFromHandlers(name, handlers, permissions) {
|
|
|
80
81
|
// Import path string — validate and dynamically import
|
|
81
82
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
82
83
|
throw new Error(`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
83
|
-
`Only
|
|
84
|
+
`Only same-origin paths or import-map-covered specifiers are allowed. ` +
|
|
84
85
|
`Pass { crossDomainImports: true } in permissions to override.`);
|
|
85
86
|
}
|
|
86
87
|
const module = await import(entry);
|
|
@@ -102,6 +103,7 @@ async function resolveFromHandlers(name, handlers, permissions) {
|
|
|
102
103
|
* @param handlerRegistry - The registry of handler classes
|
|
103
104
|
*/
|
|
104
105
|
export async function processHandlerCommands(target, handlerKeys, pattern, options, permissions) {
|
|
106
|
+
const restrictedPropSet = buildRestrictedPropSet(permissions);
|
|
105
107
|
for (const key of handlerKeys) {
|
|
106
108
|
const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
|
|
107
109
|
const rhs = pattern[key];
|
|
@@ -211,11 +213,12 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
|
|
|
211
213
|
// Instantiate and invoke the handler
|
|
212
214
|
const handler = new HandlerClass(config);
|
|
213
215
|
//return; //1.5ms
|
|
214
|
-
const result = await handler.assign(lhsTarget, resolvedParams, options);
|
|
216
|
+
const result = await handler.assign(lhsTarget, resolvedParams, options, permissions);
|
|
215
217
|
//return; //1.5ms
|
|
216
218
|
// Return-value protocol: if handler returns a non-undefined value,
|
|
217
219
|
// assign it back to the LHS path
|
|
218
|
-
if (result !== undefined && lhsParent != null && lhsKey != null
|
|
220
|
+
if (result !== undefined && lhsParent != null && lhsKey != null
|
|
221
|
+
&& !redirectRestrictedProp(restrictedPropSet, lhsParent, lhsKey, result)) {
|
|
219
222
|
lhsParent[lhsKey] = result;
|
|
220
223
|
}
|
|
221
224
|
}
|
|
@@ -7,8 +7,9 @@
|
|
|
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';
|
|
11
|
-
import
|
|
10
|
+
import { isAllowedImportPath } from './assignPermissions/isAllowedImportPath.js';
|
|
11
|
+
import { buildRestrictedPropSet, redirectRestrictedProp } from './assignPermissions/restrictedProps.js';
|
|
12
|
+
import type { AssignPermissions } from './types/assign-gingerly/types.js';
|
|
12
13
|
import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -93,7 +94,7 @@ async function resolveFromHandlers(
|
|
|
93
94
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
94
95
|
throw new Error(
|
|
95
96
|
`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
96
|
-
`Only
|
|
97
|
+
`Only same-origin paths or import-map-covered specifiers are allowed. ` +
|
|
97
98
|
`Pass { crossDomainImports: true } in permissions to override.`
|
|
98
99
|
);
|
|
99
100
|
}
|
|
@@ -125,8 +126,9 @@ export async function processHandlerCommands(
|
|
|
125
126
|
pattern: Record<string, any>,
|
|
126
127
|
options: AssignFromOptions,
|
|
127
128
|
permissions?: AssignPermissions
|
|
128
|
-
): Promise<void> {
|
|
129
|
-
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const restrictedPropSet = buildRestrictedPropSet(permissions);
|
|
131
|
+
|
|
130
132
|
for (const key of handlerKeys) {
|
|
131
133
|
const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
|
|
132
134
|
const rhs = pattern[key];
|
|
@@ -238,12 +240,13 @@ export async function processHandlerCommands(
|
|
|
238
240
|
// Instantiate and invoke the handler
|
|
239
241
|
const handler = new HandlerClass(config);
|
|
240
242
|
//return; //1.5ms
|
|
241
|
-
const result = await handler.assign(lhsTarget, resolvedParams, options);
|
|
243
|
+
const result = await handler.assign(lhsTarget, resolvedParams, options, permissions);
|
|
242
244
|
//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
|
|
245
|
+
// Return-value protocol: if handler returns a non-undefined value,
|
|
246
|
+
// assign it back to the LHS path
|
|
247
|
+
if (result !== undefined && lhsParent != null && lhsKey != null
|
|
248
|
+
&& !redirectRestrictedProp(restrictedPropSet, lhsParent, lhsKey, result)) {
|
|
249
|
+
lhsParent[lhsKey] = result;
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {ParserOptions} from '../nested-regex-groups/types.js';
|
|
2
|
+
|
|
1
3
|
export type EnhKey = string | symbol;
|
|
2
4
|
|
|
3
5
|
// type NoUnderscore<T extends string> = T extends `_${string}` ? never : T;
|
|
@@ -196,7 +198,7 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
|
196
198
|
* For named parsers like 'parse-pattern-statements', this is forwarded
|
|
197
199
|
* as the options argument to the underlying parse function.
|
|
198
200
|
*/
|
|
199
|
-
parserOptions?:
|
|
201
|
+
parserOptions?: ParserOptions;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
export type AttrPatterns<T = any> = {
|
|
@@ -252,6 +254,12 @@ export interface IAssignGingerlyOptions {
|
|
|
252
254
|
bypassChecks?: boolean;
|
|
253
255
|
/**
|
|
254
256
|
* Method names to call during path evaluation (e.g., for `?.method()` calls)
|
|
257
|
+
*
|
|
258
|
+
* Append `|` to a path segment (e.g., `?.deref|?.classList?.add`) to call a
|
|
259
|
+
* listed method with zero arguments instead of consuming the next path segment
|
|
260
|
+
* as its argument. On the last segment, `|` calls the method with no arguments
|
|
261
|
+
* and ignores the value. The `|` suffix only applies to names listed here —
|
|
262
|
+
* for any other segment it is treated as part of a literal property name.
|
|
255
263
|
*/
|
|
256
264
|
withMethods?: string[] | Set<string>;
|
|
257
265
|
/**
|
|
@@ -343,7 +351,7 @@ export interface AssignFromOptions {
|
|
|
343
351
|
/** Protocol handlers (sync or async) */
|
|
344
352
|
protocols?: Record<string, (key: string) => any | Promise<any>>;
|
|
345
353
|
|
|
346
|
-
/** Method names to call during path evaluation */
|
|
354
|
+
/** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
|
|
347
355
|
withMethods?: string[] | Set<string>;
|
|
348
356
|
|
|
349
357
|
/** Alias mappings for path segments */
|
|
@@ -701,7 +709,7 @@ export interface HandlerConfig {
|
|
|
701
709
|
* Handlers are invoked when a LHS key ends with ' =>'.
|
|
702
710
|
*/
|
|
703
711
|
export interface AssignFromHandler {
|
|
704
|
-
assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any): Promise<void> | void;
|
|
712
|
+
assign(lhsTarget: any, resolvedParams: Record<string, any>, options: any, permissions?: AssignPermissions): Promise<void> | void;
|
|
705
713
|
}
|
|
706
714
|
|
|
707
715
|
/**
|
|
@@ -879,7 +887,7 @@ export interface LazyLoadInstantiatedContext {
|
|
|
879
887
|
export declare class LazyLoadHandler implements AssignFromHandler {
|
|
880
888
|
config: any;
|
|
881
889
|
constructor(config: any);
|
|
882
|
-
assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any): Promise<void>;
|
|
890
|
+
assign(lhsTarget: any, resolvedParams: Record<string, any>, options?: any, permissions?: AssignPermissions): Promise<void>;
|
|
883
891
|
protected onCloneInserted(nodes: Node[], lhsTarget: Element, resolvedParams: Record<string, any>): Promise<void>;
|
|
884
892
|
}
|
|
885
893
|
|
|
@@ -927,3 +935,54 @@ export interface AddEventListenerConfig extends AssignDispatchVector {
|
|
|
927
935
|
|
|
928
936
|
}
|
|
929
937
|
//#endregion
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
// =============================================================================
|
|
941
|
+
// Permissions
|
|
942
|
+
// =============================================================================
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Phase II+: object form for a restricted property setting.
|
|
946
|
+
*/
|
|
947
|
+
export interface RestrictedPropSetting {
|
|
948
|
+
prop: string;
|
|
949
|
+
useMethod?: string; // Phase II: redirect to a safe method
|
|
950
|
+
attr?: string; // Phase III: also watch setAttribute for this attr
|
|
951
|
+
allowFromSameHost?: boolean; // Phase III
|
|
952
|
+
allowCrossDomain?: boolean; // Phase III
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Phase IV+: object form for a restricted method setting.
|
|
957
|
+
*/
|
|
958
|
+
export interface RestrictedMethodConfig {
|
|
959
|
+
method: string;
|
|
960
|
+
addArgs?: string[]; // Phase V: append sanitizer args
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Permissions interface for controlling security-sensitive operations.
|
|
965
|
+
* Only trusted script can set these — never parsed from HTML attributes.
|
|
966
|
+
*/
|
|
967
|
+
export interface AssignPermissions {
|
|
968
|
+
/** Allow imports from cross-domain URLs (default: false) */
|
|
969
|
+
crossDomainImports?: boolean;
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Restricted property settings.
|
|
973
|
+
* Phase I: string entries are property names that cannot be assigned.
|
|
974
|
+
* Phase II: an object with useMethod redirects ordinary assignment to that
|
|
975
|
+
* method; command operations remain blocked. Phase III+ adds attr support.
|
|
976
|
+
*
|
|
977
|
+
* NOTE: This is a property-assignment guard only. Method calls (setAttribute, etc.)
|
|
978
|
+
* are not blocked — see Phase III+. Event listeners can still be registered, but
|
|
979
|
+
* assignments performed by their vectors inherit these permissions.
|
|
980
|
+
*/
|
|
981
|
+
restrictedPropSettings?: Array<string | RestrictedPropSetting>;
|
|
982
|
+
|
|
983
|
+
/** Sanitizer options (Phase III+) */
|
|
984
|
+
sanitizerOptions?: Record<string, any>;
|
|
985
|
+
|
|
986
|
+
/** Restricted method settings (Phase IV+) */
|
|
987
|
+
restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
|
|
988
|
+
}
|
package/isAllowedImportPath.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* isAllowedImportPath.ts — Security utility for validating import paths.
|
|
3
|
-
*
|
|
4
|
-
* Checks that a path is local (relative, absolute, or bare specifier) and
|
|
5
|
-
* not a cross-domain URL. Used to prevent untrusted HTML attributes from
|
|
6
|
-
* triggering imports to arbitrary external domains.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* import { isAllowedImportPath } from 'assign-gingerly/isAllowedImportPath.js';
|
|
10
|
-
*
|
|
11
|
-
* isAllowedImportPath('./local.js'); // true
|
|
12
|
-
* isAllowedImportPath('../parent/file.js'); // true
|
|
13
|
-
* isAllowedImportPath('/absolute/path.js'); // true
|
|
14
|
-
* isAllowedImportPath('bare-specifier/mod.js'); // true
|
|
15
|
-
* isAllowedImportPath('https://evil.com/x.js'); // false
|
|
16
|
-
* isAllowedImportPath('//cdn.example.com/x.js'); // false
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Check if an import path is allowed (non-cross-domain).
|
|
20
|
-
*
|
|
21
|
-
* Allowed:
|
|
22
|
-
* - Relative paths: ./foo.js, ../bar.js
|
|
23
|
-
* - Absolute paths: /path/to/file.js
|
|
24
|
-
* - Bare specifiers: package-name/file.js, @scope/package/file.js
|
|
25
|
-
*
|
|
26
|
-
* Blocked:
|
|
27
|
-
* - Protocol URLs: https://..., http://..., data:..., etc.
|
|
28
|
-
* - Protocol-relative: //cdn.example.com/...
|
|
29
|
-
*
|
|
30
|
-
* @param path - The import path to validate
|
|
31
|
-
* @returns true if the path is local/safe, false if cross-domain
|
|
32
|
-
*/
|
|
33
|
-
export function isAllowedImportPath(path) {
|
|
34
|
-
if (path.startsWith('./') || path.startsWith('../') || path.startsWith('/')) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
if (path.includes('://') || path.startsWith('//')) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
// Bare specifier (no protocol, no //) — allowed
|
|
41
|
-
return true;
|
|
42
|
-
}
|
package/isAllowedImportPath.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* isAllowedImportPath.ts — Security utility for validating import paths.
|
|
3
|
-
*
|
|
4
|
-
* Checks that a path is local (relative, absolute, or bare specifier) and
|
|
5
|
-
* not a cross-domain URL. Used to prevent untrusted HTML attributes from
|
|
6
|
-
* triggering imports to arbitrary external domains.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* import { isAllowedImportPath } from 'assign-gingerly/isAllowedImportPath.js';
|
|
10
|
-
*
|
|
11
|
-
* isAllowedImportPath('./local.js'); // true
|
|
12
|
-
* isAllowedImportPath('../parent/file.js'); // true
|
|
13
|
-
* isAllowedImportPath('/absolute/path.js'); // true
|
|
14
|
-
* isAllowedImportPath('bare-specifier/mod.js'); // true
|
|
15
|
-
* isAllowedImportPath('https://evil.com/x.js'); // false
|
|
16
|
-
* isAllowedImportPath('//cdn.example.com/x.js'); // false
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
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.
|
|
23
|
-
*/
|
|
24
|
-
export interface AssignPermissions {
|
|
25
|
-
/** Allow imports from cross-domain URLs (default: false) */
|
|
26
|
-
crossDomainImports?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Check if an import path is allowed (non-cross-domain).
|
|
31
|
-
*
|
|
32
|
-
* Allowed:
|
|
33
|
-
* - Relative paths: ./foo.js, ../bar.js
|
|
34
|
-
* - Absolute paths: /path/to/file.js
|
|
35
|
-
* - Bare specifiers: package-name/file.js, @scope/package/file.js
|
|
36
|
-
*
|
|
37
|
-
* Blocked:
|
|
38
|
-
* - Protocol URLs: https://..., http://..., data:..., etc.
|
|
39
|
-
* - Protocol-relative: //cdn.example.com/...
|
|
40
|
-
*
|
|
41
|
-
* @param path - The import path to validate
|
|
42
|
-
* @returns true if the path is local/safe, false if cross-domain
|
|
43
|
-
*/
|
|
44
|
-
export function isAllowedImportPath(path: string): boolean {
|
|
45
|
-
if (path.startsWith('./') || path.startsWith('../') || path.startsWith('/')) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
if (path.includes('://') || path.startsWith('//')) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
// Bare specifier (no protocol, no //) — allowed
|
|
52
|
-
return true;
|
|
53
|
-
}
|