@memberjunction/global 5.51.2 → 5.51.4
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/README.md +2 -2
- package/dist/SQLExpressionValidator.d.ts +37 -31
- package/dist/SQLExpressionValidator.d.ts.map +1 -1
- package/dist/SQLExpressionValidator.js +129 -117
- package/dist/SQLExpressionValidator.js.map +1 -1
- package/dist/SQLScreeningScanner.d.ts +91 -0
- package/dist/SQLScreeningScanner.d.ts.map +1 -0
- package/dist/SQLScreeningScanner.js +332 -0
- package/dist/SQLScreeningScanner.js.map +1 -0
- package/dist/SafeExpressionEvaluator.d.ts +131 -6
- package/dist/SafeExpressionEvaluator.d.ts.map +1 -1
- package/dist/SafeExpressionEvaluator.js +344 -41
- package/dist/SafeExpressionEvaluator.js.map +1 -1
- package/dist/__tests__/SQLExpressionValidator.security.test.js +176 -2
- package/dist/__tests__/SQLExpressionValidator.security.test.js.map +1 -1
- package/dist/__tests__/SQLScreeningScanner.test.d.ts +2 -0
- package/dist/__tests__/SQLScreeningScanner.test.d.ts.map +1 -0
- package/dist/__tests__/SQLScreeningScanner.test.js +172 -0
- package/dist/__tests__/SQLScreeningScanner.test.js.map +1 -0
- package/dist/__tests__/SafeExpressionEvaluator.security.test.d.ts +2 -0
- package/dist/__tests__/SafeExpressionEvaluator.security.test.d.ts.map +1 -0
- package/dist/__tests__/SafeExpressionEvaluator.security.test.js +284 -0
- package/dist/__tests__/SafeExpressionEvaluator.security.test.js.map +1 -0
- package/dist/__tests__/SafeExpressionEvaluator.test.d.ts +2 -0
- package/dist/__tests__/SafeExpressionEvaluator.test.d.ts.map +1 -0
- package/dist/__tests__/SafeExpressionEvaluator.test.js +355 -0
- package/dist/__tests__/SafeExpressionEvaluator.test.js.map +1 -0
- package/dist/__tests__/util.jsonRepair.test.d.ts +2 -0
- package/dist/__tests__/util.jsonRepair.test.d.ts.map +1 -0
- package/dist/__tests__/util.jsonRepair.test.js +112 -0
- package/dist/__tests__/util.jsonRepair.test.js.map +1 -0
- package/dist/__tests__/util.test.js +19 -1
- package/dist/__tests__/util.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/util.d.ts +80 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +220 -2
- package/dist/util.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dialect-aware SQL scanner for MemberJunction's SQL denylist screens.
|
|
3
|
+
*
|
|
4
|
+
* A denylist screen is only as good as its idea of which text is code. This scanner reads SQL the
|
|
5
|
+
* way SQL Server or PostgreSQL splits it into tokens and marks every string literal, quoted
|
|
6
|
+
* identifier and comment, so a screen checks exactly the text the database runs as code. When it
|
|
7
|
+
* cannot read the text the way the database would, it reports that, so the screen rejects the text
|
|
8
|
+
* instead of guessing.
|
|
9
|
+
*
|
|
10
|
+
* Used by `SQLExpressionValidator` and `DatabaseProviderBase.ValidateUserProvidedSQLClause`.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/global/SQLScreeningScanner
|
|
13
|
+
*/
|
|
14
|
+
/** SQL dialects whose lexical rules {@link ScanSQLForScreening} knows. */
|
|
15
|
+
export type SQLScreeningDialect = 'sqlserver' | 'postgresql';
|
|
16
|
+
/**
|
|
17
|
+
* Every dialect {@link ScanSQLForScreening} knows. A screen that does not know which database will
|
|
18
|
+
* run the SQL must accept it under each one.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SQL_SCREENING_DIALECTS: readonly SQLScreeningDialect[];
|
|
21
|
+
/** Kinds of text that the database never runs as code. */
|
|
22
|
+
export type SQLScreeningSpanKind = 'string' | 'identifier' | 'comment';
|
|
23
|
+
/** One string literal, quoted identifier or comment found by {@link ScanSQLForScreening}. */
|
|
24
|
+
export interface SQLScreeningSpan {
|
|
25
|
+
Kind: SQLScreeningSpanKind;
|
|
26
|
+
/** Offset of the first character, including a prefix such as `N` or `E`. */
|
|
27
|
+
Start: number;
|
|
28
|
+
/** Offset just past the last character. */
|
|
29
|
+
End: number;
|
|
30
|
+
}
|
|
31
|
+
/** Result of {@link ScanSQLForScreening}. */
|
|
32
|
+
export interface SQLScreeningScan {
|
|
33
|
+
/** The dialect whose rules were applied. */
|
|
34
|
+
Dialect: SQLScreeningDialect;
|
|
35
|
+
/** True when every character was read the way the database reads it. When false, reject the text. */
|
|
36
|
+
Classified: boolean;
|
|
37
|
+
/** Why the text could not be classified; null when {@link Classified} is true. */
|
|
38
|
+
Error: string | null;
|
|
39
|
+
/** The string literals, quoted identifiers and comments found, in order. */
|
|
40
|
+
Spans: SQLScreeningSpan[];
|
|
41
|
+
/** Number of comments found. */
|
|
42
|
+
CommentCount: number;
|
|
43
|
+
/**
|
|
44
|
+
* The input with each string literal, quoted identifier and comment replaced by one space, and a
|
|
45
|
+
* space between a number and a word written against it. Only code remains, split where the
|
|
46
|
+
* database splits it, so `N'x'UNION` reads as ` UNION` and `1UNION` as `1 UNION`. Keyword,
|
|
47
|
+
* statement-separator and comment-marker checks read this. When {@link Classified} is false,
|
|
48
|
+
* the text from the point where the scan stopped is left as written.
|
|
49
|
+
*/
|
|
50
|
+
Code: string;
|
|
51
|
+
/**
|
|
52
|
+
* {@link Code}, except each quoted identifier is written as its bare name between spaces, so
|
|
53
|
+
* `[sys].[objects]` reads as ` sys . objects `. Checks for object and function names read this,
|
|
54
|
+
* so quoting a name does not hide it.
|
|
55
|
+
*/
|
|
56
|
+
CodeWithIdentifiers: string;
|
|
57
|
+
/**
|
|
58
|
+
* The first token that is not whitespace or a comment, when that token is an unquoted word and
|
|
59
|
+
* the character after it is ASCII, so every server ends the word in the same place. Null when a
|
|
60
|
+
* literal, quoted identifier, number or symbol comes first, or there is no token. Checks for the
|
|
61
|
+
* statement's opening keyword read this.
|
|
62
|
+
*/
|
|
63
|
+
LeadingWord: string | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads `sql` the way `dialect` splits it into tokens. Returns the string literals, quoted
|
|
67
|
+
* identifiers and comments it contains, and the code that remains.
|
|
68
|
+
*
|
|
69
|
+
* Both dialects: `'…'` and `N'…'` strings, in which `''` is an escaped quote and a backslash is an
|
|
70
|
+
* ordinary character; `"…"` identifiers with `""` escapes; `--` comments to the end of the line; and
|
|
71
|
+
* nested block comments. SQL Server adds `[…]` identifiers with `]]` escapes. PostgreSQL adds `E'…'`
|
|
72
|
+
* strings, in which a backslash escapes the next character, and dollar-quoted strings (`$$…$$`,
|
|
73
|
+
* `$tag$…$tag$`); a `[` is code there.
|
|
74
|
+
*
|
|
75
|
+
* 🚨 Do not add backslash escapes to plain `'…'` strings. Neither server honors them (PostgreSQL is
|
|
76
|
+
* assumed to run with `standard_conforming_strings = on`, its default since 9.1), and a scanner that
|
|
77
|
+
* does reads `x = 'a\') ; DROP TABLE t; --'` as one literal while the database runs the DROP.
|
|
78
|
+
*
|
|
79
|
+
* The scan stops and reports an error, instead of guessing, when:
|
|
80
|
+
* - a string, quoted identifier, dollar-quoted string or block comment is not closed;
|
|
81
|
+
* - the text contains a NUL character;
|
|
82
|
+
* - a `--` comment contains a line break other than `\n` or `\r\n`;
|
|
83
|
+
* - a PostgreSQL `E'…'` string is followed by another quoted segment, which PostgreSQL can join to
|
|
84
|
+
* it and read with backslash escapes;
|
|
85
|
+
* - on SQL Server, a number is followed by `e` and a word character (`1EXEC` splits two ways).
|
|
86
|
+
*
|
|
87
|
+
* @param sql The clause, expression or query to read
|
|
88
|
+
* @param dialect The dialect whose lexical rules apply
|
|
89
|
+
*/
|
|
90
|
+
export declare function ScanSQLForScreening(sql: string, dialect: SQLScreeningDialect): SQLScreeningScan;
|
|
91
|
+
//# sourceMappingURL=SQLScreeningScanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SQLScreeningScanner.d.ts","sourceRoot":"","sources":["../src/SQLScreeningScanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,0EAA0E;AAC1E,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,YAAY,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAAgC,CAAC;AAElG,0DAA0D;AAC1D,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAC;AAEvE,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACf;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC7B,4CAA4C;IAC5C,OAAO,EAAE,mBAAmB,CAAC;IAC7B,qGAAqG;IACrG,UAAU,EAAE,OAAO,CAAC;IACpB,kFAAkF;IAClF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4EAA4E;IAC5E,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AA2DD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,gBAAgB,CAE/F"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dialect-aware SQL scanner for MemberJunction's SQL denylist screens.
|
|
3
|
+
*
|
|
4
|
+
* A denylist screen is only as good as its idea of which text is code. This scanner reads SQL the
|
|
5
|
+
* way SQL Server or PostgreSQL splits it into tokens and marks every string literal, quoted
|
|
6
|
+
* identifier and comment, so a screen checks exactly the text the database runs as code. When it
|
|
7
|
+
* cannot read the text the way the database would, it reports that, so the screen rejects the text
|
|
8
|
+
* instead of guessing.
|
|
9
|
+
*
|
|
10
|
+
* Used by `SQLExpressionValidator` and `DatabaseProviderBase.ValidateUserProvidedSQLClause`.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/global/SQLScreeningScanner
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Every dialect {@link ScanSQLForScreening} knows. A screen that does not know which database will
|
|
16
|
+
* run the SQL must accept it under each one.
|
|
17
|
+
*/
|
|
18
|
+
export const SQL_SCREENING_DIALECTS = ['sqlserver', 'postgresql'];
|
|
19
|
+
const LEXICAL_RULES = {
|
|
20
|
+
sqlserver: {
|
|
21
|
+
BracketIdentifiers: true,
|
|
22
|
+
PostgresStrings: false,
|
|
23
|
+
// Letters and digits of any script. Other non-ASCII characters are not part of a word: SQL
|
|
24
|
+
// Server reads a currency symbol such as ¥ as the start of a money literal, so in `¥1SELECT`
|
|
25
|
+
// the keyword is a separate token.
|
|
26
|
+
WordStart: /[\p{L}_@#]/u,
|
|
27
|
+
WordPart: /[\p{L}\p{N}_@#$]/u,
|
|
28
|
+
BareExponent: true,
|
|
29
|
+
},
|
|
30
|
+
postgresql: {
|
|
31
|
+
BracketIdentifiers: false,
|
|
32
|
+
PostgresStrings: true,
|
|
33
|
+
WordStart: /[A-Za-z_\u0080-]/,
|
|
34
|
+
WordPart: /[A-Za-z0-9_$\u0080-]/,
|
|
35
|
+
BareExponent: false,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const DIGIT = /[0-9]/;
|
|
39
|
+
const WHITESPACE = /\s/;
|
|
40
|
+
/** Whitespace every server skips. Other Unicode spaces count as a token when finding the leading word. */
|
|
41
|
+
const ASCII_WHITESPACE = /[ \t\n\r\f\v]/;
|
|
42
|
+
const NON_ASCII = /[^\x00-\x7f]/;
|
|
43
|
+
/**
|
|
44
|
+
* A numeric literal at `lastIndex` (sticky). An exponent needs digits, so in PostgreSQL's `1e'…'`
|
|
45
|
+
* the `e` is left to open an E'…' string.
|
|
46
|
+
*/
|
|
47
|
+
const NUMBER = /0[xX][0-9A-Fa-f]*|(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/y;
|
|
48
|
+
/** A PostgreSQL `$$` or `$tag$` delimiter at `lastIndex` (sticky). */
|
|
49
|
+
const DOLLAR_DELIMITER = /\$(?:[A-Za-z_\u0080-][A-Za-z0-9_\u0080-]*)?\$/y;
|
|
50
|
+
/** Line breaks other than `\n` and `\r\n`. Servers disagree on whether they end a `--` comment. */
|
|
51
|
+
const AMBIGUOUS_LINE_BREAKS = new Set(['\r', '\u000b', '\u000c', '\u0085', '
', '
']);
|
|
52
|
+
/**
|
|
53
|
+
* Reads `sql` the way `dialect` splits it into tokens. Returns the string literals, quoted
|
|
54
|
+
* identifiers and comments it contains, and the code that remains.
|
|
55
|
+
*
|
|
56
|
+
* Both dialects: `'…'` and `N'…'` strings, in which `''` is an escaped quote and a backslash is an
|
|
57
|
+
* ordinary character; `"…"` identifiers with `""` escapes; `--` comments to the end of the line; and
|
|
58
|
+
* nested block comments. SQL Server adds `[…]` identifiers with `]]` escapes. PostgreSQL adds `E'…'`
|
|
59
|
+
* strings, in which a backslash escapes the next character, and dollar-quoted strings (`$$…$$`,
|
|
60
|
+
* `$tag$…$tag$`); a `[` is code there.
|
|
61
|
+
*
|
|
62
|
+
* 🚨 Do not add backslash escapes to plain `'…'` strings. Neither server honors them (PostgreSQL is
|
|
63
|
+
* assumed to run with `standard_conforming_strings = on`, its default since 9.1), and a scanner that
|
|
64
|
+
* does reads `x = 'a\') ; DROP TABLE t; --'` as one literal while the database runs the DROP.
|
|
65
|
+
*
|
|
66
|
+
* The scan stops and reports an error, instead of guessing, when:
|
|
67
|
+
* - a string, quoted identifier, dollar-quoted string or block comment is not closed;
|
|
68
|
+
* - the text contains a NUL character;
|
|
69
|
+
* - a `--` comment contains a line break other than `\n` or `\r\n`;
|
|
70
|
+
* - a PostgreSQL `E'…'` string is followed by another quoted segment, which PostgreSQL can join to
|
|
71
|
+
* it and read with backslash escapes;
|
|
72
|
+
* - on SQL Server, a number is followed by `e` and a word character (`1EXEC` splits two ways).
|
|
73
|
+
*
|
|
74
|
+
* @param sql The clause, expression or query to read
|
|
75
|
+
* @param dialect The dialect whose lexical rules apply
|
|
76
|
+
*/
|
|
77
|
+
export function ScanSQLForScreening(sql, dialect) {
|
|
78
|
+
return new SQLScreeningScanner(sql, dialect).Scan();
|
|
79
|
+
}
|
|
80
|
+
/** One left-to-right pass over the input, with the dialect's rules. */
|
|
81
|
+
class SQLScreeningScanner {
|
|
82
|
+
constructor(sql, dialect) {
|
|
83
|
+
this.sql = sql;
|
|
84
|
+
this.dialect = dialect;
|
|
85
|
+
this.spans = [];
|
|
86
|
+
this.code = [];
|
|
87
|
+
this.codeWithIdentifiers = [];
|
|
88
|
+
this.commentCount = 0;
|
|
89
|
+
this.pos = 0;
|
|
90
|
+
/** Offset of the first token that is not whitespace or a comment, once one is reached. */
|
|
91
|
+
this.leadingTokenStart = null;
|
|
92
|
+
this.leadingWord = null;
|
|
93
|
+
this.rules = LEXICAL_RULES[dialect];
|
|
94
|
+
}
|
|
95
|
+
Scan() {
|
|
96
|
+
if (!SQL_SCREENING_DIALECTS.includes(this.dialect))
|
|
97
|
+
return this.stop(`"${this.dialect}" is not a known SQL dialect`);
|
|
98
|
+
if (this.sql.includes('\0'))
|
|
99
|
+
return this.stop('the text contains a NUL character');
|
|
100
|
+
while (this.pos < this.sql.length) {
|
|
101
|
+
const error = this.readToken();
|
|
102
|
+
if (error !== null)
|
|
103
|
+
return this.stop(error);
|
|
104
|
+
}
|
|
105
|
+
return this.result(null);
|
|
106
|
+
}
|
|
107
|
+
/** Reads the token at `pos`. Returns why the text cannot be classified, or null. */
|
|
108
|
+
readToken() {
|
|
109
|
+
const c = this.sql[this.pos];
|
|
110
|
+
const next = this.sql[this.pos + 1] ?? '';
|
|
111
|
+
if (c === '-' && next === '-')
|
|
112
|
+
return this.readLineComment();
|
|
113
|
+
if (c === '/' && next === '*')
|
|
114
|
+
return this.readBlockComment();
|
|
115
|
+
if (this.leadingTokenStart === null && !ASCII_WHITESPACE.test(c))
|
|
116
|
+
this.leadingTokenStart = this.pos;
|
|
117
|
+
if (c === "'")
|
|
118
|
+
return this.readString(this.pos + 1);
|
|
119
|
+
if ((c === 'N' || c === 'n') && next === "'")
|
|
120
|
+
return this.readString(this.pos + 2);
|
|
121
|
+
if (this.rules.PostgresStrings && (c === 'E' || c === 'e') && next === "'")
|
|
122
|
+
return this.readEscapeString();
|
|
123
|
+
if (this.rules.PostgresStrings && c === '$') {
|
|
124
|
+
const delimiter = this.dollarDelimiterAt(this.pos);
|
|
125
|
+
if (delimiter !== null)
|
|
126
|
+
return this.readDollarString(delimiter);
|
|
127
|
+
}
|
|
128
|
+
if (c === '"')
|
|
129
|
+
return this.readQuotedIdentifier('"');
|
|
130
|
+
if (this.rules.BracketIdentifiers && c === '[')
|
|
131
|
+
return this.readQuotedIdentifier(']');
|
|
132
|
+
if (DIGIT.test(c) || (c === '.' && DIGIT.test(next)))
|
|
133
|
+
return this.readNumber();
|
|
134
|
+
if (this.rules.WordStart.test(c))
|
|
135
|
+
return this.readWord();
|
|
136
|
+
this.emitCode(this.pos + 1);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/** Reads a `'…'` string whose content starts at `contentStart`. */
|
|
140
|
+
readString(contentStart) {
|
|
141
|
+
const end = this.findQuoteEnd(contentStart, "'", false);
|
|
142
|
+
if (end === -1)
|
|
143
|
+
return 'a string literal is not closed';
|
|
144
|
+
this.emitSpan('string', end);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
/** Reads a PostgreSQL `E'…'` string, in which a backslash escapes the next character. */
|
|
148
|
+
readEscapeString() {
|
|
149
|
+
const end = this.findQuoteEnd(this.pos + 2, "'", true);
|
|
150
|
+
if (end === -1)
|
|
151
|
+
return "an E'…' string is not closed";
|
|
152
|
+
if (this.quoteFollows(end)) {
|
|
153
|
+
return "an E'…' string is followed by another quoted segment, which PostgreSQL can read as part of it";
|
|
154
|
+
}
|
|
155
|
+
this.emitSpan('string', end);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/** Reads a PostgreSQL dollar-quoted string that opens with `delimiter`. */
|
|
159
|
+
readDollarString(delimiter) {
|
|
160
|
+
const close = this.sql.indexOf(delimiter, this.pos + delimiter.length);
|
|
161
|
+
if (close === -1)
|
|
162
|
+
return 'a dollar-quoted string is not closed';
|
|
163
|
+
this.emitSpan('string', close + delimiter.length);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/** Reads a `"…"` or `[…]` identifier; `close` is its closing character. */
|
|
167
|
+
readQuotedIdentifier(close) {
|
|
168
|
+
const end = this.findQuoteEnd(this.pos + 1, close, false);
|
|
169
|
+
if (end === -1)
|
|
170
|
+
return 'a quoted identifier is not closed';
|
|
171
|
+
const name = this.sql.slice(this.pos + 1, end - 1).split(close + close).join(close);
|
|
172
|
+
this.emitSpan('identifier', end, name);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/** Reads a `--` comment, which ends at the end of the line. */
|
|
176
|
+
readLineComment() {
|
|
177
|
+
let j = this.pos + 2;
|
|
178
|
+
while (j < this.sql.length && this.sql[j] !== '\n') {
|
|
179
|
+
if (this.sql[j] === '\r' && this.sql[j + 1] === '\n')
|
|
180
|
+
break;
|
|
181
|
+
if (AMBIGUOUS_LINE_BREAKS.has(this.sql[j])) {
|
|
182
|
+
return 'a -- comment contains a line break that database servers read differently';
|
|
183
|
+
}
|
|
184
|
+
j++;
|
|
185
|
+
}
|
|
186
|
+
this.commentCount++;
|
|
187
|
+
this.emitSpan('comment', j);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/** Reads a block comment. Block comments nest on both SQL Server and PostgreSQL. */
|
|
191
|
+
readBlockComment() {
|
|
192
|
+
let depth = 0;
|
|
193
|
+
let j = this.pos;
|
|
194
|
+
while (j < this.sql.length) {
|
|
195
|
+
if (this.sql[j] === '/' && this.sql[j + 1] === '*') {
|
|
196
|
+
depth++;
|
|
197
|
+
j += 2;
|
|
198
|
+
}
|
|
199
|
+
else if (this.sql[j] === '*' && this.sql[j + 1] === '/') {
|
|
200
|
+
depth--;
|
|
201
|
+
j += 2;
|
|
202
|
+
if (depth === 0) {
|
|
203
|
+
this.commentCount++;
|
|
204
|
+
this.emitSpan('comment', j);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
j++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return 'a block comment is not closed';
|
|
213
|
+
}
|
|
214
|
+
/** Reads a number. A word written against it is a separate token, so a space is added between. */
|
|
215
|
+
readNumber() {
|
|
216
|
+
NUMBER.lastIndex = this.pos;
|
|
217
|
+
const match = NUMBER.exec(this.sql);
|
|
218
|
+
const end = this.pos + (match ? match[0].length : 1);
|
|
219
|
+
const following = this.sql[end] ?? '';
|
|
220
|
+
const afterThat = this.sql[end + 1] ?? '';
|
|
221
|
+
if (this.rules.BareExponent && (following === 'e' || following === 'E') && this.rules.WordStart.test(afterThat)) {
|
|
222
|
+
return 'a number is followed by a word that starts with E, which the server can read as part of the number';
|
|
223
|
+
}
|
|
224
|
+
this.emitCode(end);
|
|
225
|
+
if (following !== '' && this.rules.WordStart.test(following))
|
|
226
|
+
this.emitBoundary();
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
/** Reads an unquoted keyword or identifier. */
|
|
230
|
+
readWord() {
|
|
231
|
+
let j = this.pos + 1;
|
|
232
|
+
while (j < this.sql.length && this.rules.WordPart.test(this.sql[j]))
|
|
233
|
+
j++;
|
|
234
|
+
if (this.pos === this.leadingTokenStart && !NON_ASCII.test(this.sql[j] ?? '')) {
|
|
235
|
+
this.leadingWord = this.sql.slice(this.pos, j);
|
|
236
|
+
}
|
|
237
|
+
this.emitCode(j);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Offset just past the `close` character that ends a quoted run whose content starts at `from`,
|
|
242
|
+
* or -1 when none does. A doubled `close` is an escaped one; with `backslashEscapes`, so is any
|
|
243
|
+
* character after a backslash.
|
|
244
|
+
*/
|
|
245
|
+
findQuoteEnd(from, close, backslashEscapes) {
|
|
246
|
+
let j = from;
|
|
247
|
+
while (j < this.sql.length) {
|
|
248
|
+
const ch = this.sql[j];
|
|
249
|
+
if (backslashEscapes && ch === '\\') {
|
|
250
|
+
j += 2;
|
|
251
|
+
}
|
|
252
|
+
else if (ch !== close) {
|
|
253
|
+
j++;
|
|
254
|
+
}
|
|
255
|
+
else if (this.sql[j + 1] === close) {
|
|
256
|
+
j += 2;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return j + 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
/** Whether only whitespace and `--` comments come between `from` and the next `'`. */
|
|
265
|
+
quoteFollows(from) {
|
|
266
|
+
let j = from;
|
|
267
|
+
while (j < this.sql.length) {
|
|
268
|
+
const ch = this.sql[j];
|
|
269
|
+
if (ch === "'")
|
|
270
|
+
return true;
|
|
271
|
+
if (ch === '-' && this.sql[j + 1] === '-') {
|
|
272
|
+
j = this.lineEnd(j);
|
|
273
|
+
}
|
|
274
|
+
else if (WHITESPACE.test(ch)) {
|
|
275
|
+
j++;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
/** Offset of the first `\n` or `\r` at or after `from`, or the input length. */
|
|
284
|
+
lineEnd(from) {
|
|
285
|
+
let j = from;
|
|
286
|
+
while (j < this.sql.length && this.sql[j] !== '\n' && this.sql[j] !== '\r')
|
|
287
|
+
j++;
|
|
288
|
+
return j;
|
|
289
|
+
}
|
|
290
|
+
/** The `$$` or `$tag$` delimiter at `at`, or null when the `$` does not open a dollar-quoted string. */
|
|
291
|
+
dollarDelimiterAt(at) {
|
|
292
|
+
DOLLAR_DELIMITER.lastIndex = at;
|
|
293
|
+
return DOLLAR_DELIMITER.exec(this.sql)?.[0] ?? null;
|
|
294
|
+
}
|
|
295
|
+
/** Copies code up to `end` into both projections. */
|
|
296
|
+
emitCode(end) {
|
|
297
|
+
const text = this.sql.slice(this.pos, end);
|
|
298
|
+
this.code.push(text);
|
|
299
|
+
this.codeWithIdentifiers.push(text);
|
|
300
|
+
this.pos = end;
|
|
301
|
+
}
|
|
302
|
+
/** Records a span up to `end` and writes a space for it, or an identifier's bare name. */
|
|
303
|
+
emitSpan(kind, end, identifierName) {
|
|
304
|
+
this.spans.push({ Kind: kind, Start: this.pos, End: end });
|
|
305
|
+
this.code.push(' ');
|
|
306
|
+
this.codeWithIdentifiers.push(identifierName === undefined ? ' ' : ` ${identifierName} `);
|
|
307
|
+
this.pos = end;
|
|
308
|
+
}
|
|
309
|
+
/** Writes a token boundary that the input leaves implicit. */
|
|
310
|
+
emitBoundary() {
|
|
311
|
+
this.code.push(' ');
|
|
312
|
+
this.codeWithIdentifiers.push(' ');
|
|
313
|
+
}
|
|
314
|
+
/** Ends the scan at `pos`, leaving the unread text as written. */
|
|
315
|
+
stop(error) {
|
|
316
|
+
this.emitCode(this.sql.length);
|
|
317
|
+
return this.result(error);
|
|
318
|
+
}
|
|
319
|
+
result(error) {
|
|
320
|
+
return {
|
|
321
|
+
Dialect: this.dialect,
|
|
322
|
+
Classified: error === null,
|
|
323
|
+
Error: error,
|
|
324
|
+
Spans: this.spans,
|
|
325
|
+
CommentCount: this.commentCount,
|
|
326
|
+
Code: this.code.join(''),
|
|
327
|
+
CodeWithIdentifiers: this.codeWithIdentifiers.join(''),
|
|
328
|
+
LeadingWord: this.leadingWord,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=SQLScreeningScanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SQLScreeningScanner.js","sourceRoot":"","sources":["../src/SQLScreeningScanner.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,33 @@
|
|
|
9
9
|
* @author MemberJunction.com
|
|
10
10
|
* @since 2.76.0
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Namespace globals whose listed methods a condition may CALL, keyed by namespace name.
|
|
14
|
+
*
|
|
15
|
+
* Both halves of `Math.abs(x)` are fixed identifiers in the AST, so admitting them adds no dynamic
|
|
16
|
+
* lookup: the four invariants that close the sandbox escape — literal-only computed keys, no
|
|
17
|
+
* `constructor`/`__proto__`/`prototype`, a closed non-computed call surface with no
|
|
18
|
+
* `call`/`apply`/`bind`, and screened free identifiers — are all untouched by this list.
|
|
19
|
+
*
|
|
20
|
+
* Every entry is a pure value function. `Math.random` is deliberately absent (a condition that
|
|
21
|
+
* evaluates differently on each run is not a guard), as are `Object.assign`,
|
|
22
|
+
* `Object.defineProperty` and `Object.setPrototypeOf`, which mutate.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SAFE_GLOBAL_NAMESPACE_METHODS: ReadonlyMap<string, ReadonlySet<string>>;
|
|
25
|
+
/**
|
|
26
|
+
* Global functions a condition may call by bare name — the coercions and numeric predicates that
|
|
27
|
+
* authored specs use (`Number(payload.count) > 3`, `isNaN(payload.count)`).
|
|
28
|
+
*/
|
|
29
|
+
export declare const SAFE_GLOBAL_CALLABLES: ReadonlySet<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Every global name a condition may reference, as a namespace or as a bare callable.
|
|
32
|
+
*
|
|
33
|
+
* **This is the single source of truth for the ambient globals of the condition dialect.** It lives
|
|
34
|
+
* beside the policy screen that enforces it so a caller validating conditions ahead of evaluation
|
|
35
|
+
* (`ai-core-plus`'s task-graph door) cannot bless a name the runtime would refuse — the failure mode
|
|
36
|
+
* that shipped once already, where the door's own list and the evaluator disagreed silently.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SAFE_EXPRESSION_GLOBALS: ReadonlySet<string>;
|
|
12
39
|
/**
|
|
13
40
|
* Result of expression evaluation including success status and diagnostics
|
|
14
41
|
*/
|
|
@@ -29,11 +56,22 @@ export interface ExpressionEvaluationResult {
|
|
|
29
56
|
* Supported operations:
|
|
30
57
|
* - Comparison: ==, ===, !=, !==, <, >, <=, >=
|
|
31
58
|
* - Logical: &&, ||, !
|
|
32
|
-
* - Property access: dot notation (e.g., payload.customer.name)
|
|
33
|
-
* - Array access: bracket notation (e.g., items[0])
|
|
59
|
+
* - Property access: dot notation (e.g., payload.customer.name), including optional chaining (`?.`)
|
|
60
|
+
* - Array access: bracket notation with a literal index/key (e.g., items[0])
|
|
34
61
|
* - Safe methods: .length, .includes(), .startsWith(), .endsWith()
|
|
35
62
|
* - Array methods: .some(), .every(), .find(), .filter()
|
|
36
|
-
* -
|
|
63
|
+
* - Safe globals: the namespaces and bare callables in {@link SAFE_EXPRESSION_GLOBALS}
|
|
64
|
+
* (e.g. `Math.abs(output.delta) < 5`, `Number(payload.count) > 3`, `Object.keys(payload).length`)
|
|
65
|
+
* - Type checking: typeof
|
|
66
|
+
*
|
|
67
|
+
* Safety is enforced by parsing the expression to an AST and walking it against
|
|
68
|
+
* an ALLOWLIST of node types before it is compiled — an unlisted construct
|
|
69
|
+
* (computed member access with a non-literal key, `.constructor`/`__proto__`
|
|
70
|
+
* access, any call outside the safe-method and safe-global lists, host-global
|
|
71
|
+
* identifiers, etc.) is rejected at validation time and never reaches the compiler. A structural
|
|
72
|
+
* allowlist cannot be defeated by string concatenation the way a textual
|
|
73
|
+
* denylist can, and it does not over-reject data that merely mentions a reserved
|
|
74
|
+
* word (e.g. `name == 'constructor'` is a legal comparison).
|
|
37
75
|
*
|
|
38
76
|
* @class SafeExpressionEvaluator
|
|
39
77
|
*
|
|
@@ -62,10 +100,24 @@ export interface ExpressionEvaluationResult {
|
|
|
62
100
|
*/
|
|
63
101
|
export declare class SafeExpressionEvaluator {
|
|
64
102
|
/**
|
|
65
|
-
*
|
|
103
|
+
* Identifiers that must never resolve to a host global. Because the compiled
|
|
104
|
+
* function runs in global scope, a bare `process`/`Function`/`globalThis`/…
|
|
105
|
+
* would reach the real global object; identifiers are atomic AST tokens, so a
|
|
106
|
+
* name that is split with string concatenation parses as separate identifiers
|
|
107
|
+
* or an operator and is caught by the structural rules instead.
|
|
66
108
|
* @private
|
|
67
109
|
*/
|
|
68
|
-
private static readonly
|
|
110
|
+
private static readonly DANGEROUS_IDENTIFIERS;
|
|
111
|
+
/**
|
|
112
|
+
* Property names that open the prototype chain / constructor climb. Denied on
|
|
113
|
+
* both dotted (`x.constructor`) and literal-bracket (`x["constructor"]`) access.
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
private static readonly DANGEROUS_PROPERTY_NAMES;
|
|
117
|
+
/** Binary operators the evaluator permits. `instanceof`/`in`/bitwise are excluded. @private */
|
|
118
|
+
private static readonly ALLOWED_BINARY_OPERATORS;
|
|
119
|
+
/** Unary operators the evaluator permits. `delete`/`void` are excluded. @private */
|
|
120
|
+
private static readonly ALLOWED_UNARY_OPERATORS;
|
|
69
121
|
/**
|
|
70
122
|
* Cache for compiled expressions to improve performance on repeated evaluations
|
|
71
123
|
* @private
|
|
@@ -87,7 +139,8 @@ export declare class SafeExpressionEvaluator {
|
|
|
87
139
|
*/
|
|
88
140
|
evaluate(expression: string, context: Record<string, any>, enableDiagnostics?: boolean): ExpressionEvaluationResult;
|
|
89
141
|
/**
|
|
90
|
-
* Validates an expression for safety
|
|
142
|
+
* Validates an expression for safety by parsing it and walking the AST against
|
|
143
|
+
* an allowlist. Returns an error message if any construct is not permitted.
|
|
91
144
|
*
|
|
92
145
|
* @param {string} expression - The expression to validate
|
|
93
146
|
*
|
|
@@ -96,6 +149,44 @@ export declare class SafeExpressionEvaluator {
|
|
|
96
149
|
* @private
|
|
97
150
|
*/
|
|
98
151
|
private validateExpression;
|
|
152
|
+
/** Builds the standard rejection message. @private */
|
|
153
|
+
private forbidden;
|
|
154
|
+
/**
|
|
155
|
+
* Walks an AST node against the allowlist. Returns an error message on the first
|
|
156
|
+
* disallowed construct, or null if the entire subtree is permitted.
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
private checkNode;
|
|
160
|
+
/**
|
|
161
|
+
* Member access is allowed for dotted property reads and literal-key bracket
|
|
162
|
+
* reads, but never for a computed key built at runtime (the string-concat
|
|
163
|
+
* escape route) or a prototype-chain property.
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
private checkMemberExpression;
|
|
167
|
+
/**
|
|
168
|
+
* A call is admitted only when its callee is a FIXED name on one of two allowlists: a bare
|
|
169
|
+
* safe global (`parseInt(...)`) or a dotted method that is either a safe instance method or a
|
|
170
|
+
* method of a safe global namespace (`Math.abs(...)`). Everything else — a computed callee, an
|
|
171
|
+
* unlisted bare identifier (`eval(...)`, `Function(...)`), a call on a call's result that is
|
|
172
|
+
* not itself a safe method — is rejected.
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
private checkCallExpression;
|
|
176
|
+
/**
|
|
177
|
+
* Whether `<receiver>.<method>()` names a permitted call: a safe instance method on any value,
|
|
178
|
+
* or a method of a safe global namespace when the receiver is that namespace by bare name.
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
private isCallableMethod;
|
|
182
|
+
/** Walks a call's arguments; spreads are rejected outright. @private */
|
|
183
|
+
private checkCallArguments;
|
|
184
|
+
/**
|
|
185
|
+
* Arrow functions are permitted only as callback arguments to the safe array
|
|
186
|
+
* methods; parameters must be simple identifiers and the body an expression.
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
private checkArrowFunction;
|
|
99
190
|
/**
|
|
100
191
|
* Creates a safe context object with only allowed properties
|
|
101
192
|
*
|
|
@@ -126,6 +217,40 @@ export declare class SafeExpressionEvaluator {
|
|
|
126
217
|
* @private
|
|
127
218
|
*/
|
|
128
219
|
private cloneValue;
|
|
220
|
+
/**
|
|
221
|
+
* Checks whether an expression *could* be evaluated, without evaluating it.
|
|
222
|
+
*
|
|
223
|
+
* The difference matters because `evaluate` reports two unrelated problems the same way. Given
|
|
224
|
+
* a condition authored against a runtime envelope, an empty context makes `payload.x > 1` fail
|
|
225
|
+
* with `payload is not defined` — indistinguishable, by result shape, from `payload.x >` failing
|
|
226
|
+
* with `Unexpected token`. One is a typo the author should be told about at the door; the other
|
|
227
|
+
* is a perfectly good condition that simply has no data yet. A submit-time check built on
|
|
228
|
+
* `evaluate` therefore refuses every legitimate condition.
|
|
229
|
+
*
|
|
230
|
+
* So this compiles and never runs. The expression goes through the same policy screen
|
|
231
|
+
* `evaluate` applies (the AST allowlist), then the same function body is BUILT and discarded —
|
|
232
|
+
* never invoked. Values are never consulted, which is precisely the property wanted: unknown
|
|
233
|
+
* identifiers, absent properties and undefined chains all PASS, because none of them is a syntax
|
|
234
|
+
* error and whether they resolve is a question about a run that has not happened yet.
|
|
235
|
+
*
|
|
236
|
+
* **Compilation is not evaluation**, and the distinction is load-bearing for safety: `Function`
|
|
237
|
+
* parses the body and returns; nothing in the expression executes. The policy screen still runs
|
|
238
|
+
* first, so the constructs `evaluate` refuses are refused here too — those produce a permanent
|
|
239
|
+
* runtime refusal, and an author is better told now.
|
|
240
|
+
*
|
|
241
|
+
* `Undecidable` is the honest third answer. A host that forbids dynamic compilation (a strict
|
|
242
|
+
* CSP without `unsafe-eval`) cannot answer the question at all, and a validator that read that
|
|
243
|
+
* as "invalid" would refuse every condition in the browser. Callers should treat it as a pass.
|
|
244
|
+
*
|
|
245
|
+
* @param expression the expression to check
|
|
246
|
+
* @returns `Valid: true` when it parses; `Valid: false` with `Error` when it definitely does
|
|
247
|
+
* not; `Valid: true` with `Undecidable: true` when this environment cannot compile
|
|
248
|
+
*/
|
|
249
|
+
validateSyntax(expression: string): {
|
|
250
|
+
Valid: boolean;
|
|
251
|
+
Error?: string;
|
|
252
|
+
Undecidable?: boolean;
|
|
253
|
+
};
|
|
129
254
|
/**
|
|
130
255
|
* Evaluates multiple expressions and returns all results
|
|
131
256
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SafeExpressionEvaluator.d.ts","sourceRoot":"","sources":["../src/SafeExpressionEvaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"SafeExpressionEvaluator.d.ts","sourceRoot":"","sources":["../src/SafeExpressionEvaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAuBH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAUjF,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAEpD,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAGtD,CAAC;AAEH;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,cAAc,EAAE,MAAM,CAAC;KAC1B,CAAC;CACL;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBAAa,uBAAuB;IAChC;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAK1C;IAEH;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAE7C;IAEH,+FAA+F;IAC/F,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAE7C;IAEH,oFAAoF;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAA8C;IAE7F;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAuD;IAEhG;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAoBlC;IAEF;;;;;;;;OAQG;IACI,QAAQ,CACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,iBAAiB,GAAE,OAAe,GACnC,0BAA0B;IAwE7B;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB;IA4C1B,sDAAsD;IACtD,OAAO,CAAC,SAAS;IAIjB;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAmEjB;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IA4B7B;;;;;;;OAOG;IACH,OAAO,CAAC,mBAAmB;IA2B3B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAUxB,wEAAwE;IACxE,OAAO,CAAC,kBAAkB;IAW1B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAkB1B;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAiBzB;;;;;;;;OAQG;IACH,OAAO,CAAC,uBAAuB;IAW/B;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAwDlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACI,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE;IAqBpG;;;;;;;OAOG;IACI,gBAAgB,CACnB,WAAW,EAAE,KAAK,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,EACvD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC;CAUhD;AAED;;GAEG;AACH,eAAO,MAAM,0BAA0B,yBAAgC,CAAC"}
|