assign-gingerly 0.0.94 → 0.0.96
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/strictDefaultPermissions.js +64 -0
- package/README.md +2 -2
- package/index.js +1 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/resolve/getValues.js +41 -22
- package/resolve/getValues.ts +47 -28
- package/resolve/resolveValues.js +9 -17
- package/resolve/resolveValues.ts +116 -126
- package/types/assign-gingerly/types.d.ts +34 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML event-handler content attributes. The browser compiles these into
|
|
3
|
+
* live event handlers as soon as they're set via `setAttribute` (and they
|
|
4
|
+
* shadow the identically-named element properties), so both forms are
|
|
5
|
+
* blocked outright — there's no safe "same-domain" version of inline script.
|
|
6
|
+
*
|
|
7
|
+
* Not exhaustive (there is no wildcard/regex support in `restrictedPropSettings`
|
|
8
|
+
* today — see docs/assign-permissions.md). Spread this array and add more
|
|
9
|
+
* names for handlers not listed here.
|
|
10
|
+
*/
|
|
11
|
+
export const xssSensitiveAttrs = [
|
|
12
|
+
'onabort', 'onanimationend', 'onanimationiteration', 'onanimationstart',
|
|
13
|
+
'onauxclick', 'onbeforeinput', 'onbeforetoggle', 'onblur',
|
|
14
|
+
'onchange', 'onclick', 'oncontextmenu', 'oncopy', 'oncut',
|
|
15
|
+
'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave',
|
|
16
|
+
'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onfocus',
|
|
17
|
+
'oninput', 'onkeydown', 'onkeypress', 'onkeyup', 'onload',
|
|
18
|
+
'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove',
|
|
19
|
+
'onmouseout', 'onmouseover', 'onmouseup', 'onpaste',
|
|
20
|
+
'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove',
|
|
21
|
+
'onpointerout', 'onpointerover', 'onpointerup', 'onreset',
|
|
22
|
+
'onscroll', 'onselect', 'onsubmit', 'ontoggle',
|
|
23
|
+
'ontouchcancel', 'ontouchend', 'ontouchmove', 'ontouchstart',
|
|
24
|
+
'ontransitionend', 'onwheel',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Properties that assign raw markup/CSS into the DOM. There is no generic
|
|
28
|
+
* safe redirect for these, so they're blocked outright (Phase I) rather than
|
|
29
|
+
* gated by origin. Consumers who need HTML/CSS injection should route
|
|
30
|
+
* through a sanitizer via `useMethod` in their own extended config — see
|
|
31
|
+
* docs/assign-permissions.md, Phase II.
|
|
32
|
+
*/
|
|
33
|
+
export const xssSensitiveMarkupProps = ['innerHTML', 'outerHTML', 'srcdoc', 'cssText'];
|
|
34
|
+
/**
|
|
35
|
+
* Properties that carry a URL. Cross-origin values are blocked; same-origin
|
|
36
|
+
* values (including relative paths) are allowed through for both the
|
|
37
|
+
* property and the matching attribute. This stops `javascript:`, `data:`,
|
|
38
|
+
* and cross-origin navigation/exfiltration while leaving normal same-app
|
|
39
|
+
* links, images, scripts, and form actions working.
|
|
40
|
+
*/
|
|
41
|
+
export const xssSensitiveUrlProps = ['src', 'href', 'action', 'formAction'];
|
|
42
|
+
/**
|
|
43
|
+
* Methods most associated with runtime HTML/rich-text injection. Method
|
|
44
|
+
* blocking in `restrictedMethodSettings` is by name only, not by target type
|
|
45
|
+
* (see docs/assign-permissions.md, Phase IV), so this list is deliberately
|
|
46
|
+
* narrow to DOM-specific names unlikely to collide with unrelated methods on
|
|
47
|
+
* plain objects.
|
|
48
|
+
*/
|
|
49
|
+
export const xssSensitiveMethods = ['insertAdjacentHTML', 'setHTMLUnsafe', 'execCommand'];
|
|
50
|
+
/**
|
|
51
|
+
* A restrictive `AssignPermissions` default suitable as a starting point for
|
|
52
|
+
* any consumer that lets less-trusted input drive `assignGingerly`. Merge
|
|
53
|
+
* or spread it to tighten further — see the module doc above for examples.
|
|
54
|
+
*/
|
|
55
|
+
export const strictDefaultPermissions = {
|
|
56
|
+
crossDomainImports: false,
|
|
57
|
+
restrictedPropSettings: [
|
|
58
|
+
...xssSensitiveMarkupProps,
|
|
59
|
+
{ props: xssSensitiveUrlProps, attr: true, allowFromSameDomain: true },
|
|
60
|
+
{ props: xssSensitiveAttrs, attr: true },
|
|
61
|
+
],
|
|
62
|
+
restrictedMethodSettings: [...xssSensitiveMethods],
|
|
63
|
+
};
|
|
64
|
+
export default strictDefaultPermissions;
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ The third fundamental utility function is:
|
|
|
82
82
|
**`assignFrom` is synchronous** — it resolves paths, expands substitutions, handles spreads, processes `#[x]` refs, and runs inferred assignments all without yielding to the event loop. Handler commands (` =>`), `beVigilant`, and `enhance` are fire-and-forget (kicked off asynchronously in the background).
|
|
83
83
|
|
|
84
84
|
**`assignFromAsync`** is the awaitable variant for when you need to:
|
|
85
|
-
- Use async protocol handlers (e.g., `fetch`-based resolution)
|
|
85
|
+
- Use async protocol handlers (e.g., `fetch`-based resolution — protocols are introduced just below, and covered in full in [Protocol Resolution](#protocol-resolution-in-getvalues-and-assignfrom) later in this doc)
|
|
86
86
|
- `await` handler completion before proceeding
|
|
87
87
|
- Wait for `enhance` (EMC JSON imports) to finish
|
|
88
88
|
|
|
@@ -97,7 +97,7 @@ assignFrom adds support for:
|
|
|
97
97
|
|
|
98
98
|
1. Resolving RHS path strings against a source object (`from`).
|
|
99
99
|
2. Target-relative root references via `$0` so paths can resolve from the first argument passed to `assignFrom`/`assignFromAsync` (the target object) instead of the `from` object.
|
|
100
|
-
3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
|
|
100
|
+
3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols) — don't worry if this is unfamiliar yet, it's explained in full in [Protocol Resolution in `getValues` and `assignFrom`](#protocol-resolution-in-getvalues-and-assignfrom) later in this doc.
|
|
101
101
|
4. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
|
|
102
102
|
5. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
|
|
103
103
|
6. Dynamic substitutions via the `substitutions` option for injecting runtime string values into path segments. See [docs/substitutions.md](docs/substitutions.md).
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { buildCSSQuery } from './buildCSSQuery.js';
|
|
|
8
8
|
export { resolveTemplate } from './resolve/resolveTemplate.js';
|
|
9
9
|
export { getHost } from './getHost.js';
|
|
10
10
|
export { resolveValues, resolveValue } from './resolve/resolveValues.js';
|
|
11
|
+
export { getValues, getValue, hasProtocol, parseProtocolRef, isPlainObject } from './resolve/getValues.js';
|
|
11
12
|
export { assignFromAsync } from './assignFromAsync.js';
|
|
12
13
|
export { assignFrom } from './assignFrom.js';
|
|
13
14
|
export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
|
package/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export {buildCSSQuery} from './buildCSSQuery.js';
|
|
|
8
8
|
export {resolveTemplate} from './resolve/resolveTemplate.js';
|
|
9
9
|
export {getHost} from './getHost.js';
|
|
10
10
|
export {resolveValues, resolveValue} from './resolve/resolveValues.js';
|
|
11
|
+
export {getValues, getValue, hasProtocol, parseProtocolRef, isPlainObject} from './resolve/getValues.js';
|
|
11
12
|
export {assignFromAsync} from './assignFromAsync.js';
|
|
12
13
|
export {assignFrom} from './assignFrom.js';
|
|
13
14
|
export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
|
package/package.json
CHANGED
package/resolve/getValues.js
CHANGED
|
@@ -183,11 +183,47 @@ function navigatePath(source, parts, withMethods, permissionProcessor) {
|
|
|
183
183
|
return current;
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
186
|
+
* Whether a string carries a protocol prefix (contains `://`).
|
|
187
|
+
*
|
|
188
|
+
* Exported as the single source of truth for the outer USL grammar, so
|
|
189
|
+
* downstream packages (and resolveValues) don't re-implement the check.
|
|
187
190
|
*/
|
|
188
|
-
function hasProtocol(value) {
|
|
191
|
+
export function hasProtocol(value) {
|
|
189
192
|
return value.includes('://');
|
|
190
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Split a protocol-prefixed value string into its outer-grammar parts:
|
|
196
|
+
* `‹protocol›://‹key›?.‹path›`.
|
|
197
|
+
*
|
|
198
|
+
* - `protocol` is the text before `://` (`''` when there is no `://`).
|
|
199
|
+
* - `key` is the text between `://` and the first `?.` (or end of string).
|
|
200
|
+
* - `path` is the `?.`-onward remainder (starting with `?.`), or `null`.
|
|
201
|
+
*
|
|
202
|
+
* Pure string parsing — no handler lookup, no resolution. Shared by the sync
|
|
203
|
+
* (`getProtocolValue`) and async (`resolveProtocolValue`) resolvers.
|
|
204
|
+
*/
|
|
205
|
+
export function parseProtocolRef(value) {
|
|
206
|
+
const protoEnd = value.indexOf('://');
|
|
207
|
+
if (protoEnd === -1)
|
|
208
|
+
return { protocol: '', key: value, path: null };
|
|
209
|
+
const protocol = value.substring(0, protoEnd);
|
|
210
|
+
const rest = value.substring(protoEnd + 3);
|
|
211
|
+
const pathStart = rest.indexOf('?.');
|
|
212
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
213
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
214
|
+
return { protocol, key, path };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Whether a value is a plain object (prototype is `Object.prototype` or `null`),
|
|
218
|
+
* as opposed to an array, a class instance, or a primitive. Used to decide
|
|
219
|
+
* whether a nested value should be recursed into as a pattern.
|
|
220
|
+
*/
|
|
221
|
+
export function isPlainObject(value) {
|
|
222
|
+
if (!value || typeof value !== 'object')
|
|
223
|
+
return false;
|
|
224
|
+
const proto = Object.getPrototypeOf(value);
|
|
225
|
+
return proto === Object.prototype || proto === null;
|
|
226
|
+
}
|
|
191
227
|
/**
|
|
192
228
|
* Detect a leading run of `!` characters used as a boolean coercion / negation
|
|
193
229
|
* marker (`!` = negate, `!!` = coerce to boolean, `!!!` = negate, …). Returns the
|
|
@@ -266,15 +302,10 @@ function resolveStringValue(value, source, aliasMap, withMethods, protocols, opt
|
|
|
266
302
|
* Resolve a protocol-prefixed value synchronously.
|
|
267
303
|
*/
|
|
268
304
|
function getProtocolValue(value, protocols, options) {
|
|
269
|
-
const
|
|
270
|
-
const protocol = value.substring(0, protoEnd);
|
|
305
|
+
const { protocol, key, path } = parseProtocolRef(value);
|
|
271
306
|
const handler = protocols[protocol];
|
|
272
307
|
if (!handler)
|
|
273
308
|
return value; // not a recognized protocol
|
|
274
|
-
const rest = value.substring(protoEnd + 3);
|
|
275
|
-
const pathStart = rest.indexOf('?.');
|
|
276
|
-
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
277
|
-
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
278
309
|
const resolved = handler(key);
|
|
279
310
|
if (path) {
|
|
280
311
|
return getValue(path, resolved, options);
|
|
@@ -295,13 +326,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options, substi
|
|
|
295
326
|
result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
|
|
296
327
|
}
|
|
297
328
|
else if (item && typeof item === 'object') {
|
|
298
|
-
|
|
299
|
-
if (proto === Object.prototype || proto === null) {
|
|
300
|
-
result.push(getValues(item, source, options));
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
result.push(item);
|
|
304
|
-
}
|
|
329
|
+
result.push(isPlainObject(item) ? getValues(item, source, options) : item);
|
|
305
330
|
}
|
|
306
331
|
else {
|
|
307
332
|
result.push(item);
|
|
@@ -334,13 +359,7 @@ export function getValues(pattern, source, options) {
|
|
|
334
359
|
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
|
|
335
360
|
}
|
|
336
361
|
else if (typeof value === 'object' && value !== null) {
|
|
337
|
-
|
|
338
|
-
if (proto === Object.prototype || proto === null) {
|
|
339
|
-
result[key] = getValues(value, source, options);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
result[key] = value;
|
|
343
|
-
}
|
|
362
|
+
result[key] = isPlainObject(value) ? getValues(value, source, options) : value;
|
|
344
363
|
}
|
|
345
364
|
else {
|
|
346
365
|
result[key] = value;
|
package/resolve/getValues.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { GetValuesOptions, PermissionProcessor } from '../types/assign-gingerly/types.js';
|
|
20
|
+
import type { GetValuesOptions, ParsedProtocolRef, PermissionProcessor, SyncProtocolHandlers } from '../types/assign-gingerly/types.js';
|
|
21
21
|
|
|
22
22
|
export function normalizeAliasOptions(options?: {
|
|
23
23
|
aka?: Record<string, string>;
|
|
@@ -221,12 +221,48 @@ function navigatePath(
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
*
|
|
224
|
+
* Whether a string carries a protocol prefix (contains `://`).
|
|
225
|
+
*
|
|
226
|
+
* Exported as the single source of truth for the outer USL grammar, so
|
|
227
|
+
* downstream packages (and resolveValues) don't re-implement the check.
|
|
225
228
|
*/
|
|
226
|
-
function hasProtocol(value: string): boolean {
|
|
229
|
+
export function hasProtocol(value: string): boolean {
|
|
227
230
|
return value.includes('://');
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Split a protocol-prefixed value string into its outer-grammar parts:
|
|
235
|
+
* `‹protocol›://‹key›?.‹path›`.
|
|
236
|
+
*
|
|
237
|
+
* - `protocol` is the text before `://` (`''` when there is no `://`).
|
|
238
|
+
* - `key` is the text between `://` and the first `?.` (or end of string).
|
|
239
|
+
* - `path` is the `?.`-onward remainder (starting with `?.`), or `null`.
|
|
240
|
+
*
|
|
241
|
+
* Pure string parsing — no handler lookup, no resolution. Shared by the sync
|
|
242
|
+
* (`getProtocolValue`) and async (`resolveProtocolValue`) resolvers.
|
|
243
|
+
*/
|
|
244
|
+
export function parseProtocolRef(value: string): ParsedProtocolRef {
|
|
245
|
+
const protoEnd = value.indexOf('://');
|
|
246
|
+
if (protoEnd === -1) return { protocol: '', key: value, path: null };
|
|
247
|
+
const protocol = value.substring(0, protoEnd);
|
|
248
|
+
const rest = value.substring(protoEnd + 3);
|
|
249
|
+
const pathStart = rest.indexOf('?.');
|
|
250
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
251
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
252
|
+
return { protocol, key, path };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Whether a value is a plain object (prototype is `Object.prototype` or `null`),
|
|
257
|
+
* as opposed to an array, a class instance, or a primitive. Used to decide
|
|
258
|
+
* whether a nested value should be recursed into as a pattern.
|
|
259
|
+
*/
|
|
260
|
+
export function isPlainObject(value: any): boolean {
|
|
261
|
+
if (!value || typeof value !== 'object') return false;
|
|
262
|
+
const proto = Object.getPrototypeOf(value);
|
|
263
|
+
return proto === Object.prototype || proto === null;
|
|
264
|
+
}
|
|
265
|
+
|
|
230
266
|
/**
|
|
231
267
|
* Detect a leading run of `!` characters used as a boolean coercion / negation
|
|
232
268
|
* marker (`!` = negate, `!!` = coerce to boolean, `!!!` = negate, …). Returns the
|
|
@@ -249,7 +285,7 @@ function parseNegationMarker(value: string): { count: number; rest: string } | n
|
|
|
249
285
|
*/
|
|
250
286
|
function looksLikeReference(
|
|
251
287
|
value: string,
|
|
252
|
-
protocols:
|
|
288
|
+
protocols: SyncProtocolHandlers | undefined
|
|
253
289
|
): boolean {
|
|
254
290
|
return value.startsWith('?.')
|
|
255
291
|
|| value.startsWith('$0')
|
|
@@ -265,7 +301,7 @@ function resolveReferenceString(
|
|
|
265
301
|
source: any,
|
|
266
302
|
aliasMap: Map<string, string>,
|
|
267
303
|
withMethods: Set<string> | undefined,
|
|
268
|
-
protocols:
|
|
304
|
+
protocols: SyncProtocolHandlers | undefined,
|
|
269
305
|
options: GetValuesOptions | undefined,
|
|
270
306
|
substitutionMap: Map<string, string> | undefined
|
|
271
307
|
): any {
|
|
@@ -298,7 +334,7 @@ function resolveStringValue(
|
|
|
298
334
|
source: any,
|
|
299
335
|
aliasMap: Map<string, string>,
|
|
300
336
|
withMethods: Set<string> | undefined,
|
|
301
|
-
protocols:
|
|
337
|
+
protocols: SyncProtocolHandlers | undefined,
|
|
302
338
|
options: GetValuesOptions | undefined,
|
|
303
339
|
substitutionMap: Map<string, string> | undefined
|
|
304
340
|
): any {
|
|
@@ -325,21 +361,14 @@ function resolveStringValue(
|
|
|
325
361
|
*/
|
|
326
362
|
function getProtocolValue(
|
|
327
363
|
value: string,
|
|
328
|
-
protocols:
|
|
364
|
+
protocols: SyncProtocolHandlers,
|
|
329
365
|
options?: GetValuesOptions
|
|
330
366
|
): any {
|
|
331
|
-
const
|
|
332
|
-
const protocol = value.substring(0, protoEnd);
|
|
367
|
+
const { protocol, key, path } = parseProtocolRef(value);
|
|
333
368
|
|
|
334
369
|
const handler = protocols[protocol];
|
|
335
370
|
if (!handler) return value; // not a recognized protocol
|
|
336
371
|
|
|
337
|
-
const rest = value.substring(protoEnd + 3);
|
|
338
|
-
|
|
339
|
-
const pathStart = rest.indexOf('?.');
|
|
340
|
-
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
341
|
-
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
342
|
-
|
|
343
372
|
const resolved = handler(key);
|
|
344
373
|
|
|
345
374
|
if (path) {
|
|
@@ -357,7 +386,7 @@ function getArray(
|
|
|
357
386
|
source: any,
|
|
358
387
|
aliasMap: Map<string, string>,
|
|
359
388
|
withMethods: Set<string> | undefined,
|
|
360
|
-
protocols:
|
|
389
|
+
protocols: SyncProtocolHandlers | undefined,
|
|
361
390
|
options?: GetValuesOptions,
|
|
362
391
|
substitutionMap?: Map<string, string>
|
|
363
392
|
): any[] {
|
|
@@ -368,12 +397,7 @@ function getArray(
|
|
|
368
397
|
} else if (Array.isArray(item)) {
|
|
369
398
|
result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
|
|
370
399
|
} else if (item && typeof item === 'object') {
|
|
371
|
-
|
|
372
|
-
if (proto === Object.prototype || proto === null) {
|
|
373
|
-
result.push(getValues(item, source, options));
|
|
374
|
-
} else {
|
|
375
|
-
result.push(item);
|
|
376
|
-
}
|
|
400
|
+
result.push(isPlainObject(item) ? getValues(item, source, options) : item);
|
|
377
401
|
} else {
|
|
378
402
|
result.push(item);
|
|
379
403
|
}
|
|
@@ -410,12 +434,7 @@ export function getValues(
|
|
|
410
434
|
} else if (Array.isArray(value)) {
|
|
411
435
|
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
|
|
412
436
|
} else if (typeof value === 'object' && value !== null) {
|
|
413
|
-
|
|
414
|
-
if (proto === Object.prototype || proto === null) {
|
|
415
|
-
result[key] = getValues(value, source, options);
|
|
416
|
-
} else {
|
|
417
|
-
result[key] = value;
|
|
418
|
-
}
|
|
437
|
+
result[key] = isPlainObject(value) ? getValues(value, source, options) : value;
|
|
419
438
|
} else {
|
|
420
439
|
result[key] = value;
|
|
421
440
|
}
|
package/resolve/resolveValues.js
CHANGED
|
@@ -4,30 +4,24 @@
|
|
|
4
4
|
* Thin async wrapper around getValues that adds support for async protocol handlers.
|
|
5
5
|
* For synchronous-only use cases, import getValues/getValue directly for better performance.
|
|
6
6
|
*
|
|
7
|
+
* The outer-grammar primitives (`hasProtocol`, `parseProtocolRef`, `isPlainObject`)
|
|
8
|
+
* live in getValues.js and are re-exported here for convenience.
|
|
9
|
+
*
|
|
7
10
|
* Re-exports ResolveValuesOptions for backward compatibility.
|
|
8
11
|
*/
|
|
9
|
-
import { getValue, getValues } from './getValues.js';
|
|
12
|
+
import { getValue, getValues, hasProtocol, isPlainObject, parseProtocolRef } from './getValues.js';
|
|
10
13
|
// Re-export getValue as resolveValue for backward compatibility
|
|
11
14
|
export { getValue as resolveValue };
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*/
|
|
15
|
-
function hasProtocol(value) {
|
|
16
|
-
return value.includes('://');
|
|
17
|
-
}
|
|
15
|
+
// Re-export the shared outer-grammar primitives (defined in getValues.js)
|
|
16
|
+
export { hasProtocol, parseProtocolRef, isPlainObject };
|
|
18
17
|
/**
|
|
19
18
|
* Resolves a protocol-prefixed value asynchronously.
|
|
20
19
|
*/
|
|
21
20
|
async function resolveProtocolValue(value, protocols, options) {
|
|
22
|
-
const
|
|
23
|
-
const protocol = value.substring(0, protoEnd);
|
|
21
|
+
const { protocol, key, path } = parseProtocolRef(value);
|
|
24
22
|
const handler = protocols[protocol];
|
|
25
23
|
if (!handler)
|
|
26
24
|
return value;
|
|
27
|
-
const rest = value.substring(protoEnd + 3);
|
|
28
|
-
const pathStart = rest.indexOf('?.');
|
|
29
|
-
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
30
|
-
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
31
25
|
const resolved = await handler(key);
|
|
32
26
|
if (path) {
|
|
33
27
|
return getValue(path, resolved, options);
|
|
@@ -52,8 +46,7 @@ async function resolveArray(arr, source, protocols, options) {
|
|
|
52
46
|
result.push(await resolveArray(item, source, protocols, options));
|
|
53
47
|
}
|
|
54
48
|
else if (item && typeof item === 'object') {
|
|
55
|
-
|
|
56
|
-
if (proto === Object.prototype || proto === null) {
|
|
49
|
+
if (isPlainObject(item)) {
|
|
57
50
|
result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
|
|
58
51
|
}
|
|
59
52
|
else {
|
|
@@ -93,8 +86,7 @@ export async function resolveValues(pattern, source, options) {
|
|
|
93
86
|
result[key] = await resolveArray(value, source, protocols, options);
|
|
94
87
|
}
|
|
95
88
|
else if (typeof value === 'object' && value !== null) {
|
|
96
|
-
|
|
97
|
-
if (proto === Object.prototype || proto === null) {
|
|
89
|
+
if (isPlainObject(value)) {
|
|
98
90
|
result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
|
|
99
91
|
}
|
|
100
92
|
else {
|
package/resolve/resolveValues.ts
CHANGED
|
@@ -1,126 +1,116 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* resolveValues.ts — Async value resolution for path strings.
|
|
3
|
-
*
|
|
4
|
-
* Thin async wrapper around getValues that adds support for async protocol handlers.
|
|
5
|
-
* For synchronous-only use cases, import getValues/getValue directly for better performance.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (typeof item === '
|
|
65
|
-
if (
|
|
66
|
-
result.push(await
|
|
67
|
-
} else {
|
|
68
|
-
result.push(
|
|
69
|
-
}
|
|
70
|
-
} else
|
|
71
|
-
result.push(
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
|
|
118
|
-
} else {
|
|
119
|
-
result[key] = value;
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
result[key] = value;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* resolveValues.ts — Async value resolution for path strings.
|
|
3
|
+
*
|
|
4
|
+
* Thin async wrapper around getValues that adds support for async protocol handlers.
|
|
5
|
+
* For synchronous-only use cases, import getValues/getValue directly for better performance.
|
|
6
|
+
*
|
|
7
|
+
* The outer-grammar primitives (`hasProtocol`, `parseProtocolRef`, `isPlainObject`)
|
|
8
|
+
* live in getValues.js and are re-exported here for convenience.
|
|
9
|
+
*
|
|
10
|
+
* Re-exports ResolveValuesOptions for backward compatibility.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getValue, getValues, hasProtocol, isPlainObject, parseProtocolRef } from './getValues.js';
|
|
14
|
+
import type { ProtocolHandlers, ResolveValuesOptions } from '../types/assign-gingerly/types.js';
|
|
15
|
+
|
|
16
|
+
export type { ResolveValuesOptions };
|
|
17
|
+
|
|
18
|
+
// Re-export getValue as resolveValue for backward compatibility
|
|
19
|
+
export { getValue as resolveValue };
|
|
20
|
+
|
|
21
|
+
// Re-export the shared outer-grammar primitives (defined in getValues.js)
|
|
22
|
+
export { hasProtocol, parseProtocolRef, isPlainObject };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a protocol-prefixed value asynchronously.
|
|
26
|
+
*/
|
|
27
|
+
async function resolveProtocolValue(
|
|
28
|
+
value: string,
|
|
29
|
+
protocols: ProtocolHandlers,
|
|
30
|
+
options?: ResolveValuesOptions
|
|
31
|
+
): Promise<any> {
|
|
32
|
+
const { protocol, key, path } = parseProtocolRef(value);
|
|
33
|
+
|
|
34
|
+
const handler = protocols[protocol];
|
|
35
|
+
if (!handler) return value;
|
|
36
|
+
|
|
37
|
+
const resolved = await handler(key);
|
|
38
|
+
|
|
39
|
+
if (path) {
|
|
40
|
+
return getValue(path, resolved, options);
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve path strings and protocol references within an array (async).
|
|
47
|
+
*/
|
|
48
|
+
async function resolveArray(
|
|
49
|
+
arr: any[],
|
|
50
|
+
source: any,
|
|
51
|
+
protocols: ProtocolHandlers | undefined,
|
|
52
|
+
options?: ResolveValuesOptions
|
|
53
|
+
): Promise<any[]> {
|
|
54
|
+
const result: any[] = [];
|
|
55
|
+
for (const item of arr) {
|
|
56
|
+
if (typeof item === 'string') {
|
|
57
|
+
if (protocols && hasProtocol(item)) {
|
|
58
|
+
result.push(await resolveProtocolValue(item, protocols, options));
|
|
59
|
+
} else {
|
|
60
|
+
result.push(getValue(item, source, options));
|
|
61
|
+
}
|
|
62
|
+
} else if (Array.isArray(item)) {
|
|
63
|
+
result.push(await resolveArray(item, source, protocols, options));
|
|
64
|
+
} else if (item && typeof item === 'object') {
|
|
65
|
+
if (isPlainObject(item)) {
|
|
66
|
+
result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
|
|
67
|
+
} else {
|
|
68
|
+
result.push(item);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
result.push(item);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Async resolve RHS path strings in a pattern object against a source object.
|
|
79
|
+
*
|
|
80
|
+
* Supports async protocol handlers (e.g., fetch, IndexedDB).
|
|
81
|
+
* For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
|
|
82
|
+
*
|
|
83
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
84
|
+
* @param source - Object to resolve paths against
|
|
85
|
+
* @param options - Optional withMethods, aka, and protocol handlers
|
|
86
|
+
* @returns New object with path strings replaced by resolved values
|
|
87
|
+
*/
|
|
88
|
+
export async function resolveValues(
|
|
89
|
+
pattern: Record<string, any>,
|
|
90
|
+
source: any,
|
|
91
|
+
options?: ResolveValuesOptions
|
|
92
|
+
): Promise<Record<string, any>> {
|
|
93
|
+
const protocols = options?.protocols;
|
|
94
|
+
|
|
95
|
+
const result: Record<string, any> = {};
|
|
96
|
+
for (const [key, value] of Object.entries(pattern)) {
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
if (protocols && hasProtocol(value)) {
|
|
99
|
+
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
100
|
+
} else {
|
|
101
|
+
result[key] = getValue(value, source, options);
|
|
102
|
+
}
|
|
103
|
+
} else if (Array.isArray(value)) {
|
|
104
|
+
result[key] = await resolveArray(value, source, protocols, options);
|
|
105
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
106
|
+
if (isPlainObject(value)) {
|
|
107
|
+
result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
|
|
108
|
+
} else {
|
|
109
|
+
result[key] = value;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
result[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
@@ -406,7 +406,7 @@ export interface AssignFromOptions {
|
|
|
406
406
|
from: any;
|
|
407
407
|
|
|
408
408
|
/** Protocol handlers (sync or async) */
|
|
409
|
-
protocols?:
|
|
409
|
+
protocols?: ProtocolHandlers;
|
|
410
410
|
|
|
411
411
|
/** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
|
|
412
412
|
withMethods?: string[] | Set<string>;
|
|
@@ -480,6 +480,37 @@ export interface AssignFromOptions {
|
|
|
480
480
|
[key: string]: any;
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
/**
|
|
484
|
+
* A protocol handler resolves the key portion of a protocol-prefixed value
|
|
485
|
+
* (the text between `://` and the first `?.`) to a value. May be sync or async.
|
|
486
|
+
*/
|
|
487
|
+
export type ProtocolHandler = (key: string) => any | Promise<any>;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* A synchronous-only protocol handler, as accepted by getValues / getValue /
|
|
491
|
+
* assignFrom. For handlers that may return a Promise, use ProtocolHandler.
|
|
492
|
+
*/
|
|
493
|
+
export type SyncProtocolHandler = (key: string) => any;
|
|
494
|
+
|
|
495
|
+
/** Map of protocol name (the text before `://`) to a handler (sync or async). */
|
|
496
|
+
export type ProtocolHandlers = Record<string, ProtocolHandler>;
|
|
497
|
+
|
|
498
|
+
/** Map of protocol name to a synchronous handler. */
|
|
499
|
+
export type SyncProtocolHandlers = Record<string, SyncProtocolHandler>;
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The outer grammar of a protocol-prefixed value string,
|
|
503
|
+
* `‹protocol›://‹key›?.‹path›`, as produced by `parseProtocolRef`.
|
|
504
|
+
*/
|
|
505
|
+
export interface ParsedProtocolRef {
|
|
506
|
+
/** Text before `://`; `''` when the value contains no `://`. */
|
|
507
|
+
protocol: string;
|
|
508
|
+
/** Text between `://` and the first `?.` (or the end of the string). */
|
|
509
|
+
key: string;
|
|
510
|
+
/** Text from the first `?.` onward (always starts with `?.`), or `null`. */
|
|
511
|
+
path: string | null;
|
|
512
|
+
}
|
|
513
|
+
|
|
483
514
|
/**
|
|
484
515
|
* Options for synchronous value resolution (getValues / getValue).
|
|
485
516
|
* Extends IAssignGingerlyOptions with synchronous protocol handlers.
|
|
@@ -507,7 +538,7 @@ export interface GetValuesOptions extends IAssignGingerlyOptions {
|
|
|
507
538
|
* localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
|
|
508
539
|
* }
|
|
509
540
|
*/
|
|
510
|
-
protocols?:
|
|
541
|
+
protocols?: SyncProtocolHandlers;
|
|
511
542
|
}
|
|
512
543
|
|
|
513
544
|
/**
|
|
@@ -530,7 +561,7 @@ export interface ResolveValuesOptions extends IAssignGingerlyOptions {
|
|
|
530
561
|
* Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
|
|
531
562
|
* Each handler receives the key portion and returns the resolved value (sync or async).
|
|
532
563
|
*/
|
|
533
|
-
protocols?:
|
|
564
|
+
protocols?: ProtocolHandlers;
|
|
534
565
|
}
|
|
535
566
|
|
|
536
567
|
/**
|