pyric 0.1.0-alpha.19 → 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 +50 -0
- package/dist/ai/broker/broker.d.ts.map +1 -1
- package/dist/ai/broker/broker.js +157 -21
- package/dist/ai/broker/broker.js.map +1 -1
- package/dist/ai/broker/gemini-engine.d.ts +56 -0
- package/dist/ai/broker/gemini-engine.d.ts.map +1 -0
- package/dist/ai/broker/gemini-engine.js +235 -0
- package/dist/ai/broker/gemini-engine.js.map +1 -0
- package/dist/ai/broker/index.d.ts +2 -1
- package/dist/ai/broker/index.d.ts.map +1 -1
- package/dist/ai/broker/index.js +2 -1
- 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 +29 -0
- package/dist/ai/broker/types.d.ts.map +1 -1
- package/dist/ai/instances.js +1 -1
- package/dist/ai/instances.js.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/auth/instances.js +1 -1
- package/dist/auth/instances.js.map +1 -1
- package/dist/database/controls.d.ts +1 -1
- package/dist/database/controls.js +1 -1
- package/dist/database/instances.js +1 -1
- package/dist/database/instances.js.map +1 -1
- package/dist/database/sandbox/backend.d.ts +1 -0
- package/dist/database/sandbox/backend.d.ts.map +1 -1
- package/dist/database/sandbox/backend.js +1 -0
- package/dist/database/sandbox/backend.js.map +1 -1
- package/dist/database/sandbox/rules-eval.d.ts +6 -2
- package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
- package/dist/database/sandbox/rules-eval.js +15 -2
- package/dist/database/sandbox/rules-eval.js.map +1 -1
- package/dist/database/sandbox/write-plane.d.ts +1 -0
- package/dist/database/sandbox/write-plane.d.ts.map +1 -1
- package/dist/database/sandbox/write-plane.js +7 -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/database/sandbox-namespace.d.ts +19 -1
- package/dist/database/sandbox-namespace.d.ts.map +1 -1
- package/dist/database/sandbox-namespace.js +24 -1
- package/dist/database/sandbox-namespace.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/instances.js +1 -1
- package/dist/firestore/instances.js.map +1 -1
- 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/firestore/sandbox-controls.d.ts +2 -0
- package/dist/firestore/sandbox-controls.d.ts.map +1 -1
- package/dist/firestore/sandbox-controls.js +8 -0
- package/dist/firestore/sandbox-controls.js.map +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/messaging/client.d.ts +2 -1
- package/dist/messaging/client.d.ts.map +1 -1
- package/dist/messaging/client.js +2 -2
- package/dist/messaging/client.js.map +1 -1
- package/dist/messaging/instance.d.ts +2 -2
- package/dist/messaging/instance.d.ts.map +1 -1
- package/dist/messaging/instance.js +40 -6
- package/dist/messaging/instance.js.map +1 -1
- package/dist/messaging/sw.d.ts +2 -1
- package/dist/messaging/sw.d.ts.map +1 -1
- package/dist/messaging/sw.js +2 -2
- package/dist/messaging/sw.js.map +1 -1
- 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/RtdbExpr.ohm +5 -2
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts +1 -1
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -1
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js +5 -2
- package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
- package/dist/rules/rtdb/grammar/simulator.js +61 -15
- 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/persistence.d.ts.map +1 -1
- package/dist/storage/persistence.js +66 -14
- package/dist/storage/persistence.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 +37 -26
- package/dist/storage/service.js.map +1 -1
- package/package.json +42 -2
- package/src/ai/blocked.ts +97 -0
- package/src/ai/broker/broker.ts +153 -22
- package/src/ai/broker/gemini-engine.ts +311 -0
- package/src/ai/broker/index.ts +2 -0
- package/src/ai/broker/openai-engine.ts +31 -3
- package/src/ai/broker/synthesizer.ts +34 -0
- package/src/ai/broker/types.ts +27 -1
- package/src/ai/instances.ts +1 -1
- 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/auth/instances.ts +1 -1
- package/src/database/controls.ts +1 -1
- package/src/database/instances.ts +1 -1
- package/src/database/sandbox/backend.ts +1 -0
- package/src/database/sandbox/rules-eval.ts +18 -2
- package/src/database/sandbox/write-plane.ts +8 -2
- package/src/database/sandbox-controls.ts +29 -9
- package/src/database/sandbox-namespace.ts +28 -1
- package/src/deferred/entry.ts +167 -0
- package/src/firestore/instances.ts +1 -1
- package/src/firestore/lite.ts +91 -0
- package/src/firestore/persistence.ts +2 -2
- package/src/firestore/sandbox-controls.ts +10 -0
- package/src/functions/index.ts +38 -0
- package/src/messaging/client.ts +3 -2
- package/src/messaging/instance.ts +44 -7
- package/src/messaging/sw.ts +3 -2
- 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/RtdbExpr.ohm +5 -2
- package/src/rules/rtdb/grammar/RtdbExpr.ohm.generated.ts +5 -2
- package/src/rules/rtdb/grammar/simulator.ts +41 -15
- 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/persistence.ts +74 -16
- 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 +93 -27
- package/README.md.orig +0 -53
|
@@ -59,8 +59,11 @@ RtdbExpr {
|
|
|
59
59
|
| null
|
|
60
60
|
|
|
61
61
|
number
|
|
62
|
-
= digit+ "." digit+ -- float
|
|
63
|
-
| digit+ --
|
|
62
|
+
= digit+ "." digit+ exponent? -- float
|
|
63
|
+
| digit+ exponent -- exp
|
|
64
|
+
| digit+ -- int
|
|
65
|
+
|
|
66
|
+
exponent = ("e" | "E") ("+" | "-")? digit+
|
|
64
67
|
|
|
65
68
|
string
|
|
66
69
|
= "\"" doubleStringChar* "\"" -- double
|
|
@@ -63,8 +63,11 @@ export const RTDB_EXPR_OHM_SOURCE = `RtdbExpr {
|
|
|
63
63
|
| null
|
|
64
64
|
|
|
65
65
|
number
|
|
66
|
-
= digit+ "." digit+ -- float
|
|
67
|
-
| digit+ --
|
|
66
|
+
= digit+ "." digit+ exponent? -- float
|
|
67
|
+
| digit+ exponent -- exp
|
|
68
|
+
| digit+ -- int
|
|
69
|
+
|
|
70
|
+
exponent = ("e" | "E") ("+" | "-")? digit+
|
|
68
71
|
|
|
69
72
|
string
|
|
70
73
|
= "\\"" doubleStringChar* "\\"" -- double
|
|
@@ -29,6 +29,32 @@ function safeMemberRead(recv: unknown, key: string): unknown {
|
|
|
29
29
|
return (recv as Record<string, unknown>)[key] ?? null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** Process standard escape sequences in single-quoted string literals. */
|
|
33
|
+
function processStringEscapes(raw: string): string {
|
|
34
|
+
let out = '';
|
|
35
|
+
for (let i = 0; i < raw.length; i++) {
|
|
36
|
+
const c = raw[i];
|
|
37
|
+
if (c !== '\\' || i === raw.length - 1) {
|
|
38
|
+
out += c;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const next = raw[++i];
|
|
42
|
+
switch (next) {
|
|
43
|
+
case '\\': out += '\\'; break;
|
|
44
|
+
case '\'': out += '\''; break;
|
|
45
|
+
case '"': out += '"'; break;
|
|
46
|
+
case 'n': out += '\n'; break;
|
|
47
|
+
case 'r': out += '\r'; break;
|
|
48
|
+
case 't': out += '\t'; break;
|
|
49
|
+
case 'b': out += '\b'; break;
|
|
50
|
+
case 'f': out += '\f'; break;
|
|
51
|
+
case '/': out += '/'; break;
|
|
52
|
+
default: out += '\\' + next; break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
export interface EvalContext {
|
|
33
59
|
auth: SimulatedAuth | null;
|
|
34
60
|
data: DataSnapshot;
|
|
@@ -89,23 +115,21 @@ export class DataSnapshot {
|
|
|
89
115
|
|
|
90
116
|
child(path: string): DataSnapshot {
|
|
91
117
|
const parts = path.split('/').filter(p => p.length > 0);
|
|
92
|
-
|
|
93
|
-
let currentPath = this._path;
|
|
118
|
+
if (parts.length === 0) return this;
|
|
94
119
|
|
|
120
|
+
let current: unknown = this._value;
|
|
95
121
|
for (const part of parts) {
|
|
96
|
-
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 {
|
|
97
127
|
current = null;
|
|
98
|
-
currentPath = `${currentPath}/${part}`.replace(/\/+/g, '/');
|
|
99
|
-
break;
|
|
100
128
|
}
|
|
101
|
-
// Own-property access only: a `__proto__`/`constructor` child must
|
|
102
|
-
// not resolve to the JS prototype chain.
|
|
103
|
-
current = Object.hasOwn(current as object, part)
|
|
104
|
-
? ((current as Record<string, unknown>)[part] ?? null)
|
|
105
|
-
: null;
|
|
106
|
-
currentPath = `${currentPath === '/' ? '' : currentPath}/${part}`;
|
|
107
129
|
}
|
|
108
130
|
|
|
131
|
+
const prefix = this._path === '/' ? '' : this._path;
|
|
132
|
+
const currentPath = `${prefix}/${parts.join('/')}`;
|
|
109
133
|
return new DataSnapshot(current, currentPath, this._root);
|
|
110
134
|
}
|
|
111
135
|
|
|
@@ -113,9 +137,10 @@ export class DataSnapshot {
|
|
|
113
137
|
if (this._path === '/') return null;
|
|
114
138
|
const parts = this._path.split('/').filter(p => p.length > 0);
|
|
115
139
|
parts.pop();
|
|
116
|
-
|
|
140
|
+
if (parts.length === 0) {
|
|
141
|
+
return new DataSnapshot(this._root, '/');
|
|
142
|
+
}
|
|
117
143
|
const rootSnap = new DataSnapshot(this._root, '/');
|
|
118
|
-
if (parentPath === '/') return rootSnap;
|
|
119
144
|
return rootSnap.child(parts.join('/'));
|
|
120
145
|
}
|
|
121
146
|
|
|
@@ -289,7 +314,8 @@ function getEvalSemantics(): Semantics {
|
|
|
289
314
|
|
|
290
315
|
literal(node) { return (node as any).eval(); },
|
|
291
316
|
|
|
292
|
-
number_float(_int, _dot, _frac) { return parseFloat(this.sourceString); },
|
|
317
|
+
number_float(_int, _dot, _frac, _exp) { return parseFloat(this.sourceString); },
|
|
318
|
+
number_exp(_digits, _exp) { return parseFloat(this.sourceString); },
|
|
293
319
|
number_int(_digits) { return parseInt(this.sourceString, 10); },
|
|
294
320
|
number(node) { return (node as any).eval(); },
|
|
295
321
|
|
|
@@ -297,7 +323,7 @@ function getEvalSemantics(): Semantics {
|
|
|
297
323
|
return JSON.parse(this.sourceString);
|
|
298
324
|
},
|
|
299
325
|
string_single(_open, chars, _close) {
|
|
300
|
-
return chars.
|
|
326
|
+
return processStringEscapes(chars.sourceString);
|
|
301
327
|
},
|
|
302
328
|
string(node) { return (node as any).eval(); },
|
|
303
329
|
|
|
@@ -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 {
|
|
@@ -2,10 +2,43 @@ import type { SimulationContext, SimResource } from './evaluation-context.js';
|
|
|
2
2
|
import { EvalError } from './eval-error.js';
|
|
3
3
|
import { Path } from './wrappers/path.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Check if a path or segment list points to a document (non-empty, even segment count).
|
|
7
|
+
*/
|
|
8
|
+
export function isDocumentPath(segmentsOrPath: string | readonly string[]): boolean {
|
|
9
|
+
const segments = typeof segmentsOrPath === 'string'
|
|
10
|
+
? segmentsOrPath.split('/').filter(Boolean)
|
|
11
|
+
: segmentsOrPath;
|
|
12
|
+
return segments.length > 0 && segments.length % 2 === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
export function normalizeDocumentPath(rawPath: string): string {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
let cleaned = rawPath.replace(/\$\(database\)/g, '(default)');
|
|
17
|
+
const dbPrefix = '/databases/(default)/documents/';
|
|
18
|
+
if (cleaned.startsWith(dbPrefix)) {
|
|
19
|
+
cleaned = cleaned.slice(dbPrefix.length);
|
|
20
|
+
} else if (cleaned.startsWith('/databases/(default)/documents')) {
|
|
21
|
+
cleaned = cleaned.slice('/databases/(default)/documents'.length);
|
|
22
|
+
}
|
|
23
|
+
if (cleaned.startsWith('/')) {
|
|
24
|
+
cleaned = cleaned.slice(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rawSegments = cleaned.split('/').filter((s) => s.length > 0 && s !== '.');
|
|
28
|
+
const stack: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (const seg of rawSegments) {
|
|
31
|
+
if (seg === '..') {
|
|
32
|
+
if (stack.length > 1) {
|
|
33
|
+
stack.pop();
|
|
34
|
+
}
|
|
35
|
+
// When stack.length <= 1, .. is clamped to prevent escaping collection or document root.
|
|
36
|
+
} else {
|
|
37
|
+
stack.push(seg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return stack.join('/');
|
|
9
42
|
}
|
|
10
43
|
|
|
11
44
|
/** Build the identity-bearing value returned by a real document lookup. */
|
|
@@ -20,6 +53,12 @@ export function makeGetResource(relPath: string, data: Record<string, unknown>):
|
|
|
20
53
|
|
|
21
54
|
export function resolveGet(rawPath: string, context: SimulationContext): SimResource {
|
|
22
55
|
const path = normalizeDocumentPath(rawPath);
|
|
56
|
+
const segments = path.split('/').filter(Boolean);
|
|
57
|
+
if (!isDocumentPath(segments)) {
|
|
58
|
+
throw new EvalError(
|
|
59
|
+
`get() requires a path pointing to a document (even segment count), got '${path}'`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
23
62
|
let document = context.mockDocuments.get(path);
|
|
24
63
|
if (!document && context.getDoc) {
|
|
25
64
|
const loaded = context.getDoc(path);
|
|
@@ -38,6 +77,10 @@ export function resolveGet(rawPath: string, context: SimulationContext): SimReso
|
|
|
38
77
|
|
|
39
78
|
export function resolveExists(rawPath: string, context: SimulationContext): boolean {
|
|
40
79
|
const path = normalizeDocumentPath(rawPath);
|
|
80
|
+
const segments = path.split('/').filter(Boolean);
|
|
81
|
+
if (!isDocumentPath(segments)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
41
84
|
if (context.mockDocuments.has(path)) return true;
|
|
42
85
|
if (context.getDoc) {
|
|
43
86
|
const loaded = context.getDoc(path);
|
|
@@ -6,3 +6,19 @@ export class EvalError extends Error {
|
|
|
6
6
|
this.name = 'EvalError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A resource limit the whole request ran into: the per-request document
|
|
12
|
+
* access budget today, and any future per-request cap. Production fails the
|
|
13
|
+
* entire request closed on these instead of producing a CEL error value, so
|
|
14
|
+
* they are never absorbed by a determining `&&`/`||` operand and never
|
|
15
|
+
* confined to the allow rule that raised them.
|
|
16
|
+
*/
|
|
17
|
+
export class ResourceLimitError extends EvalError {
|
|
18
|
+
constructor(message: string, expr?: Expression) {
|
|
19
|
+
super(message, expr);
|
|
20
|
+
this.name = 'ResourceLimitError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { EvalError as RuleEvalError };
|
|
@@ -11,7 +11,8 @@ import { Path } from './wrappers/path.js';
|
|
|
11
11
|
import { RulesFloat } from './wrappers/float.js';
|
|
12
12
|
import { EvalError } from './eval-error.js';
|
|
13
13
|
import { UnsupportedError } from './unsupported-error.js';
|
|
14
|
-
import { makeGetResource, normalizeDocumentPath, resolveExists, resolveGet } from './document-lookups.js';
|
|
14
|
+
import { isDocumentPath, makeGetResource, normalizeDocumentPath, resolveExists, resolveGet } from './document-lookups.js';
|
|
15
|
+
import { chargeLookup } from './lookup-budget.js';
|
|
15
16
|
import type { SimulationContext } from './evaluation-context.js';
|
|
16
17
|
import { evaluate, isKnownGlobal, resolveIdentifier } from './evaluator.js';
|
|
17
18
|
import { evaluateHashingMethod } from './hashing-builtins.js';
|
|
@@ -53,10 +54,14 @@ export function evaluateFunctionCall(
|
|
|
53
54
|
switch (name) {
|
|
54
55
|
case 'get': {
|
|
55
56
|
const path = String(evaluate(args[0], ctx, scope));
|
|
57
|
+
// Charge the per-request lookup budget BEFORE resolving so the 11th
|
|
58
|
+
// distinct access fails closed even when the doc would resolve.
|
|
59
|
+
chargeLookup(ctx, 'doc', path);
|
|
56
60
|
return resolveGet(path, ctx);
|
|
57
61
|
}
|
|
58
62
|
case 'exists': {
|
|
59
63
|
const path = String(evaluate(args[0], ctx, scope));
|
|
64
|
+
chargeLookup(ctx, 'doc', path); // exists() shares get()'s per-path slot
|
|
60
65
|
return resolveExists(path, ctx);
|
|
61
66
|
}
|
|
62
67
|
case 'getAfter': {
|
|
@@ -71,15 +76,27 @@ export function evaluateFunctionCall(
|
|
|
71
76
|
// doesn't touch.
|
|
72
77
|
const argVal = evaluate(args[0], ctx, scope);
|
|
73
78
|
const pathStr = String(argVal);
|
|
79
|
+
const normalized = normalizeDocumentPath(pathStr);
|
|
80
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
81
|
+
if (!isDocumentPath(segments)) {
|
|
82
|
+
throw new EvalError(
|
|
83
|
+
`getAfter() requires a path pointing to a document (even segment count), got '${normalized}'`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
// getAfter() is a document access call and counts against the
|
|
87
|
+
// per-request budget. 'after' keying: the post-write projection is a
|
|
88
|
+
// different snapshot than get()'s pre-write read, so a path read by
|
|
89
|
+
// both charges two slots. The fall-through resolveGet below does NOT
|
|
90
|
+
// re-charge, because charging lives only here.
|
|
91
|
+
chargeLookup(ctx, 'after', normalized);
|
|
74
92
|
if (pathStr === ctx.afterStatePath.toString()) {
|
|
75
93
|
// RULES-B8: getAfter() of a doc that won't exist post-write (delete,
|
|
76
94
|
// or projected-null) ERRORS like get() — guard with existsAfter().
|
|
77
95
|
if (ctx.afterState === null) {
|
|
78
96
|
throw new EvalError(`getAfter() of non-existent document '${pathStr}' (guard with existsAfter() first)`);
|
|
79
97
|
}
|
|
80
|
-
return makeGetResource(
|
|
98
|
+
return makeGetResource(normalized, ctx.afterState);
|
|
81
99
|
}
|
|
82
|
-
const normalized = normalizeDocumentPath(pathStr);
|
|
83
100
|
if (ctx.batchProjection?.has(normalized)) {
|
|
84
101
|
const projected = ctx.batchProjection.get(normalized)!;
|
|
85
102
|
if (projected === null) {
|
|
@@ -96,19 +113,29 @@ export function evaluateFunctionCall(
|
|
|
96
113
|
// through to exists().
|
|
97
114
|
const argVal = evaluate(args[0], ctx, scope);
|
|
98
115
|
const pathStr = String(argVal);
|
|
116
|
+
const normalized = normalizeDocumentPath(pathStr);
|
|
117
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
118
|
+
if (!isDocumentPath(segments)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// existsAfter() counts against the per-request lookup budget and
|
|
122
|
+
// shares getAfter()'s post-write slot for the same path.
|
|
123
|
+
chargeLookup(ctx, 'after', normalized);
|
|
99
124
|
if (pathStr === ctx.afterStatePath.toString()) {
|
|
100
125
|
return ctx.existsAfter;
|
|
101
126
|
}
|
|
102
|
-
const normalized = normalizeDocumentPath(pathStr);
|
|
103
127
|
if (ctx.batchProjection?.has(normalized)) {
|
|
104
128
|
return ctx.batchProjection.get(normalized) !== null;
|
|
105
129
|
}
|
|
106
130
|
return resolveExists(pathStr, ctx);
|
|
107
131
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
// NOTE: `debug()` is deliberately NOT handled here. Production Firestore
|
|
133
|
+
// rejects it at compile ("Function not found error: Name: [debug]", the
|
|
134
|
+
// captured acceptance probe for firestore.function.debug), so the local
|
|
135
|
+
// evaluator must fail it the same way. Falling through to the
|
|
136
|
+
// user-defined lookup below yields `EvalError('Unknown function: debug')`,
|
|
137
|
+
// the exact message the conformance rejection-signature normalizer maps
|
|
138
|
+
// to `function-not-found:debug`.
|
|
112
139
|
// RULES-B5 / RULES-B6: type-conversion builtins follow CEL's strict
|
|
113
140
|
// semantics, not JS coercion. `String()` routes a RulesFloat through its
|
|
114
141
|
// `.toString()` so `string(1.0)` → "1.0" (decimal preserved); bare ints
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FunctionDef } from '../grammar/FirestoreAST.js';
|
|
2
|
+
import type { LookupBudget } from './lookup-budget.js';
|
|
2
3
|
import type { TraceRecorder } from './trace-recorder.js';
|
|
3
4
|
import { Path } from './wrappers/path.js';
|
|
4
5
|
import { Timestamp } from './wrappers/timestamp.js';
|
|
@@ -108,6 +109,17 @@ export interface SimulationContext {
|
|
|
108
109
|
* through to `get()` exactly as before.
|
|
109
110
|
*/
|
|
110
111
|
batchProjection?: Map<string, Record<string, unknown> | null>;
|
|
112
|
+
/**
|
|
113
|
+
* Per-request document access budget (production: 10 distinct
|
|
114
|
+
* get/exists/getAfter/existsAfter reads per single-document request; the
|
|
115
|
+
* 11th fails the evaluation closed). The handler creates one per test
|
|
116
|
+
* case and shares it across every match block and allow rule evaluated
|
|
117
|
+
* for that request; a fresh budget per test case is the "resets between
|
|
118
|
+
* requests" semantics. Optional so non-handler evaluation paths are
|
|
119
|
+
* unaffected. See LookupBudget in lookup-budget.ts for the counting
|
|
120
|
+
* semantics and the in-repo production evidence.
|
|
121
|
+
*/
|
|
122
|
+
lookupBudget?: LookupBudget;
|
|
111
123
|
/** Optional per-rule expression-trace recorder. When set, the evaluator
|
|
112
124
|
* wraps every `evaluate()` call and records the sub-expression tree;
|
|
113
125
|
* when absent (the default), the evaluator is unchanged. The handler
|
|
@@ -22,10 +22,10 @@ import { Timestamp } from './wrappers/timestamp.js';
|
|
|
22
22
|
import { Bytes } from './wrappers/bytes.js';
|
|
23
23
|
import { Path } from './wrappers/path.js';
|
|
24
24
|
import { RulesFloat } from './wrappers/float.js';
|
|
25
|
-
import { EvalError } from './eval-error.js';
|
|
25
|
+
import { EvalError, ResourceLimitError } from './eval-error.js';
|
|
26
26
|
import { UnsupportedError } from './unsupported-error.js';
|
|
27
27
|
|
|
28
|
-
export { EvalError } from './eval-error.js';
|
|
28
|
+
export { EvalError, EvalError as RuleEvalError } from './eval-error.js';
|
|
29
29
|
export { UnsupportedError } from './unsupported-error.js';
|
|
30
30
|
|
|
31
31
|
// ═══ Simulation Context ═══
|
|
@@ -71,7 +71,9 @@ function evaluateExpr(expr: Expression, ctx: SimulationContext, scope: Record<st
|
|
|
71
71
|
return evaluateBinaryOp(expr.op, expr.left, expr.right, ctx, scope);
|
|
72
72
|
|
|
73
73
|
case 'unaryOp': {
|
|
74
|
-
if (expr.op === '!')
|
|
74
|
+
if (expr.op === '!') {
|
|
75
|
+
return !requireBoolean(evaluate(expr.operand, ctx, scope), expr.operand);
|
|
76
|
+
}
|
|
75
77
|
if (expr.op === '-') {
|
|
76
78
|
const v = evaluate(expr.operand, ctx, scope);
|
|
77
79
|
// RULES-B5: negating a float stays a float (`-1.5 is float`); negating
|
|
@@ -363,6 +365,13 @@ function evaluateBinaryOp(
|
|
|
363
365
|
if (op === '&&') {
|
|
364
366
|
let lv: unknown, lErr: unknown;
|
|
365
367
|
try { lv = requireBoolean(evaluate(left, ctx, scope), left); } catch (e) { lErr = e; }
|
|
368
|
+
// A resource-limit failure (the document access budget) fails the WHOLE
|
|
369
|
+
// request, it is not a CEL error value: a determining RHS must not
|
|
370
|
+
// absorb it into an ALLOW. Fail closed immediately.
|
|
371
|
+
// TODO(unverified): confirm against a live production capture that the
|
|
372
|
+
// 11th-access error is not absorbable. The Rules Test API does not
|
|
373
|
+
// enforce the budget, so the capture has to be a deployed ruleset.
|
|
374
|
+
if (lErr instanceof ResourceLimitError) throw lErr;
|
|
366
375
|
if (lErr === undefined && lv === false) {
|
|
367
376
|
// LHS is false → determines the result; RHS need not be evaluated.
|
|
368
377
|
ctx.trace?.skip(right);
|
|
@@ -380,6 +389,8 @@ function evaluateBinaryOp(
|
|
|
380
389
|
if (op === '||') {
|
|
381
390
|
let lv: unknown, lErr: unknown;
|
|
382
391
|
try { lv = requireBoolean(evaluate(left, ctx, scope), left); } catch (e) { lErr = e; }
|
|
392
|
+
// See the `&&` branch: a resource-limit failure is not absorbable.
|
|
393
|
+
if (lErr instanceof ResourceLimitError) throw lErr;
|
|
383
394
|
if (lErr === undefined && lv === true) {
|
|
384
395
|
// LHS is true → determines the result; RHS need not be evaluated.
|
|
385
396
|
ctx.trace?.skip(right);
|