@memberjunction/search-engine 5.49.0 → 5.51.0
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/dist/generic/EntitySearchProvider.d.ts +17 -3
- package/dist/generic/EntitySearchProvider.d.ts.map +1 -1
- package/dist/generic/EntitySearchProvider.js +23 -6
- package/dist/generic/EntitySearchProvider.js.map +1 -1
- package/dist/generic/ExternalHitMapper.d.ts +48 -0
- package/dist/generic/ExternalHitMapper.d.ts.map +1 -0
- package/dist/generic/ExternalHitMapper.js +88 -0
- package/dist/generic/ExternalHitMapper.js.map +1 -0
- package/dist/generic/FullTextSearchProvider.d.ts +9 -1
- package/dist/generic/FullTextSearchProvider.d.ts.map +1 -1
- package/dist/generic/FullTextSearchProvider.js +11 -3
- package/dist/generic/FullTextSearchProvider.js.map +1 -1
- package/dist/generic/ScopeDimensionResolver.d.ts +146 -0
- package/dist/generic/ScopeDimensionResolver.d.ts.map +1 -0
- package/dist/generic/ScopeDimensionResolver.js +464 -0
- package/dist/generic/ScopeDimensionResolver.js.map +1 -0
- package/dist/generic/ScopeExplanation.d.ts +141 -0
- package/dist/generic/ScopeExplanation.d.ts.map +1 -0
- package/dist/generic/ScopeExplanation.js +72 -0
- package/dist/generic/ScopeExplanation.js.map +1 -0
- package/dist/generic/ScopeFilterGuard.d.ts +127 -0
- package/dist/generic/ScopeFilterGuard.d.ts.map +1 -0
- package/dist/generic/ScopeFilterGuard.js +290 -0
- package/dist/generic/ScopeFilterGuard.js.map +1 -0
- package/dist/generic/ScopeTemplateRenderer.d.ts +12 -2
- package/dist/generic/ScopeTemplateRenderer.d.ts.map +1 -1
- package/dist/generic/ScopeTemplateRenderer.js +25 -5
- package/dist/generic/ScopeTemplateRenderer.js.map +1 -1
- package/dist/generic/ScopeValueEscaper.d.ts +112 -0
- package/dist/generic/ScopeValueEscaper.d.ts.map +1 -0
- package/dist/generic/ScopeValueEscaper.js +152 -0
- package/dist/generic/ScopeValueEscaper.js.map +1 -0
- package/dist/generic/SearchEngine.d.ts +188 -8
- package/dist/generic/SearchEngine.d.ts.map +1 -1
- package/dist/generic/SearchEngine.js +600 -35
- package/dist/generic/SearchEngine.js.map +1 -1
- package/dist/generic/VectorSearchProvider.d.ts +2 -1
- package/dist/generic/VectorSearchProvider.d.ts.map +1 -1
- package/dist/generic/VectorSearchProvider.js +23 -26
- package/dist/generic/VectorSearchProvider.js.map +1 -1
- package/dist/generic/search.types.d.ts +160 -0
- package/dist/generic/search.types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/permissions/SearchScopePermissionResolver.d.ts +37 -2
- package/dist/permissions/SearchScopePermissionResolver.d.ts.map +1 -1
- package/dist/permissions/SearchScopePermissionResolver.js +74 -2
- package/dist/permissions/SearchScopePermissionResolver.js.map +1 -1
- package/dist/providers/AzureAISearchProvider.d.ts.map +1 -1
- package/dist/providers/AzureAISearchProvider.js +24 -6
- package/dist/providers/AzureAISearchProvider.js.map +1 -1
- package/dist/providers/ElasticsearchSearchProvider.d.ts.map +1 -1
- package/dist/providers/ElasticsearchSearchProvider.js +17 -5
- package/dist/providers/ElasticsearchSearchProvider.js.map +1 -1
- package/dist/providers/OpenSearchSearchProvider.d.ts.map +1 -1
- package/dist/providers/OpenSearchSearchProvider.js +16 -5
- package/dist/providers/OpenSearchSearchProvider.js.map +1 -1
- package/dist/providers/TypesenseSearchProvider.d.ts.map +1 -1
- package/dist/providers/TypesenseSearchProvider.js +17 -6
- package/dist/providers/TypesenseSearchProvider.js.map +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The shape of a scope decision — what a search WOULD be able to reach, and why.
|
|
3
|
+
*
|
|
4
|
+
* Everything the dimensional-scope work added is a decision made from inputs that are gone by
|
|
5
|
+
* the time anyone asks about them. A grant applied because a time window happened to be open;
|
|
6
|
+
* a dimension was discarded because it was caller-authored on a ServerDerived key; a lane was
|
|
7
|
+
* skipped because its filter lost a clause. Each of those is invisible afterwards — the result
|
|
8
|
+
* set looks the same whether it was correctly bounded or accidentally widened, so neither an
|
|
9
|
+
* administrator configuring a scope nor an auditor reconstructing an incident can tell.
|
|
10
|
+
*
|
|
11
|
+
* `ScopeExplanation` is that decision, captured. It is used in two places deliberately:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Before** a search — `SearchEngine.ExplainScope()` runs the entire resolution chain
|
|
14
|
+
* (entitlement → dimensions → lane filters) and returns this WITHOUT querying anything.
|
|
15
|
+
* "As this user, on this date, with this skill active: what could this reach?"
|
|
16
|
+
* 2. **After** a search — the same object is serialized into
|
|
17
|
+
* `SearchExecutionLog.ScopeDecisionJSON`.
|
|
18
|
+
*
|
|
19
|
+
* One shape for both is the point. The preview an administrator approves is structurally the
|
|
20
|
+
* record the audit log keeps, so a claim made at configuration time is checkable against what
|
|
21
|
+
* actually ran, rather than being two representations that can quietly disagree.
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/search-engine
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Render an explanation as human-readable lines.
|
|
27
|
+
*
|
|
28
|
+
* Kept as a pure function next to the type rather than inside the engine so a CLI, an
|
|
29
|
+
* Explorer panel, or a test can format one without constructing a `SearchEngine`.
|
|
30
|
+
*/
|
|
31
|
+
export function SummarizeExplanation(explanation) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
lines.push(`Scope: ${explanation.ScopeName} (${explanation.ScopeID})`);
|
|
34
|
+
const ent = explanation.Entitlement;
|
|
35
|
+
lines.push(` Reachable: ${explanation.Reachable ? 'YES' : 'NO'} — ` +
|
|
36
|
+
(ent
|
|
37
|
+
? `entitlement ${ent.Allowed ? 'granted' : 'DENIED'} at ${ent.Level} via ${ent.Source}`
|
|
38
|
+
: 'entitlement not evaluated at this layer'));
|
|
39
|
+
if (ent)
|
|
40
|
+
lines.push(` Why: ${ent.Reason}`);
|
|
41
|
+
if (explanation.Dimensions.length) {
|
|
42
|
+
lines.push(' Dimensions:');
|
|
43
|
+
for (const d of explanation.Dimensions) {
|
|
44
|
+
const bound = d.Restricts ? ' [BOUND]' : '';
|
|
45
|
+
const note = d.Note ? ` — ${d.Note}` : '';
|
|
46
|
+
lines.push(` ${d.Name}${bound} = ${JSON.stringify(d.Value)} (${d.Provenance})${note}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push(' Dimensions: none declared (legacy scope — the caller context passes through unchecked)');
|
|
51
|
+
}
|
|
52
|
+
lines.push(' Lanes:');
|
|
53
|
+
if (!explanation.Lanes.length) {
|
|
54
|
+
lines.push(' (NONE CONFIGURED — this scope is UNSCOPED: providers apply no filter)');
|
|
55
|
+
}
|
|
56
|
+
for (const lane of explanation.Lanes) {
|
|
57
|
+
lines.push(` [${lane.Status}] ${lane.Kind}: ${lane.Target}`);
|
|
58
|
+
if (lane.RequiredMetadataKeys?.length) {
|
|
59
|
+
lines.push(` requires: ${lane.RequiredMetadataKeys.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (lane.RenderedFilter) {
|
|
62
|
+
lines.push(` filter: ${lane.RenderedFilter.substring(0, 200)}`);
|
|
63
|
+
}
|
|
64
|
+
if (lane.Reason) {
|
|
65
|
+
lines.push(` SKIPPED: ${lane.Reason}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const d of explanation.Diagnostics)
|
|
69
|
+
lines.push(` note: ${d}`);
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=ScopeExplanation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScopeExplanation.js","sourceRoot":"","sources":["../../src/generic/ScopeExplanation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAqHH;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAA6B;IAC9D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,UAAU,WAAW,CAAC,SAAS,KAAK,WAAW,CAAC,OAAO,GAAG,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,WAAW,CAAC,WAAW,CAAC;IACpC,KAAK,CAAC,IAAI,CACN,gBAAgB,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK;QACzD,CAAC,GAAG;YACA,CAAC,CAAC,eAAe,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,KAAK,QAAQ,GAAG,CAAC,MAAM,EAAE;YACvF,CAAC,CAAC,yCAAyC,CAAC,CACnD,CAAC;IACF,IAAI,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAE5C,IAAI,WAAW,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,UAAU,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9F,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,KAAK,CAAC,IAAI,CAAC,0FAA0F,CAAC,CAAC;IAC3G,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvB,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;IAC5F,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,oBAAoB,EAAE,MAAM,EAAE,CAAC;YACpC,KAAK,CAAC,IAAI,CAAC,qBAAqB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAClD,CAAC;IACL,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Guard for a scope's rendered filter values.
|
|
3
|
+
*
|
|
4
|
+
* A scope's `MetadataFilter` is where tenant and permission push-down lives for every
|
|
5
|
+
* external-index lane. The providers previously applied it behind a bare type guard:
|
|
6
|
+
*
|
|
7
|
+
* ```typescript
|
|
8
|
+
* if (idx.MetadataFilter && typeof idx.MetadataFilter === 'string') { body.filter = idx.MetadataFilter; }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* That conflates two very different situations. If no filter was authored, running
|
|
12
|
+
* unfiltered is correct. But if a filter **was** authored and merely arrived in an
|
|
13
|
+
* unusable shape — a template that failed to render, a JSON filter that didn't parse,
|
|
14
|
+
* an object where the lane needs a string — the guard fell through and the lane ran
|
|
15
|
+
* **completely unfiltered**, silently dropping the tenant predicate. Breaking the
|
|
16
|
+
* filter was therefore the cheapest way to defeat it.
|
|
17
|
+
*
|
|
18
|
+
* This module makes the distinction explicit so a lane can fail closed:
|
|
19
|
+
*
|
|
20
|
+
* - `absent` — nothing authored; run unfiltered (unchanged behaviour)
|
|
21
|
+
* - `usable` — apply it
|
|
22
|
+
* - `unusable` — a filter was authored but cannot be applied; the caller MUST skip
|
|
23
|
+
* the lane rather than query without it
|
|
24
|
+
*
|
|
25
|
+
* @module @memberjunction/search-engine
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Outcome of checking a scope's rendered filter value for a particular lane.
|
|
29
|
+
*
|
|
30
|
+
* `unusable` is the case that matters: it means an author intended a restriction that
|
|
31
|
+
* the lane cannot express, so proceeding would silently widen the search.
|
|
32
|
+
*/
|
|
33
|
+
export type ScopeFilterCheck<T> = {
|
|
34
|
+
Status: 'absent';
|
|
35
|
+
} | {
|
|
36
|
+
Status: 'usable';
|
|
37
|
+
Value: T;
|
|
38
|
+
} | {
|
|
39
|
+
Status: 'unusable';
|
|
40
|
+
Reason: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Check a filter for a lane that needs a **string** (Azure AI Search OData `$filter`,
|
|
44
|
+
* Typesense `filter_by`).
|
|
45
|
+
*
|
|
46
|
+
* An object here means the scope's template rendered JSON for a lane that cannot consume
|
|
47
|
+
* it — authored intent that cannot be applied, hence `unusable` rather than ignored.
|
|
48
|
+
*/
|
|
49
|
+
export declare function CheckScopeStringFilter(raw: unknown): ScopeFilterCheck<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Check a filter for a lane that needs a structured **object** (Elasticsearch /
|
|
52
|
+
* OpenSearch filter DSL).
|
|
53
|
+
*
|
|
54
|
+
* A leftover string means the rendered template never parsed as JSON — usually a render
|
|
55
|
+
* failure or malformed JSON in the scope definition.
|
|
56
|
+
*/
|
|
57
|
+
export declare function CheckScopeObjectFilter(raw: unknown): ScopeFilterCheck<object>;
|
|
58
|
+
/**
|
|
59
|
+
* Check a **rendered** scope template against its source, for fields that RESTRICT
|
|
60
|
+
* (`ExtraFilter`, `MetadataFilter`, `ExternalIndexConfig`).
|
|
61
|
+
*
|
|
62
|
+
* This catches the two ways a restriction can evaporate during rendering, neither of which
|
|
63
|
+
* the value alone can reveal — by the time a provider sees the constraint, the source
|
|
64
|
+
* template has been discarded, so "authored but rendered to nothing" is indistinguishable
|
|
65
|
+
* from "never authored":
|
|
66
|
+
*
|
|
67
|
+
* 1. **Rendered empty.** `RenderScopeTemplate` runs Nunjucks with `throwOnUndefined: false`,
|
|
68
|
+
* so a mistyped dimension (`SecondaryScopes.EffectiveChanneID`) makes a `{% if %}` guard
|
|
69
|
+
* false and the entire restricting clause silently disappears — the lane then runs
|
|
70
|
+
* unrestricted. (We deliberately do NOT flip `throwOnUndefined`: legitimate templates
|
|
71
|
+
* output optional dimensions, and flipping it would break them all.)
|
|
72
|
+
* 2. **Template leaked through.** On a render error `RenderScopeTemplate` returns the RAW
|
|
73
|
+
* template, so `{% if %}`/`{{ }}` syntax reaches a filter as literal text.
|
|
74
|
+
*
|
|
75
|
+
* @param source the un-rendered template from the scope row
|
|
76
|
+
* @param rendered the output of RenderScopeTemplate / RenderScopeJsonTemplate
|
|
77
|
+
*/
|
|
78
|
+
export declare function CheckRenderedTemplate(source: string | null | undefined, rendered: unknown): ScopeFilterCheck<unknown>;
|
|
79
|
+
/**
|
|
80
|
+
* Parse a `RequiredMetadataKeys` declaration into a list of key names.
|
|
81
|
+
*
|
|
82
|
+
* Accepts a JSON array (`["OrganizationID","ContentSourceID"]`) or a comma-separated
|
|
83
|
+
* string, because both shapes turn up in hand-authored metadata. Returns an empty array
|
|
84
|
+
* when nothing is declared.
|
|
85
|
+
*
|
|
86
|
+
* @throws {Error} when a value IS declared but cannot be parsed. A malformed contract must
|
|
87
|
+
* not silently degrade to "no contract" — that would turn a typo into an unguarded lane,
|
|
88
|
+
* which is the exact failure this feature exists to prevent.
|
|
89
|
+
*/
|
|
90
|
+
export declare function ParseRequiredMetadataKeys(raw: string | null | undefined): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Check that a **rendered** metadata filter actually constrains on every key its scope
|
|
93
|
+
* declared in `RequiredMetadataKeys`.
|
|
94
|
+
*
|
|
95
|
+
* This catches the failure mode {@link CheckRenderedTemplate} structurally cannot: a filter
|
|
96
|
+
* that rendered **partially**. A realistic scope filter is several optional clauses —
|
|
97
|
+
*
|
|
98
|
+
* ```
|
|
99
|
+
* {"OrganizationID": "{{ ctx.PrimaryScopeRecordID }}"
|
|
100
|
+
* {% if ctx.SecondaryScopes.EffectiveChannelID %}, "ContentSourceID": {...}{% endif %}}
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* — and if the channel dimension is absent (a mistyped dimension name, a caller that omitted
|
|
104
|
+
* it, a value the dimension resolver discarded as spoofed) the org clause still renders. The
|
|
105
|
+
* filter is non-empty, contains no leftover template syntax, and passes every other guard,
|
|
106
|
+
* yet the lane now searches the entire tenant instead of the one channel. The restriction
|
|
107
|
+
* did not fail; it evaporated, and nothing downstream can tell the difference.
|
|
108
|
+
*
|
|
109
|
+
* Declaring the keys makes the author's intent checkable. The check is deliberately a
|
|
110
|
+
* **presence** test rather than a semantic one: proving that a filter genuinely restricts on
|
|
111
|
+
* a key would mean interpreting five different provider filter dialects. Presence is cheap,
|
|
112
|
+
* dialect-agnostic, and catches the whole vanished-clause class, which is what actually
|
|
113
|
+
* happens in practice.
|
|
114
|
+
*
|
|
115
|
+
* @param rendered the rendered filter — a JSON string, or an already-parsed object
|
|
116
|
+
* @param requiredKeys key names from `ParseRequiredMetadataKeys`
|
|
117
|
+
*/
|
|
118
|
+
export declare function CheckRequiredMetadataKeys(rendered: unknown, requiredKeys: string[]): ScopeFilterCheck<unknown>;
|
|
119
|
+
/**
|
|
120
|
+
* Check a filter for a lane that needs **JSON** and will accept either an already-parsed
|
|
121
|
+
* object or a JSON string it can parse itself (vector metadata filters).
|
|
122
|
+
*
|
|
123
|
+
* A string that fails to parse is `unusable`. This is the specific path that previously
|
|
124
|
+
* dropped the whole filter — including the tenant clause — and queried the entire index.
|
|
125
|
+
*/
|
|
126
|
+
export declare function CheckScopeJsonFilter(raw: unknown): ScopeFilterCheck<object>;
|
|
127
|
+
//# sourceMappingURL=ScopeFilterGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScopeFilterGuard.d.ts","sourceRoot":"","sources":["../../src/generic/ScopeFilterGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,IACxB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC9B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAQ7C;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAO7E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAU7E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAkBrH;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAsClF;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAiB9G;AAyDD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAoB3E"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Guard for a scope's rendered filter values.
|
|
3
|
+
*
|
|
4
|
+
* A scope's `MetadataFilter` is where tenant and permission push-down lives for every
|
|
5
|
+
* external-index lane. The providers previously applied it behind a bare type guard:
|
|
6
|
+
*
|
|
7
|
+
* ```typescript
|
|
8
|
+
* if (idx.MetadataFilter && typeof idx.MetadataFilter === 'string') { body.filter = idx.MetadataFilter; }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* That conflates two very different situations. If no filter was authored, running
|
|
12
|
+
* unfiltered is correct. But if a filter **was** authored and merely arrived in an
|
|
13
|
+
* unusable shape — a template that failed to render, a JSON filter that didn't parse,
|
|
14
|
+
* an object where the lane needs a string — the guard fell through and the lane ran
|
|
15
|
+
* **completely unfiltered**, silently dropping the tenant predicate. Breaking the
|
|
16
|
+
* filter was therefore the cheapest way to defeat it.
|
|
17
|
+
*
|
|
18
|
+
* This module makes the distinction explicit so a lane can fail closed:
|
|
19
|
+
*
|
|
20
|
+
* - `absent` — nothing authored; run unfiltered (unchanged behaviour)
|
|
21
|
+
* - `usable` — apply it
|
|
22
|
+
* - `unusable` — a filter was authored but cannot be applied; the caller MUST skip
|
|
23
|
+
* the lane rather than query without it
|
|
24
|
+
*
|
|
25
|
+
* @module @memberjunction/search-engine
|
|
26
|
+
*/
|
|
27
|
+
/** True when a value carries no authored filter at all (null/undefined/blank string). */
|
|
28
|
+
function isAbsent(raw) {
|
|
29
|
+
if (raw === null || raw === undefined)
|
|
30
|
+
return true;
|
|
31
|
+
return typeof raw === 'string' && raw.trim().length === 0;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check a filter for a lane that needs a **string** (Azure AI Search OData `$filter`,
|
|
35
|
+
* Typesense `filter_by`).
|
|
36
|
+
*
|
|
37
|
+
* An object here means the scope's template rendered JSON for a lane that cannot consume
|
|
38
|
+
* it — authored intent that cannot be applied, hence `unusable` rather than ignored.
|
|
39
|
+
*/
|
|
40
|
+
export function CheckScopeStringFilter(raw) {
|
|
41
|
+
if (isAbsent(raw))
|
|
42
|
+
return { Status: 'absent' };
|
|
43
|
+
if (typeof raw === 'string')
|
|
44
|
+
return { Status: 'usable', Value: raw };
|
|
45
|
+
return {
|
|
46
|
+
Status: 'unusable',
|
|
47
|
+
Reason: `expected a string filter for this lane but got ${typeof raw}; the scope's MetadataFilter likely rendered JSON for a lane that requires a string`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check a filter for a lane that needs a structured **object** (Elasticsearch /
|
|
52
|
+
* OpenSearch filter DSL).
|
|
53
|
+
*
|
|
54
|
+
* A leftover string means the rendered template never parsed as JSON — usually a render
|
|
55
|
+
* failure or malformed JSON in the scope definition.
|
|
56
|
+
*/
|
|
57
|
+
export function CheckScopeObjectFilter(raw) {
|
|
58
|
+
if (isAbsent(raw))
|
|
59
|
+
return { Status: 'absent' };
|
|
60
|
+
if (typeof raw === 'object')
|
|
61
|
+
return { Status: 'usable', Value: raw };
|
|
62
|
+
if (typeof raw === 'string') {
|
|
63
|
+
return {
|
|
64
|
+
Status: 'unusable',
|
|
65
|
+
Reason: `expected a structured filter object but got a string; the scope's MetadataFilter did not render to valid JSON: ${raw.trim().substring(0, 120)}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { Status: 'unusable', Reason: `expected a structured filter object but got ${typeof raw}` };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check a **rendered** scope template against its source, for fields that RESTRICT
|
|
72
|
+
* (`ExtraFilter`, `MetadataFilter`, `ExternalIndexConfig`).
|
|
73
|
+
*
|
|
74
|
+
* This catches the two ways a restriction can evaporate during rendering, neither of which
|
|
75
|
+
* the value alone can reveal — by the time a provider sees the constraint, the source
|
|
76
|
+
* template has been discarded, so "authored but rendered to nothing" is indistinguishable
|
|
77
|
+
* from "never authored":
|
|
78
|
+
*
|
|
79
|
+
* 1. **Rendered empty.** `RenderScopeTemplate` runs Nunjucks with `throwOnUndefined: false`,
|
|
80
|
+
* so a mistyped dimension (`SecondaryScopes.EffectiveChanneID`) makes a `{% if %}` guard
|
|
81
|
+
* false and the entire restricting clause silently disappears — the lane then runs
|
|
82
|
+
* unrestricted. (We deliberately do NOT flip `throwOnUndefined`: legitimate templates
|
|
83
|
+
* output optional dimensions, and flipping it would break them all.)
|
|
84
|
+
* 2. **Template leaked through.** On a render error `RenderScopeTemplate` returns the RAW
|
|
85
|
+
* template, so `{% if %}`/`{{ }}` syntax reaches a filter as literal text.
|
|
86
|
+
*
|
|
87
|
+
* @param source the un-rendered template from the scope row
|
|
88
|
+
* @param rendered the output of RenderScopeTemplate / RenderScopeJsonTemplate
|
|
89
|
+
*/
|
|
90
|
+
export function CheckRenderedTemplate(source, rendered) {
|
|
91
|
+
if (isAbsent(source))
|
|
92
|
+
return { Status: 'absent' };
|
|
93
|
+
const renderedIsEmpty = rendered === null || rendered === undefined
|
|
94
|
+
|| (typeof rendered === 'string' && rendered.trim().length === 0);
|
|
95
|
+
if (renderedIsEmpty) {
|
|
96
|
+
return {
|
|
97
|
+
Status: 'unusable',
|
|
98
|
+
Reason: `a restricting template was authored but rendered to nothing, so the restriction would vanish — usually an undefined dimension in a {% if %} guard (Nunjucks runs with throwOnUndefined:false). Template: ${String(source).substring(0, 160)}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (typeof rendered === 'string' && (rendered.includes('{{') || rendered.includes('{%'))) {
|
|
102
|
+
return {
|
|
103
|
+
Status: 'unusable',
|
|
104
|
+
Reason: `the template did not render — raw template syntax survived into the value, which means RenderScopeTemplate hit an error and returned the source verbatim: ${rendered.substring(0, 160)}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { Status: 'usable', Value: rendered };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parse a `RequiredMetadataKeys` declaration into a list of key names.
|
|
111
|
+
*
|
|
112
|
+
* Accepts a JSON array (`["OrganizationID","ContentSourceID"]`) or a comma-separated
|
|
113
|
+
* string, because both shapes turn up in hand-authored metadata. Returns an empty array
|
|
114
|
+
* when nothing is declared.
|
|
115
|
+
*
|
|
116
|
+
* @throws {Error} when a value IS declared but cannot be parsed. A malformed contract must
|
|
117
|
+
* not silently degrade to "no contract" — that would turn a typo into an unguarded lane,
|
|
118
|
+
* which is the exact failure this feature exists to prevent.
|
|
119
|
+
*/
|
|
120
|
+
export function ParseRequiredMetadataKeys(raw) {
|
|
121
|
+
if (isAbsent(raw))
|
|
122
|
+
return [];
|
|
123
|
+
const trimmed = String(raw).trim();
|
|
124
|
+
if (trimmed.startsWith('{')) {
|
|
125
|
+
// A JSON object is a shape the author clearly meant as structured data. Falling through
|
|
126
|
+
// to the comma-split branch would turn the entire literal into one nonsense "key name"
|
|
127
|
+
// that no filter can ever satisfy, permanently skipping the lane with a reason nobody
|
|
128
|
+
// can act on. Reject it as the malformed declaration it is.
|
|
129
|
+
throw new Error(`RequiredMetadataKeys must be a JSON array or a comma-separated list of key names, but got a JSON object: ${trimmed.substring(0, 120)}`);
|
|
130
|
+
}
|
|
131
|
+
let keys;
|
|
132
|
+
if (trimmed.startsWith('[')) {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(trimmed);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error(`RequiredMetadataKeys is not valid JSON: ${trimmed.substring(0, 120)}`);
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(parsed))
|
|
141
|
+
throw new Error(`RequiredMetadataKeys must be a JSON array, got ${typeof parsed}`);
|
|
142
|
+
keys = parsed.map((k) => String(k).trim());
|
|
143
|
+
if (keys.some((k) => k.length === 0))
|
|
144
|
+
throw new Error('RequiredMetadataKeys contains a blank key name');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
keys = trimmed.split(',').map((k) => k.trim()).filter((k) => k.length > 0);
|
|
148
|
+
}
|
|
149
|
+
// Metadata keys are identifiers. Anything else is a typo — an unbalanced bracket, a stray
|
|
150
|
+
// quote — and a typo'd key silently disables the lane forever, since no rendered filter can
|
|
151
|
+
// contain it. Naming the bad token is far more useful than a lane that never runs.
|
|
152
|
+
const malformed = keys.filter((k) => !VALID_METADATA_KEY_RE.test(k));
|
|
153
|
+
if (malformed.length) {
|
|
154
|
+
throw new Error(`RequiredMetadataKeys contains value(s) that are not valid metadata key names: ${malformed.map((k) => JSON.stringify(k)).join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
return keys;
|
|
157
|
+
}
|
|
158
|
+
/** Metadata key names are identifiers; dots and dashes are allowed for nested/hyphenated labels. */
|
|
159
|
+
const VALID_METADATA_KEY_RE = /^[A-Za-z_][A-Za-z0-9_.\-]*$/;
|
|
160
|
+
/**
|
|
161
|
+
* Check that a **rendered** metadata filter actually constrains on every key its scope
|
|
162
|
+
* declared in `RequiredMetadataKeys`.
|
|
163
|
+
*
|
|
164
|
+
* This catches the failure mode {@link CheckRenderedTemplate} structurally cannot: a filter
|
|
165
|
+
* that rendered **partially**. A realistic scope filter is several optional clauses —
|
|
166
|
+
*
|
|
167
|
+
* ```
|
|
168
|
+
* {"OrganizationID": "{{ ctx.PrimaryScopeRecordID }}"
|
|
169
|
+
* {% if ctx.SecondaryScopes.EffectiveChannelID %}, "ContentSourceID": {...}{% endif %}}
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* — and if the channel dimension is absent (a mistyped dimension name, a caller that omitted
|
|
173
|
+
* it, a value the dimension resolver discarded as spoofed) the org clause still renders. The
|
|
174
|
+
* filter is non-empty, contains no leftover template syntax, and passes every other guard,
|
|
175
|
+
* yet the lane now searches the entire tenant instead of the one channel. The restriction
|
|
176
|
+
* did not fail; it evaporated, and nothing downstream can tell the difference.
|
|
177
|
+
*
|
|
178
|
+
* Declaring the keys makes the author's intent checkable. The check is deliberately a
|
|
179
|
+
* **presence** test rather than a semantic one: proving that a filter genuinely restricts on
|
|
180
|
+
* a key would mean interpreting five different provider filter dialects. Presence is cheap,
|
|
181
|
+
* dialect-agnostic, and catches the whole vanished-clause class, which is what actually
|
|
182
|
+
* happens in practice.
|
|
183
|
+
*
|
|
184
|
+
* @param rendered the rendered filter — a JSON string, or an already-parsed object
|
|
185
|
+
* @param requiredKeys key names from `ParseRequiredMetadataKeys`
|
|
186
|
+
*/
|
|
187
|
+
export function CheckRequiredMetadataKeys(rendered, requiredKeys) {
|
|
188
|
+
if (requiredKeys.length === 0)
|
|
189
|
+
return { Status: 'usable', Value: rendered };
|
|
190
|
+
if (isAbsent(rendered)) {
|
|
191
|
+
return {
|
|
192
|
+
Status: 'unusable',
|
|
193
|
+
Reason: `this lane declares RequiredMetadataKeys [${requiredKeys.join(', ')}] but no filter was rendered at all, so none of them constrain the search`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const present = collectFilterKeys(rendered);
|
|
197
|
+
const missing = requiredKeys.filter((k) => !present.has(k.toLowerCase()));
|
|
198
|
+
if (missing.length === 0)
|
|
199
|
+
return { Status: 'usable', Value: rendered };
|
|
200
|
+
return {
|
|
201
|
+
Status: 'unusable',
|
|
202
|
+
Reason: `the rendered filter does not constrain on required metadata key${missing.length > 1 ? 's' : ''} [${missing.join(', ')}] — the clause was almost certainly dropped because a dimension it depends on was absent or discarded, which would widen this lane. Rendered: ${renderedPreview(rendered)}`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Collect every key name a rendered filter mentions, lowercased.
|
|
207
|
+
*
|
|
208
|
+
* For an object (or a JSON string that parses to one) this walks the structure and gathers
|
|
209
|
+
* property names at every depth, so a key nested inside a provider's boolean wrapper
|
|
210
|
+
* (`{"$and":[{"OrganizationID":…}]}`) still counts. For a non-JSON string — Azure OData,
|
|
211
|
+
* Typesense `filter_by` — it falls back to identifier tokens, since those dialects express
|
|
212
|
+
* a constraint as `Key eq 'value'` rather than as structure.
|
|
213
|
+
*/
|
|
214
|
+
function collectFilterKeys(rendered) {
|
|
215
|
+
const keys = new Set();
|
|
216
|
+
const walk = (node) => {
|
|
217
|
+
if (Array.isArray(node)) {
|
|
218
|
+
for (const item of node)
|
|
219
|
+
walk(item);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (node !== null && typeof node === 'object') {
|
|
223
|
+
for (const [key, value] of Object.entries(node)) {
|
|
224
|
+
keys.add(key.toLowerCase());
|
|
225
|
+
walk(value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
if (typeof rendered === 'string') {
|
|
230
|
+
const trimmed = rendered.trim();
|
|
231
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
232
|
+
try {
|
|
233
|
+
walk(JSON.parse(trimmed));
|
|
234
|
+
return keys;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Not JSON after all — fall through to token scanning.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Strip quoted literals BEFORE tokenizing. Without this, a filter that merely mentions
|
|
241
|
+
// the key name as a VALUE — `Title eq 'ContentSourceID'` — satisfies a contract it does
|
|
242
|
+
// not actually constrain on, which is a fail-open. The structured path never had this
|
|
243
|
+
// problem because a JSON walk distinguishes keys from values by position; a flat string
|
|
244
|
+
// has to earn that distinction by removing the places values live.
|
|
245
|
+
const withoutLiterals = trimmed.replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
246
|
+
for (const token of withoutLiterals.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [])
|
|
247
|
+
keys.add(token.toLowerCase());
|
|
248
|
+
return keys;
|
|
249
|
+
}
|
|
250
|
+
walk(rendered);
|
|
251
|
+
return keys;
|
|
252
|
+
}
|
|
253
|
+
/** Short, safe preview of a rendered filter for an error message. */
|
|
254
|
+
function renderedPreview(rendered) {
|
|
255
|
+
const text = typeof rendered === 'string' ? rendered : JSON.stringify(rendered);
|
|
256
|
+
return (text ?? '').substring(0, 160);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Check a filter for a lane that needs **JSON** and will accept either an already-parsed
|
|
260
|
+
* object or a JSON string it can parse itself (vector metadata filters).
|
|
261
|
+
*
|
|
262
|
+
* A string that fails to parse is `unusable`. This is the specific path that previously
|
|
263
|
+
* dropped the whole filter — including the tenant clause — and queried the entire index.
|
|
264
|
+
*/
|
|
265
|
+
export function CheckScopeJsonFilter(raw) {
|
|
266
|
+
if (isAbsent(raw))
|
|
267
|
+
return { Status: 'absent' };
|
|
268
|
+
if (typeof raw === 'object')
|
|
269
|
+
return { Status: 'usable', Value: raw };
|
|
270
|
+
if (typeof raw === 'string') {
|
|
271
|
+
const trimmed = raw.trim();
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(trimmed);
|
|
274
|
+
if (parsed !== null && typeof parsed === 'object')
|
|
275
|
+
return { Status: 'usable', Value: parsed };
|
|
276
|
+
return {
|
|
277
|
+
Status: 'unusable',
|
|
278
|
+
Reason: `MetadataFilter parsed to a ${typeof parsed} rather than an object: ${trimmed.substring(0, 120)}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return {
|
|
283
|
+
Status: 'unusable',
|
|
284
|
+
Reason: `MetadataFilter is not valid JSON: ${trimmed.substring(0, 120)}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { Status: 'unusable', Reason: `MetadataFilter has unsupported type ${typeof raw}` };
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=ScopeFilterGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScopeFilterGuard.js","sourceRoot":"","sources":["../../src/generic/ScopeFilterGuard.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,13 +17,23 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @module @memberjunction/search-engine
|
|
19
19
|
*/
|
|
20
|
+
import { type ScopeLaneKind } from './ScopeValueEscaper.js';
|
|
20
21
|
import { SearchContext } from './search.types.js';
|
|
21
22
|
/**
|
|
22
23
|
* Render a scope template string with the supplied SearchContext. Returns the original
|
|
23
24
|
* string unchanged when `template` is null/empty. Returns the original string on render
|
|
24
25
|
* failure (logged via LogError) so a single bad template does not bring down a search.
|
|
25
26
|
*/
|
|
26
|
-
export declare function RenderScopeTemplate(template: string | null | undefined, context: SearchContext | undefined, extraData?: Record<string, unknown
|
|
27
|
+
export declare function RenderScopeTemplate(template: string | null | undefined, context: SearchContext | undefined, extraData?: Record<string, unknown>,
|
|
28
|
+
/**
|
|
29
|
+
* The dialect this template renders into (§5.4). Every interpolated context value is escaped
|
|
30
|
+
* for it automatically, so an author never has to remember.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to `'none'` rather than to a guess: this function also renders NON-filter fields
|
|
33
|
+
* (`UserSearchString`, query transforms) where the value becomes search text, and escaping
|
|
34
|
+
* those would corrupt the query rather than protect it. Filter call sites pass their real kind.
|
|
35
|
+
*/
|
|
36
|
+
laneKind?: ScopeLaneKind): string;
|
|
27
37
|
/**
|
|
28
38
|
* Render and parse a JSON-valued scope template (used for `MetadataFilter` values).
|
|
29
39
|
* Returns:
|
|
@@ -32,5 +42,5 @@ export declare function RenderScopeTemplate(template: string | null | undefined,
|
|
|
32
42
|
* - the raw rendered string when it is non-empty but not JSON (provider can decide what to do)
|
|
33
43
|
* - `undefined` when render fails outright
|
|
34
44
|
*/
|
|
35
|
-
export declare function RenderScopeJsonTemplate(template: string | null | undefined, context: SearchContext | undefined, extraData?: Record<string, unknown
|
|
45
|
+
export declare function RenderScopeJsonTemplate(template: string | null | undefined, context: SearchContext | undefined, extraData?: Record<string, unknown>, laneKind?: ScopeLaneKind): unknown;
|
|
36
46
|
//# sourceMappingURL=ScopeTemplateRenderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScopeTemplateRenderer.d.ts","sourceRoot":"","sources":["../../src/generic/ScopeTemplateRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"ScopeTemplateRenderer.d.ts","sourceRoot":"","sources":["../../src/generic/ScopeTemplateRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAwC/C;;;;GAIG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;AACnC;;;;;;;GAOG;AACH,QAAQ,GAAE,aAAsB,GACjC,MAAM,CA6BR;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,QAAQ,GAAE,aAAsB,GACjC,OAAO,CAWT"}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* @module @memberjunction/search-engine
|
|
19
19
|
*/
|
|
20
20
|
import nunjucks from 'nunjucks';
|
|
21
|
+
import { EscapeScopeValueDeep } from './ScopeValueEscaper.js';
|
|
21
22
|
import { LogError } from '@memberjunction/core';
|
|
22
23
|
/** Environment used for all ad-hoc scope template rendering. */
|
|
23
24
|
const env = new nunjucks.Environment(null, {
|
|
@@ -64,16 +65,35 @@ env.addFilter('jsonparse', (value) => {
|
|
|
64
65
|
* string unchanged when `template` is null/empty. Returns the original string on render
|
|
65
66
|
* failure (logged via LogError) so a single bad template does not bring down a search.
|
|
66
67
|
*/
|
|
67
|
-
export function RenderScopeTemplate(template, context, extraData
|
|
68
|
+
export function RenderScopeTemplate(template, context, extraData,
|
|
69
|
+
/**
|
|
70
|
+
* The dialect this template renders into (§5.4). Every interpolated context value is escaped
|
|
71
|
+
* for it automatically, so an author never has to remember.
|
|
72
|
+
*
|
|
73
|
+
* Defaults to `'none'` rather than to a guess: this function also renders NON-filter fields
|
|
74
|
+
* (`UserSearchString`, query transforms) where the value becomes search text, and escaping
|
|
75
|
+
* those would corrupt the query rather than protect it. Filter call sites pass their real kind.
|
|
76
|
+
*/
|
|
77
|
+
laneKind = 'none') {
|
|
68
78
|
if (!template)
|
|
69
79
|
return '';
|
|
70
80
|
if (!template.includes('{{') && !template.includes('{%')) {
|
|
71
81
|
// No templating syntax — skip the renderer entirely
|
|
72
82
|
return template;
|
|
73
83
|
}
|
|
84
|
+
// `context` is escaped for the lane; `contextRaw` is the untouched original. Deliberate raw
|
|
85
|
+
// insertion is therefore spelled `{{ contextRaw.X }}` — one grep finds every such site, which
|
|
86
|
+
// is the point. A post-escape `| raw` filter could not do this: by the time a filter runs the
|
|
87
|
+
// original value is already gone.
|
|
74
88
|
const data = {
|
|
75
|
-
context: context ?? {},
|
|
76
|
-
|
|
89
|
+
context: EscapeScopeValueDeep(context ?? {}, laneKind),
|
|
90
|
+
contextRaw: context ?? {},
|
|
91
|
+
// extraData is escaped too. It previously spread in un-escaped, which meant the one exported
|
|
92
|
+
// function whose entire job is escaping had a silent bypass sitting in its signature. No
|
|
93
|
+
// in-repo caller passes it today, so this was latent rather than live — but leaving an
|
|
94
|
+
// unescaped channel in a security primitive is how a future caller introduces an injection
|
|
95
|
+
// without touching this file or noticing anything.
|
|
96
|
+
...EscapeScopeValueDeep(extraData ?? {}, laneKind)
|
|
77
97
|
};
|
|
78
98
|
try {
|
|
79
99
|
return env.renderString(template, data);
|
|
@@ -92,10 +112,10 @@ export function RenderScopeTemplate(template, context, extraData) {
|
|
|
92
112
|
* - the raw rendered string when it is non-empty but not JSON (provider can decide what to do)
|
|
93
113
|
* - `undefined` when render fails outright
|
|
94
114
|
*/
|
|
95
|
-
export function RenderScopeJsonTemplate(template, context, extraData) {
|
|
115
|
+
export function RenderScopeJsonTemplate(template, context, extraData, laneKind = 'json') {
|
|
96
116
|
if (!template)
|
|
97
117
|
return undefined;
|
|
98
|
-
const rendered = RenderScopeTemplate(template, context, extraData);
|
|
118
|
+
const rendered = RenderScopeTemplate(template, context, extraData, laneKind);
|
|
99
119
|
const trimmed = rendered.trim();
|
|
100
120
|
if (!trimmed)
|
|
101
121
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScopeTemplateRenderer.js","sourceRoot":"","sources":["../../src/generic/ScopeTemplateRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD,gEAAgE;AAChE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,CAAC,IAAmC,EAAE;IACtE,UAAU,EAAE,KAAK,EAAE,8CAA8C;IACjE,gBAAgB,EAAE,KAAK;IACvB,UAAU,EAAE,IAAI;IAChB,YAAY,EAAE,IAAI;CACrB,CAAC,CAAC;AAEH,mFAAmF;AACnF,sFAAsF;AACtF,oCAAoC;AACpC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,KAAc,EAAE,SAAiB,CAAC,EAAU,EAAE;IACjE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACrD,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,KAAc,EAAU,EAAE;IACnD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACrD,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,KAAc,EAAW,EAAE;IACnD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAC/B,QAAmC,EACnC,OAAkC,EAClC,SAAmC;
|
|
1
|
+
{"version":3,"file":"ScopeTemplateRenderer.js","sourceRoot":"","sources":["../../src/generic/ScopeTemplateRenderer.ts"],"names":[],"mappings":"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"}
|