pyric 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +64 -89
- package/dist/ai/blocked.d.ts +52 -0
- package/dist/ai/blocked.d.ts.map +1 -0
- package/dist/ai/blocked.js +83 -0
- package/dist/ai/blocked.js.map +1 -0
- package/dist/ai/broker/broker.d.ts +49 -0
- package/dist/ai/broker/broker.d.ts.map +1 -1
- package/dist/ai/broker/broker.js +90 -0
- package/dist/ai/broker/broker.js.map +1 -1
- package/dist/ai/broker/gemini-engine.d.ts +14 -1
- package/dist/ai/broker/gemini-engine.d.ts.map +1 -1
- package/dist/ai/broker/gemini-engine.js +30 -9
- package/dist/ai/broker/gemini-engine.js.map +1 -1
- package/dist/ai/broker/index.d.ts +2 -2
- package/dist/ai/broker/index.d.ts.map +1 -1
- package/dist/ai/broker/index.js +2 -2
- package/dist/ai/broker/index.js.map +1 -1
- package/dist/ai/broker/openai-engine.d.ts +10 -1
- package/dist/ai/broker/openai-engine.d.ts.map +1 -1
- package/dist/ai/broker/openai-engine.js +26 -4
- package/dist/ai/broker/openai-engine.js.map +1 -1
- package/dist/ai/broker/synthesizer.d.ts +14 -0
- package/dist/ai/broker/synthesizer.d.ts.map +1 -1
- package/dist/ai/broker/synthesizer.js +31 -0
- package/dist/ai/broker/synthesizer.js.map +1 -1
- package/dist/ai/broker/types.d.ts +24 -0
- package/dist/ai/broker/types.d.ts.map +1 -1
- package/dist/ai/internal.d.ts +5 -0
- package/dist/ai/internal.d.ts.map +1 -1
- package/dist/ai/internal.js +5 -0
- package/dist/ai/internal.js.map +1 -1
- package/dist/ai/response-helpers.d.ts.map +1 -1
- package/dist/ai/response-helpers.js +6 -24
- package/dist/ai/response-helpers.js.map +1 -1
- package/dist/analytics/index.d.ts +40 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/index.js +25 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/app-check/index.d.ts +31 -0
- package/dist/app-check/index.d.ts.map +1 -0
- package/dist/app-check/index.js +20 -0
- package/dist/app-check/index.js.map +1 -0
- package/dist/database/controls.d.ts +1 -1
- package/dist/database/controls.js +1 -1
- package/dist/database/sandbox/rules-eval.d.ts +1 -1
- package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
- package/dist/database/sandbox/rules-eval.js +2 -2
- package/dist/database/sandbox/rules-eval.js.map +1 -1
- package/dist/database/sandbox/write-plane.js +2 -2
- package/dist/database/sandbox/write-plane.js.map +1 -1
- package/dist/database/sandbox-controls.d.ts +12 -5
- package/dist/database/sandbox-controls.d.ts.map +1 -1
- package/dist/database/sandbox-controls.js +20 -9
- package/dist/database/sandbox-controls.js.map +1 -1
- package/dist/deferred/entry.d.ts +75 -0
- package/dist/deferred/entry.d.ts.map +1 -0
- package/dist/deferred/entry.js +143 -0
- package/dist/deferred/entry.js.map +1 -0
- package/dist/firestore/lite.d.ts +72 -0
- package/dist/firestore/lite.d.ts.map +1 -0
- package/dist/firestore/lite.js +22 -0
- package/dist/firestore/lite.js.map +1 -0
- package/dist/firestore/persistence.d.ts +2 -2
- package/dist/firestore/persistence.js +1 -1
- package/dist/functions/index.d.ts +29 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +20 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/performance/index.d.ts +22 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +19 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/remote-config/index.d.ts +40 -0
- package/dist/remote-config/index.d.ts.map +1 -0
- package/dist/remote-config/index.js +25 -0
- package/dist/remote-config/index.js.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
- package/dist/rules/grammar/FirestoreValidator.js +43 -27
- package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
- package/dist/rules/grammar/document-access-count.d.ts +24 -0
- package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
- package/dist/rules/grammar/document-access-count.js +100 -0
- package/dist/rules/grammar/document-access-count.js.map +1 -0
- package/dist/rules/linter/ast-utils.d.ts +2 -6
- package/dist/rules/linter/ast-utils.d.ts.map +1 -1
- package/dist/rules/linter/ast-utils.js +2 -63
- package/dist/rules/linter/ast-utils.js.map +1 -1
- package/dist/rules/linter/hallucinations.d.ts +8 -0
- package/dist/rules/linter/hallucinations.d.ts.map +1 -1
- package/dist/rules/linter/hallucinations.js +121 -31
- package/dist/rules/linter/hallucinations.js.map +1 -1
- package/dist/rules/linter/linter.d.ts +10 -1
- package/dist/rules/linter/linter.d.ts.map +1 -1
- package/dist/rules/linter/linter.js +23 -9
- package/dist/rules/linter/linter.js.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.js +13 -13
- package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
- package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
- package/dist/rules/rtdb/simulation/handler.js +96 -35
- package/dist/rules/rtdb/simulation/handler.js.map +1 -1
- package/dist/rules/simulator/document-lookups.d.ts +4 -0
- package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
- package/dist/rules/simulator/document-lookups.js +42 -3
- package/dist/rules/simulator/document-lookups.js.map +1 -1
- package/dist/rules/simulator/eval-error.d.ts +11 -0
- package/dist/rules/simulator/eval-error.d.ts.map +1 -1
- package/dist/rules/simulator/eval-error.js +14 -0
- package/dist/rules/simulator/eval-error.js.map +1 -1
- package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
- package/dist/rules/simulator/evaluation-builtins.js +33 -8
- package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
- package/dist/rules/simulator/evaluation-context.d.ts +12 -0
- package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
- package/dist/rules/simulator/evaluator.d.ts +1 -1
- package/dist/rules/simulator/evaluator.d.ts.map +1 -1
- package/dist/rules/simulator/evaluator.js +16 -4
- package/dist/rules/simulator/evaluator.js.map +1 -1
- package/dist/rules/simulator/handler.d.ts.map +1 -1
- package/dist/rules/simulator/handler.js +52 -5
- package/dist/rules/simulator/handler.js.map +1 -1
- package/dist/rules/simulator/lookup-budget.d.ts +37 -0
- package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
- package/dist/rules/simulator/lookup-budget.js +96 -0
- package/dist/rules/simulator/lookup-budget.js.map +1 -0
- package/dist/rules/stdlib-modules.d.ts +7 -0
- package/dist/rules/stdlib-modules.d.ts.map +1 -1
- package/dist/rules/stdlib-modules.js +11 -11
- package/dist/rules/stdlib-modules.js.map +1 -1
- package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
- package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
- package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
- package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/persistence/types.d.ts +1 -1
- package/dist/sandbox/remote.d.ts +2 -2
- package/dist/sandbox/sandbox-context.d.ts +8 -1
- package/dist/sandbox/sandbox-context.d.ts.map +1 -1
- package/dist/sandbox/sandbox-context.js +59 -8
- package/dist/sandbox/sandbox-context.js.map +1 -1
- package/dist/sandbox/types/auth-state.d.ts +1 -0
- package/dist/sandbox/types/auth-state.d.ts.map +1 -1
- package/dist/sandbox/types/events.d.ts +2 -1
- package/dist/sandbox/types/events.d.ts.map +1 -1
- package/dist/sandbox/types/operation.d.ts +1 -0
- package/dist/sandbox/types/operation.d.ts.map +1 -1
- package/dist/storage/enforce.d.ts +20 -1
- package/dist/storage/enforce.d.ts.map +1 -1
- package/dist/storage/enforce.js +47 -14
- package/dist/storage/enforce.js.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/list.js +1 -1
- package/dist/storage/list.js.map +1 -1
- package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
- package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
- package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
- package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
- package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-evaluator.js +105 -33
- package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
- package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
- package/dist/storage/sandbox/rules-methods.js +17 -7
- package/dist/storage/sandbox/rules-methods.js.map +1 -1
- package/dist/storage/service.d.ts +23 -2
- package/dist/storage/service.d.ts.map +1 -1
- package/dist/storage/service.js +36 -26
- package/dist/storage/service.js.map +1 -1
- package/package.json +41 -1
- package/src/ai/blocked.ts +97 -0
- package/src/ai/broker/broker.ts +88 -0
- package/src/ai/broker/gemini-engine.ts +36 -9
- package/src/ai/broker/index.ts +2 -1
- package/src/ai/broker/openai-engine.ts +31 -3
- package/src/ai/broker/synthesizer.ts +34 -0
- package/src/ai/broker/types.ts +25 -0
- package/src/ai/internal.ts +5 -0
- package/src/ai/response-helpers.ts +6 -25
- package/src/analytics/index.ts +53 -0
- package/src/app/dispatch.test.ts +3 -1
- package/src/app-check/index.ts +41 -0
- package/src/database/controls.ts +1 -1
- package/src/database/sandbox/rules-eval.ts +2 -2
- package/src/database/sandbox/write-plane.ts +2 -2
- package/src/database/sandbox-controls.ts +29 -9
- package/src/deferred/entry.ts +167 -0
- package/src/firestore/lite.ts +91 -0
- package/src/firestore/persistence.ts +2 -2
- package/src/functions/index.ts +38 -0
- package/src/performance/index.ts +31 -0
- package/src/remote-config/index.ts +53 -0
- package/src/rules/grammar/FirestoreValidator.ts +44 -28
- package/src/rules/grammar/document-access-count.ts +75 -0
- package/src/rules/linter/ast-utils.ts +2 -36
- package/src/rules/linter/hallucinations.ts +123 -25
- package/src/rules/linter/linter.ts +32 -10
- package/src/rules/rtdb/grammar/simulator.ts +12 -13
- package/src/rules/rtdb/simulation/handler.ts +149 -46
- package/src/rules/simulator/document-lookups.ts +46 -3
- package/src/rules/simulator/eval-error.ts +16 -0
- package/src/rules/simulator/evaluation-builtins.ts +35 -8
- package/src/rules/simulator/evaluation-context.ts +12 -0
- package/src/rules/simulator/evaluator.ts +14 -3
- package/src/rules/simulator/handler.ts +68 -4
- package/src/rules/simulator/lookup-budget.ts +101 -0
- package/src/rules/stdlib-modules.ts +19 -11
- package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
- package/src/sandbox/index.ts +5 -1
- package/src/sandbox/persistence/types.ts +1 -1
- package/src/sandbox/remote.ts +2 -2
- package/src/sandbox/sandbox-context.ts +69 -9
- package/src/sandbox/types/auth-state.ts +1 -1
- package/src/sandbox/types/events.ts +2 -1
- package/src/sandbox/types/operation.ts +1 -1
- package/src/storage/enforce.ts +53 -14
- package/src/storage/index.ts +1 -1
- package/src/storage/list.ts +1 -1
- package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
- package/src/storage/sandbox/rules-evaluator.ts +109 -32
- package/src/storage/sandbox/rules-methods.ts +21 -7
- package/src/storage/service.ts +92 -27
|
@@ -95,7 +95,14 @@ const HALLUCINATED_GLOBALS: Record<string, string> = {
|
|
|
95
95
|
undefined: '`undefined` does not exist in rules — use `null`',
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The math-namespace functions production Firestore actually compiles.
|
|
100
|
+
* Notably ABSENT: `isInfinite`. Reference docs list it, but production
|
|
101
|
+
* rejects it at compile (`Function not found error: Name: [math.isInfinite]`).
|
|
102
|
+
* Exported so the stdlib-modules drift test can assert the documented
|
|
103
|
+
* catalog never re-grows a name this validator rejects.
|
|
104
|
+
*/
|
|
105
|
+
export const VALID_MATH_METHODS = new Set(['abs', 'ceil', 'floor', 'round', 'sqrt', 'pow', 'isNaN']);
|
|
99
106
|
|
|
100
107
|
|
|
101
108
|
/**
|
|
@@ -122,6 +129,21 @@ const WRONG_CONTEXT_PATHS: WrongPath[] = [
|
|
|
122
129
|
suggestion: '`resource.id` is not available — capture the document id with `/{docId}` in the match path' },
|
|
123
130
|
];
|
|
124
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Auth-token claims that are typed BOOL in production. Comparing one
|
|
134
|
+
* against a STRING literal compiles fine (CEL is dynamically typed) but
|
|
135
|
+
* the comparison is cross-type, so `== "true"` is always false and
|
|
136
|
+
* `!= "true"` is always true, and the latter silently opens the rule.
|
|
137
|
+
* WRONG_CONTEXT_PATHS can't express this: it matches `receiver.property`
|
|
138
|
+
* at depth 2, while token claims live at `request.auth.token.<claim>`
|
|
139
|
+
* and the bug is in the *comparison*, not the path itself.
|
|
140
|
+
*
|
|
141
|
+
* Only claims verifiably typed bool belong here. `email_verified` is the
|
|
142
|
+
* documented one (`request.auth.token.email_verified: bool`).
|
|
143
|
+
* `firebase.sign_in_provider` is a string, so it is deliberately NOT listed.
|
|
144
|
+
*/
|
|
145
|
+
const BOOL_TOKEN_CLAIMS = new Set(['email_verified']);
|
|
146
|
+
|
|
125
147
|
/**
|
|
126
148
|
* Built-in CEL methods that, when accessed without parentheses, are
|
|
127
149
|
* almost certainly a missing-call mistake. Restricted to a small
|
|
@@ -186,40 +208,51 @@ interface Loc {
|
|
|
186
208
|
ruleIndex?: number;
|
|
187
209
|
}
|
|
188
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Visit every expression in the match tree, carrying the set of
|
|
213
|
+
* user-defined function names visible where the expression sits. Scope
|
|
214
|
+
* accumulates outward-in: a block sees its own declarations plus every
|
|
215
|
+
* enclosing scope's, which is the resolution order the simulator
|
|
216
|
+
* implements in `simulator/match-resolution.ts`.
|
|
217
|
+
*/
|
|
189
218
|
function walkAllExpressions(
|
|
190
219
|
match: MatchBlock,
|
|
191
|
-
|
|
220
|
+
inheritedScope: ReadonlySet<string>,
|
|
221
|
+
visit: (expr: Expression, loc: Loc, scope: ReadonlySet<string>) => void,
|
|
192
222
|
) {
|
|
193
223
|
const path = match.path.raw;
|
|
224
|
+
const scope = new Set(inheritedScope);
|
|
225
|
+
for (const fn of match.functions) scope.add(fn.name);
|
|
194
226
|
for (const fn of match.functions) {
|
|
195
227
|
const loc: Loc = { matchPath: path, functionName: fn.name };
|
|
196
|
-
walkExpr(fn.body, loc, visit);
|
|
197
|
-
for (const b of fn.lets) walkExpr(b.value, loc, visit);
|
|
228
|
+
walkExpr(fn.body, loc, scope, visit);
|
|
229
|
+
for (const b of fn.lets) walkExpr(b.value, loc, scope, visit);
|
|
198
230
|
}
|
|
199
231
|
for (let i = 0; i < match.allows.length; i++) {
|
|
200
|
-
walkExpr(match.allows[i].condition, { matchPath: path, ruleIndex: i }, visit);
|
|
232
|
+
walkExpr(match.allows[i].condition, { matchPath: path, ruleIndex: i }, scope, visit);
|
|
201
233
|
}
|
|
202
|
-
for (const child of match.children) walkAllExpressions(child, visit);
|
|
234
|
+
for (const child of match.children) walkAllExpressions(child, scope, visit);
|
|
203
235
|
}
|
|
204
236
|
|
|
205
237
|
function walkExpr(
|
|
206
238
|
expr: Expression,
|
|
207
239
|
loc: Loc,
|
|
208
|
-
|
|
240
|
+
scope: ReadonlySet<string>,
|
|
241
|
+
visit: (expr: Expression, loc: Loc, scope: ReadonlySet<string>) => void,
|
|
209
242
|
) {
|
|
210
|
-
visit(expr, loc);
|
|
243
|
+
visit(expr, loc, scope);
|
|
211
244
|
switch (expr.type) {
|
|
212
|
-
case 'binaryOp': walkExpr(expr.left, loc, visit); walkExpr(expr.right, loc, visit); break;
|
|
213
|
-
case 'unaryOp': walkExpr(expr.operand, loc, visit); break;
|
|
214
|
-
case 'methodCall': walkExpr(expr.object, loc, visit); expr.args.forEach(a => walkExpr(a, loc, visit)); break;
|
|
215
|
-
case 'memberAccess': walkExpr(expr.object, loc, visit); break;
|
|
216
|
-
case 'bracketAccess': walkExpr(expr.object, loc, visit); walkExpr(expr.index, loc, visit); break;
|
|
217
|
-
case 'ternary': walkExpr(expr.condition, loc, visit); walkExpr(expr.consequent, loc, visit); walkExpr(expr.alternate, loc, visit); break;
|
|
218
|
-
case 'inExpr': walkExpr(expr.element, loc, visit); walkExpr(expr.collection, loc, visit); break;
|
|
219
|
-
case 'isExpr': walkExpr(expr.value, loc, visit); break;
|
|
220
|
-
case 'listLiteral': expr.elements.forEach(e => walkExpr(e, loc, visit)); break;
|
|
221
|
-
case 'mapLiteral': expr.entries.forEach(en => { walkExpr(en.key, loc, visit); walkExpr(en.value, loc, visit); }); break;
|
|
222
|
-
case 'functionCall': expr.args.forEach(a => walkExpr(a, loc, visit)); break;
|
|
245
|
+
case 'binaryOp': walkExpr(expr.left, loc, scope, visit); walkExpr(expr.right, loc, scope, visit); break;
|
|
246
|
+
case 'unaryOp': walkExpr(expr.operand, loc, scope, visit); break;
|
|
247
|
+
case 'methodCall': walkExpr(expr.object, loc, scope, visit); expr.args.forEach(a => walkExpr(a, loc, scope, visit)); break;
|
|
248
|
+
case 'memberAccess': walkExpr(expr.object, loc, scope, visit); break;
|
|
249
|
+
case 'bracketAccess': walkExpr(expr.object, loc, scope, visit); walkExpr(expr.index, loc, scope, visit); break;
|
|
250
|
+
case 'ternary': walkExpr(expr.condition, loc, scope, visit); walkExpr(expr.consequent, loc, scope, visit); walkExpr(expr.alternate, loc, scope, visit); break;
|
|
251
|
+
case 'inExpr': walkExpr(expr.element, loc, scope, visit); walkExpr(expr.collection, loc, scope, visit); break;
|
|
252
|
+
case 'isExpr': walkExpr(expr.value, loc, scope, visit); break;
|
|
253
|
+
case 'listLiteral': expr.elements.forEach(e => walkExpr(e, loc, scope, visit)); break;
|
|
254
|
+
case 'mapLiteral': expr.entries.forEach(en => { walkExpr(en.key, loc, scope, visit); walkExpr(en.value, loc, scope, visit); }); break;
|
|
255
|
+
case 'functionCall': expr.args.forEach(a => walkExpr(a, loc, scope, visit)); break;
|
|
223
256
|
}
|
|
224
257
|
}
|
|
225
258
|
|
|
@@ -231,9 +264,19 @@ function walkExpr(
|
|
|
231
264
|
* distinct (rule, key, location) tuple — duplicate identical patterns
|
|
232
265
|
* within the same rule are de-duplicated.
|
|
233
266
|
*/
|
|
234
|
-
|
|
267
|
+
/**
|
|
268
|
+
* A `debug(...)` call that resolves to NOTHING. Production has no built-in
|
|
269
|
+
* `debug`, but a ruleset is free to declare `function debug(v) { ... }`, and
|
|
270
|
+
* that call resolves and evaluates like any other user function, so only an
|
|
271
|
+
* unresolved call is a finding.
|
|
272
|
+
*/
|
|
273
|
+
function isUnresolvedDebugCall(
|
|
274
|
+
expr: Expression,
|
|
275
|
+
scope: ReadonlySet<string>,
|
|
276
|
+
allowDebug?: boolean,
|
|
277
|
+
): boolean {
|
|
235
278
|
if (allowDebug) return false;
|
|
236
|
-
return expr.type === 'functionCall' && expr.name === 'debug';
|
|
279
|
+
return expr.type === 'functionCall' && expr.name === 'debug' && !scope.has('debug');
|
|
237
280
|
}
|
|
238
281
|
|
|
239
282
|
function isHallucinatedMethodCall(expr: Expression): boolean {
|
|
@@ -275,6 +318,37 @@ function isWrongContextPath(expr: Expression): { suggestion: string } | undefine
|
|
|
275
318
|
return WRONG_CONTEXT_PATHS.find(p => p.receiver === recv && p.property === prop);
|
|
276
319
|
}
|
|
277
320
|
|
|
321
|
+
/** Is `expr` a member access spelling exactly `request.auth.token.<claim>`
|
|
322
|
+
* for a claim in BOOL_TOKEN_CLAIMS? */
|
|
323
|
+
function boolTokenClaimName(expr: Expression): string | undefined {
|
|
324
|
+
if (expr.type !== 'memberAccess' || !BOOL_TOKEN_CLAIMS.has(expr.property)) return undefined;
|
|
325
|
+
const token = expr.object;
|
|
326
|
+
if (token.type !== 'memberAccess' || token.property !== 'token') return undefined;
|
|
327
|
+
const auth = token.object;
|
|
328
|
+
if (auth.type !== 'memberAccess' || auth.property !== 'auth') return undefined;
|
|
329
|
+
return auth.object.type === 'identifier' && auth.object.name === 'request'
|
|
330
|
+
? expr.property
|
|
331
|
+
: undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* `request.auth.token.email_verified == "true"` (or `!=`, either operand
|
|
336
|
+
* order). The claim is a bool; a string literal can never equal it, so the
|
|
337
|
+
* comparison is a constant: `==` always denies, `!=` always allows.
|
|
338
|
+
*/
|
|
339
|
+
function boolTokenClaimStringComparison(
|
|
340
|
+
expr: Expression,
|
|
341
|
+
): { claim: string; op: string; literal: string } | undefined {
|
|
342
|
+
if (expr.type !== 'binaryOp' || (expr.op !== '==' && expr.op !== '!=')) return undefined;
|
|
343
|
+
for (const [side, other] of [[expr.left, expr.right], [expr.right, expr.left]] as const) {
|
|
344
|
+
const claim = boolTokenClaimName(side);
|
|
345
|
+
if (claim && other.type === 'literal' && typeof other.value === 'string') {
|
|
346
|
+
return { claim, op: expr.op, literal: other.value };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
278
352
|
function isLengthPropertyAccessOnMethod(expr: Expression): boolean {
|
|
279
353
|
return expr.type === 'memberAccess' && expr.property === 'length' && expr.object.type === 'methodCall';
|
|
280
354
|
}
|
|
@@ -301,14 +375,23 @@ export function checkHallucinations(ast: FirestoreRules, options: { allowDebug?:
|
|
|
301
375
|
warnings.push(w);
|
|
302
376
|
}
|
|
303
377
|
|
|
304
|
-
|
|
305
|
-
|
|
378
|
+
// Functions declared above `service` and directly inside it are visible
|
|
379
|
+
// everywhere below, so they seed the scope the walk carries down.
|
|
380
|
+
const outerScope = new Set<string>();
|
|
381
|
+
for (const fn of ast.functions ?? []) outerScope.add(fn.name);
|
|
382
|
+
for (const fn of ast.service.functions ?? []) outerScope.add(fn.name);
|
|
383
|
+
|
|
384
|
+
walkAllExpressions(ast.service.match, outerScope, (expr, loc, scope) => {
|
|
385
|
+
if (isUnresolvedDebugCall(expr, scope, options.allowDebug)) {
|
|
306
386
|
emit('HALLUCINATED_GLOBAL', 'debug', loc, {
|
|
307
387
|
rule: 'HALLUCINATED_GLOBAL',
|
|
308
388
|
severity: 'error',
|
|
309
|
-
message:
|
|
389
|
+
message:
|
|
390
|
+
'`debug()` is not a Firestore rules function and no function named `debug` is declared in scope. '
|
|
391
|
+
+ 'Production rejects the whole ruleset at compile time with '
|
|
392
|
+
+ '`Function not found error: Name: [debug]`, so nothing in the file deploys.',
|
|
310
393
|
location: loc,
|
|
311
|
-
fix: 'Remove debug()
|
|
394
|
+
fix: 'Remove the debug() call and evaluate the inner expression directly.',
|
|
312
395
|
});
|
|
313
396
|
}
|
|
314
397
|
|
|
@@ -400,6 +483,21 @@ export function checkHallucinations(ast: FirestoreRules, options: { allowDebug?:
|
|
|
400
483
|
}
|
|
401
484
|
}
|
|
402
485
|
|
|
486
|
+
const boolClaim = boolTokenClaimStringComparison(expr);
|
|
487
|
+
if (boolClaim) {
|
|
488
|
+
const { claim, op, literal } = boolClaim;
|
|
489
|
+
emit('BOOL_TOKEN_CLAIM', `${claim}|${op}|${literal}`, loc, {
|
|
490
|
+
rule: 'BOOL_TOKEN_CLAIM',
|
|
491
|
+
severity: 'error',
|
|
492
|
+
message:
|
|
493
|
+
`\`request.auth.token.${claim}\` is a bool, but it is compared against the string ` +
|
|
494
|
+
`"${literal}", a cross-type comparison that is always ${op === '==' ? 'false (rule always denies)' : 'true (rule silently allows)'}. ` +
|
|
495
|
+
`Compare against the boolean literal instead: \`request.auth.token.${claim} ${op} true\`.`,
|
|
496
|
+
location: loc,
|
|
497
|
+
fix: `Drop the quotes: \`request.auth.token.${claim} ${op} ${literal === 'false' ? 'false' : 'true'}\`.`,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
403
501
|
if (isLengthPropertyAccessOnMethod(expr)) {
|
|
404
502
|
emit('LENGTH_PROPERTY', '', loc, {
|
|
405
503
|
rule: 'LENGTH_PROPERTY',
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
extractFirstExpression,
|
|
19
19
|
buildCallGraph,
|
|
20
20
|
maxCallDepth,
|
|
21
|
-
countGetCalls,
|
|
22
21
|
countFunctionCallSites,
|
|
23
22
|
functionContainsGet,
|
|
24
23
|
referencesRequestTime,
|
|
@@ -26,6 +25,7 @@ import {
|
|
|
26
25
|
collectAllRules,
|
|
27
26
|
} from './ast-utils.js';
|
|
28
27
|
import { checkSyntaxHints, checkHallucinations } from './hallucinations.js';
|
|
28
|
+
import { countDocumentAccessCalls } from '../grammar/document-access-count.js';
|
|
29
29
|
|
|
30
30
|
// ═══ Types ═══
|
|
31
31
|
|
|
@@ -93,7 +93,11 @@ const THRESHOLDS = {
|
|
|
93
93
|
CALL_DEPTH_WARN: 6,
|
|
94
94
|
CALL_DEPTH_ERROR: 10,
|
|
95
95
|
GET_COUNT_WARN: 5,
|
|
96
|
-
|
|
96
|
+
// Production allows EXACTLY 10 document access calls per request
|
|
97
|
+
// evaluation; the 11th fails (site-docs secure/firestore-rules-limits.md).
|
|
98
|
+
// Error fires strictly ABOVE this value, the same boundary as SEM-3 and
|
|
99
|
+
// the simulator's runtime LookupBudget.
|
|
100
|
+
GET_COUNT_ERROR: 10,
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
// ═══ Lint Rules ═══
|
|
@@ -350,12 +354,14 @@ function checkGetCount(
|
|
|
350
354
|
for (const fn of allFunctions) fnMap.set(fn.name, fn);
|
|
351
355
|
|
|
352
356
|
for (let i = 0; i < rules.length; i++) {
|
|
353
|
-
const count =
|
|
354
|
-
|
|
357
|
+
const count = countDocumentAccessCalls(rules[i].rule.condition, fnMap);
|
|
358
|
+
// Error only ABOVE the limit: production allows exactly 10 and fails
|
|
359
|
+
// the 11th, so a rule at exactly 10 is legal (still worth the WARN).
|
|
360
|
+
if (count > THRESHOLDS.GET_COUNT_ERROR) {
|
|
355
361
|
warnings.push({
|
|
356
362
|
rule: 'GET_COUNT',
|
|
357
363
|
severity: 'error',
|
|
358
|
-
message: `Rule #${i} may invoke ${count} get()/exists() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
|
|
364
|
+
message: `Rule #${i} may invoke ${count} get()/exists()/getAfter()/existsAfter() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
|
|
359
365
|
location: { ruleIndex: i },
|
|
360
366
|
fix: 'Cache get() results via a config() wrapper function. Same-path calls are cached by Firestore.',
|
|
361
367
|
});
|
|
@@ -363,7 +369,7 @@ function checkGetCount(
|
|
|
363
369
|
warnings.push({
|
|
364
370
|
rule: 'GET_COUNT',
|
|
365
371
|
severity: 'warning',
|
|
366
|
-
message: `Rule #${i} invokes ${count} get()/exists() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
|
|
372
|
+
message: `Rule #${i} invokes ${count} get()/exists()/getAfter()/existsAfter() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
|
|
367
373
|
location: { ruleIndex: i },
|
|
368
374
|
});
|
|
369
375
|
}
|
|
@@ -767,7 +773,16 @@ export interface LintOptions {
|
|
|
767
773
|
* block linting of a valid current ruleset.
|
|
768
774
|
*/
|
|
769
775
|
previousSource?: string;
|
|
770
|
-
/**
|
|
776
|
+
/**
|
|
777
|
+
* Set to true ONLY when linting a scratch ruleset that will never be
|
|
778
|
+
* deployed and an unresolved `debug()` call should be tolerated.
|
|
779
|
+
* Production Firestore rejects a ruleset that calls `debug()` at compile
|
|
780
|
+
* time (`Function not found error: Name: [debug]`), so the linter rejects
|
|
781
|
+
* it by default, including when `testCases` is supplied. A ruleset that
|
|
782
|
+
* declares its own `function debug(...)` is never flagged, with or
|
|
783
|
+
* without this flag. This flag is an explicit caller choice; it is never
|
|
784
|
+
* inferred from other options.
|
|
785
|
+
*/
|
|
771
786
|
allowDebug?: boolean;
|
|
772
787
|
}
|
|
773
788
|
|
|
@@ -836,8 +851,15 @@ export function lintFirestoreRules(source: string, options: LintOptions = {}): L
|
|
|
836
851
|
// Rule 8: Get duplication (same get()-containing function called multiple times)
|
|
837
852
|
checkGetDuplication(allRules, allFunctions, warnings);
|
|
838
853
|
|
|
839
|
-
// Rule 9: Hallucinations
|
|
840
|
-
|
|
854
|
+
// Rule 9: Hallucinations, JS-style code that parses but fails at runtime.
|
|
855
|
+
// `allowDebug` is an EXPLICIT caller opt-in only. It used to be implied by
|
|
856
|
+
// a non-empty `testCases` array, which silently disabled the debug()
|
|
857
|
+
// rejection in any lint run that also carried a test suite, exactly the
|
|
858
|
+
// authoring path that feeds the write gate. Production rejects debug() at
|
|
859
|
+
// compile time (`Function not found error: Name: [debug]`), so the default
|
|
860
|
+
// must reject; a caller linting a ruleset that will never deploy can still
|
|
861
|
+
// pass `allowDebug: true`.
|
|
862
|
+
warnings.push(...checkHallucinations(ast, { allowDebug: options.allowDebug }));
|
|
841
863
|
|
|
842
864
|
// Rule 9.5: Always-true predicates and recursive-wildcard open rules.
|
|
843
865
|
// Severity: error so deployRules refuses to swap. The agent's #1
|
|
@@ -884,7 +906,7 @@ export function lintFirestoreRules(source: string, options: LintOptions = {}): L
|
|
|
884
906
|
let maxExprs = 0;
|
|
885
907
|
let maxGets = 0;
|
|
886
908
|
for (const r of allRules) {
|
|
887
|
-
const gets =
|
|
909
|
+
const gets = countDocumentAccessCalls(r.rule.condition, fnMap);
|
|
888
910
|
if (gets > maxGets) maxGets = gets;
|
|
889
911
|
}
|
|
890
912
|
|
|
@@ -115,23 +115,21 @@ export class DataSnapshot {
|
|
|
115
115
|
|
|
116
116
|
child(path: string): DataSnapshot {
|
|
117
117
|
const parts = path.split('/').filter(p => p.length > 0);
|
|
118
|
-
|
|
119
|
-
let currentPath = this._path;
|
|
118
|
+
if (parts.length === 0) return this;
|
|
120
119
|
|
|
120
|
+
let current: unknown = this._value;
|
|
121
121
|
for (const part of parts) {
|
|
122
|
-
if (current
|
|
122
|
+
if (current !== null && current !== undefined && typeof current === 'object') {
|
|
123
|
+
current = Object.hasOwn(current as object, part)
|
|
124
|
+
? ((current as Record<string, unknown>)[part] ?? null)
|
|
125
|
+
: null;
|
|
126
|
+
} else {
|
|
123
127
|
current = null;
|
|
124
|
-
currentPath = `${currentPath}/${part}`.replace(/\/+/g, '/');
|
|
125
|
-
break;
|
|
126
128
|
}
|
|
127
|
-
// Own-property access only: a `__proto__`/`constructor` child must
|
|
128
|
-
// not resolve to the JS prototype chain.
|
|
129
|
-
current = Object.hasOwn(current as object, part)
|
|
130
|
-
? ((current as Record<string, unknown>)[part] ?? null)
|
|
131
|
-
: null;
|
|
132
|
-
currentPath = `${currentPath === '/' ? '' : currentPath}/${part}`;
|
|
133
129
|
}
|
|
134
130
|
|
|
131
|
+
const prefix = this._path === '/' ? '' : this._path;
|
|
132
|
+
const currentPath = `${prefix}/${parts.join('/')}`;
|
|
135
133
|
return new DataSnapshot(current, currentPath, this._root);
|
|
136
134
|
}
|
|
137
135
|
|
|
@@ -139,9 +137,10 @@ export class DataSnapshot {
|
|
|
139
137
|
if (this._path === '/') return null;
|
|
140
138
|
const parts = this._path.split('/').filter(p => p.length > 0);
|
|
141
139
|
parts.pop();
|
|
142
|
-
|
|
140
|
+
if (parts.length === 0) {
|
|
141
|
+
return new DataSnapshot(this._root, '/');
|
|
142
|
+
}
|
|
143
143
|
const rootSnap = new DataSnapshot(this._root, '/');
|
|
144
|
-
if (parentPath === '/') return rootSnap;
|
|
145
144
|
return rootSnap.child(parts.join('/'));
|
|
146
145
|
}
|
|
147
146
|
|
|
@@ -58,6 +58,23 @@ function snapshotChildKeys(snap: DataSnapshot): string[] {
|
|
|
58
58
|
* the first unsupported node, else `null` when every applicable `.validate`
|
|
59
59
|
* passes.
|
|
60
60
|
*/
|
|
61
|
+
function hasValidateRule(n: RtdbNode): boolean {
|
|
62
|
+
if (n.validate) return true;
|
|
63
|
+
return n.children.some(hasValidateRule);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function shouldValidateSiblingSubtree(
|
|
67
|
+
currentSegments: readonly string[],
|
|
68
|
+
hasLocalDeletion: boolean,
|
|
69
|
+
allWritePaths: readonly string[][],
|
|
70
|
+
): boolean {
|
|
71
|
+
return (
|
|
72
|
+
currentSegments.length > 0 ||
|
|
73
|
+
hasLocalDeletion ||
|
|
74
|
+
allWritePaths.some((wp) => wp.length === 1)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
function findFailingValidate(
|
|
62
79
|
node: RtdbNode,
|
|
63
80
|
data: DataSnapshot,
|
|
@@ -65,31 +82,47 @@ function findFailingValidate(
|
|
|
65
82
|
bindings: Record<string, string>,
|
|
66
83
|
buildContext: ContextBuilder,
|
|
67
84
|
pathToWrite: string[],
|
|
85
|
+
updates?: readonly { path: string; value?: unknown }[],
|
|
68
86
|
): ValidateFailure | null {
|
|
69
87
|
let firstUnsupported: ValidateFailure | null = null;
|
|
70
88
|
|
|
89
|
+
const writePathStrings = new Set<string>();
|
|
90
|
+
if (updates && updates.length > 0) {
|
|
91
|
+
for (const u of updates) {
|
|
92
|
+
writePathStrings.add(u.path.split('/').filter(Boolean).join('/'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
writePathStrings.add(pathToWrite.filter(Boolean).join('/'));
|
|
96
|
+
const allWritePaths = Array.from(writePathStrings).map((p) => p.split('/').filter(Boolean));
|
|
97
|
+
|
|
71
98
|
function walk(
|
|
72
99
|
node: RtdbNode,
|
|
73
100
|
data: DataSnapshot,
|
|
74
101
|
newData: DataSnapshot,
|
|
75
102
|
bindings: Record<string, string>,
|
|
76
|
-
|
|
103
|
+
currentSegments: string[],
|
|
104
|
+
isUnderModifiedSubtree: boolean,
|
|
77
105
|
): ValidateFailure | null {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
if (newData.exists()) {
|
|
107
|
+
const rule = node.validate;
|
|
108
|
+
if (rule) {
|
|
109
|
+
if (!rule.parsed.valid) {
|
|
110
|
+
if (!firstUnsupported) {
|
|
111
|
+
firstUnsupported = { node, rule, bindings, unsupported: true };
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const result = evaluateRtdbExpression(rule.raw, buildContext(data, newData, bindings));
|
|
115
|
+
if (!result) return { node, rule, bindings };
|
|
86
116
|
}
|
|
87
|
-
} else {
|
|
88
|
-
const result = evaluateRtdbExpression(rule.raw, buildContext(data, newData, bindings));
|
|
89
|
-
if (!result) return { node, rule, bindings };
|
|
90
117
|
}
|
|
91
118
|
}
|
|
92
119
|
|
|
120
|
+
const isAtOrBelowWriteTarget = allWritePaths.some(
|
|
121
|
+
(wp) => wp.length <= currentSegments.length && wp.every((seg, idx) => seg === currentSegments[idx]),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const hasLocalDeletion = snapshotChildKeys(data).some((k) => !newData.child(k).exists());
|
|
125
|
+
|
|
93
126
|
for (const child of node.children) {
|
|
94
127
|
const childSegments = child.path.split('/').filter(Boolean);
|
|
95
128
|
if (childSegments.length === 0) continue;
|
|
@@ -97,49 +130,118 @@ function findFailingValidate(
|
|
|
97
130
|
const lastSegment = childSegments[childSegments.length - 1];
|
|
98
131
|
const isPathVar = lastSegment.startsWith('$');
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
if (isAtOrBelowWriteTarget || isUnderModifiedSubtree) {
|
|
134
|
+
if (isPathVar) {
|
|
135
|
+
for (const key of snapshotChildKeys(newData)) {
|
|
136
|
+
const failure = walk(
|
|
137
|
+
child,
|
|
138
|
+
data.child(key),
|
|
139
|
+
newData.child(key),
|
|
140
|
+
{ ...bindings, [lastSegment]: key },
|
|
141
|
+
[...currentSegments, key],
|
|
142
|
+
true,
|
|
143
|
+
);
|
|
144
|
+
if (failure) return failure;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
if (newData.child(lastSegment).exists()) {
|
|
148
|
+
const failure = walk(
|
|
149
|
+
child,
|
|
150
|
+
data.child(lastSegment),
|
|
151
|
+
newData.child(lastSegment),
|
|
152
|
+
bindings,
|
|
153
|
+
[...currentSegments, lastSegment],
|
|
154
|
+
true,
|
|
155
|
+
);
|
|
156
|
+
if (failure) return failure;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// We are at an ancestor of write path(s)
|
|
161
|
+
if (isPathVar) {
|
|
162
|
+
const writeKeys = new Set<string>();
|
|
163
|
+
for (const wp of allWritePaths) {
|
|
164
|
+
if (
|
|
165
|
+
wp.length > currentSegments.length &&
|
|
166
|
+
currentSegments.every((seg, idx) => seg === wp[idx])
|
|
167
|
+
) {
|
|
168
|
+
writeKeys.add(wp[currentSegments.length]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const key of writeKeys) {
|
|
172
|
+
const failure = walk(
|
|
173
|
+
child,
|
|
174
|
+
data.child(key),
|
|
175
|
+
newData.child(key),
|
|
176
|
+
{ ...bindings, [lastSegment]: key },
|
|
177
|
+
[...currentSegments, key],
|
|
178
|
+
false,
|
|
179
|
+
);
|
|
180
|
+
if (failure) return failure;
|
|
181
|
+
}
|
|
113
182
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
183
|
+
if (
|
|
184
|
+
hasValidateRule(child) &&
|
|
185
|
+
shouldValidateSiblingSubtree(currentSegments, hasLocalDeletion, allWritePaths)
|
|
186
|
+
) {
|
|
187
|
+
for (const key of snapshotChildKeys(newData)) {
|
|
188
|
+
if (writeKeys.has(key)) continue;
|
|
189
|
+
const failure = walk(
|
|
190
|
+
child,
|
|
191
|
+
data.child(key),
|
|
192
|
+
newData.child(key),
|
|
193
|
+
{ ...bindings, [lastSegment]: key },
|
|
194
|
+
[...currentSegments, key],
|
|
195
|
+
true,
|
|
196
|
+
);
|
|
197
|
+
if (failure) return failure;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const key = lastSegment;
|
|
202
|
+
const isChildOnWritePath = allWritePaths.some(
|
|
203
|
+
(wp) =>
|
|
204
|
+
wp.length > currentSegments.length &&
|
|
205
|
+
wp[currentSegments.length] === key &&
|
|
206
|
+
currentSegments.every((seg, idx) => seg === wp[idx]),
|
|
124
207
|
);
|
|
125
|
-
|
|
208
|
+
|
|
209
|
+
if (isChildOnWritePath) {
|
|
210
|
+
const failure = walk(
|
|
211
|
+
child,
|
|
212
|
+
data.child(key),
|
|
213
|
+
newData.child(key),
|
|
214
|
+
bindings,
|
|
215
|
+
[...currentSegments, key],
|
|
216
|
+
false,
|
|
217
|
+
);
|
|
218
|
+
if (failure) return failure;
|
|
219
|
+
} else {
|
|
220
|
+
// Sibling branch
|
|
221
|
+
if (
|
|
222
|
+
newData.child(key).exists() &&
|
|
223
|
+
hasValidateRule(child) &&
|
|
224
|
+
shouldValidateSiblingSubtree(currentSegments, hasLocalDeletion, allWritePaths)
|
|
225
|
+
) {
|
|
226
|
+
const failure = walk(
|
|
227
|
+
child,
|
|
228
|
+
data.child(key),
|
|
229
|
+
newData.child(key),
|
|
230
|
+
bindings,
|
|
231
|
+
[...currentSegments, key],
|
|
232
|
+
true,
|
|
233
|
+
);
|
|
234
|
+
if (failure) return failure;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
126
237
|
}
|
|
127
|
-
} else {
|
|
128
|
-
const failure = walk(
|
|
129
|
-
child,
|
|
130
|
-
data.child(lastSegment),
|
|
131
|
-
newData.child(lastSegment),
|
|
132
|
-
bindings,
|
|
133
|
-
[],
|
|
134
|
-
);
|
|
135
|
-
if (failure) return failure;
|
|
136
238
|
}
|
|
137
239
|
}
|
|
138
240
|
|
|
139
241
|
return null;
|
|
140
242
|
}
|
|
141
243
|
|
|
142
|
-
const realFailure = walk(node, data, newData, bindings,
|
|
244
|
+
const realFailure = walk(node, data, newData, bindings, [], false);
|
|
143
245
|
return realFailure ?? firstUnsupported;
|
|
144
246
|
}
|
|
145
247
|
|
|
@@ -343,6 +445,7 @@ export class SimulateHandler {
|
|
|
343
445
|
{},
|
|
344
446
|
buildContext,
|
|
345
447
|
pathSegments,
|
|
448
|
+
updates,
|
|
346
449
|
);
|
|
347
450
|
if (failure) {
|
|
348
451
|
return {
|