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
package/src/messaging/sw.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* routing between the two planes is a single visibility decision.
|
|
13
13
|
*/
|
|
14
14
|
import type { FirebaseApp } from '../app/types.js';
|
|
15
|
+
import type { Sandbox, SandboxContext } from '../sandbox/index.js';
|
|
15
16
|
import {
|
|
16
17
|
defaultRegistration,
|
|
17
18
|
deliverToMessaging,
|
|
@@ -50,8 +51,8 @@ export type {
|
|
|
50
51
|
* service-worker plane (upstream: `getMessagingInSw`, component name
|
|
51
52
|
* `messaging-sw`).
|
|
52
53
|
*/
|
|
53
|
-
export function getMessaging(
|
|
54
|
-
return resolveMessaging('sw',
|
|
54
|
+
export function getMessaging(target?: FirebaseApp | Sandbox | SandboxContext): Messaging {
|
|
55
|
+
return resolveMessaging('sw', target);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric/performance` — a DEFERRED mirror of `firebase/performance`.
|
|
3
|
+
*
|
|
4
|
+
* Performance Monitoring instruments real network and render timings and
|
|
5
|
+
* ships them to Google. Nothing about it is locally observable, so it is
|
|
6
|
+
* deferred.
|
|
7
|
+
*
|
|
8
|
+
* Every symbol below resolves and links so an app that swaps `firebase` for
|
|
9
|
+
* `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
|
|
10
|
+
* subpath. See `../deferred/entry.ts` for the full rationale.
|
|
11
|
+
*
|
|
12
|
+
* The value list is the exact public runtime surface of `firebase/performance`
|
|
13
|
+
* (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
|
|
14
|
+
* real mirror.
|
|
15
|
+
*/
|
|
16
|
+
import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
|
|
17
|
+
|
|
18
|
+
export { PyricDeferredApiError } from '../deferred/entry.js';
|
|
19
|
+
|
|
20
|
+
export const {
|
|
21
|
+
getPerformance, initializePerformance, trace,
|
|
22
|
+
} = deferredEntry('performance');
|
|
23
|
+
|
|
24
|
+
// Type declarations. Aliased to the deferred placeholder so a consumer's own
|
|
25
|
+
// annotations keep type-checking: every deferred call returns `never`, which is
|
|
26
|
+
// assignable to any of these. Names that Firebase exports as a CLASS appear both
|
|
27
|
+
// here and above — a class is a value and a type, and both meanings must survive
|
|
28
|
+
// the swap.
|
|
29
|
+
export type FirebasePerformance = DeferredApi;
|
|
30
|
+
export type PerformanceSettings = DeferredApi;
|
|
31
|
+
export type PerformanceTrace = DeferredApi;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric/remote-config` — a DEFERRED mirror of `firebase/remote-config`.
|
|
3
|
+
*
|
|
4
|
+
* Remote Config serves server-side parameter values with fetch/activate
|
|
5
|
+
* caching semantics. Mirroring it needs a sandbox-side config store and a
|
|
6
|
+
* model of the fetch throttle — buildable, not yet built.
|
|
7
|
+
*
|
|
8
|
+
* Every symbol below resolves and links so an app that swaps `firebase` for
|
|
9
|
+
* `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
|
|
10
|
+
* subpath. See `../deferred/entry.ts` for the full rationale.
|
|
11
|
+
*
|
|
12
|
+
* The value list is the exact public runtime surface of `firebase/remote-config`
|
|
13
|
+
* (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
|
|
14
|
+
* real mirror.
|
|
15
|
+
*/
|
|
16
|
+
import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
|
|
17
|
+
|
|
18
|
+
export { PyricDeferredApiError } from '../deferred/entry.js';
|
|
19
|
+
|
|
20
|
+
export const {
|
|
21
|
+
activate, ensureInitialized, fetchAndActivate, fetchConfig, getAll, getBoolean, getNumber,
|
|
22
|
+
getRemoteConfig, getString, getValue, onConfigUpdate, setCustomSignals,
|
|
23
|
+
setLogLevel,
|
|
24
|
+
} = deferredEntry('remote-config');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one deferred symbol that answers instead of throwing: the real SDK
|
|
28
|
+
* resolves a boolean, and a deferred entry IS unsupported, so the standard
|
|
29
|
+
* `isSupported().then(ok => ok && get…(app))` guard must run — not crash.
|
|
30
|
+
*/
|
|
31
|
+
export const isSupported = async (): Promise<boolean> => false;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// Type declarations. Aliased to the deferred placeholder so a consumer's own
|
|
35
|
+
// annotations keep type-checking: every deferred call returns `never`, which is
|
|
36
|
+
// assignable to any of these. Names that Firebase exports as a CLASS appear both
|
|
37
|
+
// here and above — a class is a value and a type, and both meanings must survive
|
|
38
|
+
// the swap.
|
|
39
|
+
export type ConfigUpdate = DeferredApi;
|
|
40
|
+
export type ConfigUpdateObserver = DeferredApi;
|
|
41
|
+
export type CustomSignals = DeferredApi;
|
|
42
|
+
export type FetchResponse = DeferredApi;
|
|
43
|
+
export type FetchStatus = DeferredApi;
|
|
44
|
+
export type FetchType = DeferredApi;
|
|
45
|
+
export type FirebaseExperimentDescription = DeferredApi;
|
|
46
|
+
export type FirebaseRemoteConfigObject = DeferredApi;
|
|
47
|
+
export type LogLevel = DeferredApi;
|
|
48
|
+
export type RemoteConfig = DeferredApi;
|
|
49
|
+
export type RemoteConfigOptions = DeferredApi;
|
|
50
|
+
export type RemoteConfigSettings = DeferredApi;
|
|
51
|
+
export type Unsubscribe = DeferredApi;
|
|
52
|
+
export type Value = DeferredApi;
|
|
53
|
+
export type ValueSource = DeferredApi;
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
FirestoreRules, MatchBlock, AllowRule, FunctionDef, Expression, Operation,
|
|
3
3
|
} from './FirestoreAST.js';
|
|
4
4
|
import { RULES_BUILTIN_FUNCTIONS } from './builtin-functions.js';
|
|
5
|
+
import { countDocumentAccessCalls } from './document-access-count.js';
|
|
5
6
|
|
|
6
7
|
export interface ValidationFinding {
|
|
7
8
|
code: string;
|
|
@@ -26,8 +27,9 @@ export function validateFirestoreRules(ast: FirestoreRules): ValidationFinding[]
|
|
|
26
27
|
// SEC-4: Check for default deny
|
|
27
28
|
checkDefaultDeny(rootMatch, findings);
|
|
28
29
|
|
|
29
|
-
// QUA-3: Duplicate function names
|
|
30
|
-
|
|
30
|
+
// QUA-3: Duplicate function names in one scope (production compile
|
|
31
|
+
// rejection, so critical). Nested shadowing is legal and is not flagged.
|
|
32
|
+
checkDuplicateFunctions(ast, findings);
|
|
31
33
|
|
|
32
34
|
// QUA-4: Unused functions
|
|
33
35
|
checkUnusedFunctions(rootMatch, allCalls, findings);
|
|
@@ -124,12 +126,15 @@ function walkMatch(
|
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
// SEM-3:
|
|
128
|
-
|
|
129
|
+
// SEM-3: document access budget exceeded. Production allows exactly 10
|
|
130
|
+
// get/exists/getAfter/existsAfter reads per request evaluation; the
|
|
131
|
+
// 11th fails, so the finding fires only ABOVE 10 (same boundary as the
|
|
132
|
+
// linter's GET_COUNT error and the simulator's runtime LookupBudget).
|
|
133
|
+
const docReads = countDocumentAccessCalls(cond, fnMap);
|
|
129
134
|
if (docReads > 10) {
|
|
130
135
|
findings.push({
|
|
131
136
|
code: 'SEM-3', severity: 'high', path: pathStr, operation: opStr,
|
|
132
|
-
message: `Rule at ${pathStr}
|
|
137
|
+
message: `Rule at ${pathStr} may perform ${docReads} document access calls (get/exists/getAfter/existsAfter), which exceeds the 10-call budget`,
|
|
133
138
|
});
|
|
134
139
|
}
|
|
135
140
|
|
|
@@ -323,21 +328,6 @@ function referencesResourceData(expr: Expression): boolean {
|
|
|
323
328
|
);
|
|
324
329
|
}
|
|
325
330
|
|
|
326
|
-
function countDocReads(expr: Expression, functions: Map<string, FunctionDef>, visited = new Set<string>()): number {
|
|
327
|
-
let count = 0;
|
|
328
|
-
walkExpr(expr, e => {
|
|
329
|
-
if (e.type === 'functionCall' && (e.name === 'get' || e.name === 'exists' || e.name === 'getAfter')) {
|
|
330
|
-
count++;
|
|
331
|
-
}
|
|
332
|
-
// Follow user-defined function calls
|
|
333
|
-
if (e.type === 'functionCall' && functions.has(e.name) && !visited.has(e.name)) {
|
|
334
|
-
visited.add(e.name);
|
|
335
|
-
count += countDocReads(functions.get(e.name)!.body, functions, visited);
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
return count;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
331
|
function collectFunctionCalls(expr: Expression): string[] {
|
|
342
332
|
const calls: string[] = [];
|
|
343
333
|
walkExpr(expr, e => {
|
|
@@ -357,22 +347,48 @@ function collectAllFunctions(match: MatchBlock): Set<string> {
|
|
|
357
347
|
}
|
|
358
348
|
|
|
359
349
|
// ---- QUA-3: Duplicate functions ----
|
|
350
|
+
//
|
|
351
|
+
// Production REJECTS two declarations of one function name in the SAME
|
|
352
|
+
// scope at compile time, so this is severity 'critical': the write gate
|
|
353
|
+
// (`write/handler.ts`) blocks only critical validator findings, and
|
|
354
|
+
// 'medium' folded to a mere warning that deployed anyway.
|
|
355
|
+
//
|
|
356
|
+
// A scope is one declaration list: the global list above `service`, the
|
|
357
|
+
// list directly inside `service`, or one match block's own list. A nested
|
|
358
|
+
// match block that redeclares a name from an enclosing scope SHADOWS it and
|
|
359
|
+
// is legal: the simulator implements shadowing in
|
|
360
|
+
// `simulator/match-resolution.ts`, and the production capture in
|
|
361
|
+
// `conformance/rules-corpus/storage/function-scopes-and-shadowing.ts` pins
|
|
362
|
+
// inner-shadows-outer as accepted. Flagging that would block a ruleset
|
|
363
|
+
// production deploys.
|
|
364
|
+
//
|
|
365
|
+
// The code stays QUA-3 (already registered and asserted by consumers);
|
|
366
|
+
// `DUPLICATE_FUNCTION` was considered and rejected, because the module
|
|
367
|
+
// resolver already uses that name for its own error code
|
|
368
|
+
// (`modules/resolver-core.ts`), and colliding would make mixed issue
|
|
369
|
+
// lists ambiguous.
|
|
370
|
+
|
|
371
|
+
function checkDuplicateFunctions(ast: FirestoreRules, findings: ValidationFinding[]) {
|
|
372
|
+
checkDupsInScope(ast.functions ?? [], 'global scope', findings);
|
|
373
|
+
checkDupsInScope(ast.service.functions ?? [], `service ${ast.service.name}`, findings);
|
|
374
|
+
checkDupsInMatchScopes(ast.service.match, findings);
|
|
375
|
+
}
|
|
360
376
|
|
|
361
|
-
function
|
|
377
|
+
function checkDupsInMatchScopes(match: MatchBlock, findings: ValidationFinding[]) {
|
|
362
378
|
checkDupsInScope(match.functions, match.path.raw, findings);
|
|
363
|
-
for (const child of match.children)
|
|
379
|
+
for (const child of match.children) checkDupsInMatchScopes(child, findings);
|
|
364
380
|
}
|
|
365
381
|
|
|
366
|
-
function checkDupsInScope(fns: FunctionDef[],
|
|
367
|
-
const
|
|
382
|
+
function checkDupsInScope(fns: readonly FunctionDef[], scope: string, findings: ValidationFinding[]) {
|
|
383
|
+
const declared = new Set<string>();
|
|
368
384
|
for (const fn of fns) {
|
|
369
|
-
if (
|
|
385
|
+
if (declared.has(fn.name)) {
|
|
370
386
|
findings.push({
|
|
371
|
-
code: 'QUA-3', severity: '
|
|
372
|
-
message: `Duplicate function '${fn.name}' in scope at
|
|
387
|
+
code: 'QUA-3', severity: 'critical', path: scope,
|
|
388
|
+
message: `Duplicate function '${fn.name}' declared twice in ${scope}. Production rejects duplicate function declarations in one scope at compile time`,
|
|
373
389
|
});
|
|
374
390
|
}
|
|
375
|
-
|
|
391
|
+
declared.add(fn.name);
|
|
376
392
|
}
|
|
377
393
|
}
|
|
378
394
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Expression, FunctionDef } from './FirestoreAST.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The four built-in document access calls production counts against a
|
|
5
|
+
* request's 10-read budget.
|
|
6
|
+
*/
|
|
7
|
+
const DOCUMENT_ACCESS_BUILTINS = new Set(['get', 'exists', 'getAfter', 'existsAfter']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Count the document access calls (get/exists/getAfter/existsAfter)
|
|
11
|
+
* reachable from a rule condition, expanding each user-defined function
|
|
12
|
+
* once PER CALL SITE. `isOwner(a) && isOwner(b) && isOwner(c)` with three
|
|
13
|
+
* gets inside `isOwner` costs nine, matching production where each call
|
|
14
|
+
* performs its own reads (different arguments mean different paths).
|
|
15
|
+
* A function's `let` bindings evaluate on every call, so their reads count
|
|
16
|
+
* too. The call stack guards recursion only, and entries unwind on return,
|
|
17
|
+
* so sibling call sites each pay full price.
|
|
18
|
+
*
|
|
19
|
+
* This is a static over-approximation: production caches repeated reads of
|
|
20
|
+
* the SAME path within a request (see site-docs
|
|
21
|
+
* secure/firestore-rules-limits.md), but path identity is not decidable
|
|
22
|
+
* statically, so every call site is charged. The cache-aware, distinct-path
|
|
23
|
+
* count is the runtime concern of LookupBudget in
|
|
24
|
+
* simulator/lookup-budget.ts, which counts what an evaluation actually
|
|
25
|
+
* reads rather than what a condition could reach.
|
|
26
|
+
*
|
|
27
|
+
* This is the single walker behind both static consumers: the validator's
|
|
28
|
+
* SEM-3 finding and the linter's GET_COUNT warning.
|
|
29
|
+
*/
|
|
30
|
+
export function countDocumentAccessCalls(
|
|
31
|
+
expr: Expression,
|
|
32
|
+
functions: Map<string, FunctionDef>,
|
|
33
|
+
callStack: Set<string> = new Set<string>(),
|
|
34
|
+
): number {
|
|
35
|
+
let count = 0;
|
|
36
|
+
|
|
37
|
+
const visit = (e: Expression): void => {
|
|
38
|
+
switch (e.type) {
|
|
39
|
+
case 'functionCall':
|
|
40
|
+
if (DOCUMENT_ACCESS_BUILTINS.has(e.name)) {
|
|
41
|
+
count++;
|
|
42
|
+
} else {
|
|
43
|
+
const fn = functions.get(e.name);
|
|
44
|
+
const isExpandable = fn !== undefined && !callStack.has(e.name);
|
|
45
|
+
if (isExpandable) {
|
|
46
|
+
callStack.add(e.name);
|
|
47
|
+
for (const binding of fn!.lets) visit(binding.value);
|
|
48
|
+
visit(fn!.body);
|
|
49
|
+
callStack.delete(e.name); // unwind: count once per call site, not once per rule
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
e.args.forEach(visit);
|
|
53
|
+
break;
|
|
54
|
+
case 'binaryOp': visit(e.left); visit(e.right); break;
|
|
55
|
+
case 'unaryOp': visit(e.operand); break;
|
|
56
|
+
case 'ternary': visit(e.condition); visit(e.consequent); visit(e.alternate); break;
|
|
57
|
+
case 'methodCall': visit(e.object); e.args.forEach(visit); break;
|
|
58
|
+
case 'memberAccess': visit(e.object); break;
|
|
59
|
+
case 'bracketAccess': visit(e.object); visit(e.index); break;
|
|
60
|
+
case 'sliceAccess': visit(e.object); visit(e.start); visit(e.end); break;
|
|
61
|
+
case 'inExpr': visit(e.element); visit(e.collection); break;
|
|
62
|
+
case 'isExpr': visit(e.value); break;
|
|
63
|
+
case 'listLiteral': e.elements.forEach(visit); break;
|
|
64
|
+
case 'mapLiteral': e.entries.forEach((entry) => { visit(entry.key); visit(entry.value); }); break;
|
|
65
|
+
case 'pathLiteral':
|
|
66
|
+
for (const segment of e.segments) {
|
|
67
|
+
if (typeof segment !== 'string') visit(segment);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
visit(expr);
|
|
74
|
+
return count;
|
|
75
|
+
}
|
|
@@ -218,44 +218,10 @@ export function maxCallDepth(start: string, graph: Map<string, string[]>, visite
|
|
|
218
218
|
return 1 + max;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
/**
|
|
222
|
-
* Count get() and exists() calls reachable from an expression,
|
|
223
|
-
* following function calls transitively.
|
|
224
|
-
*/
|
|
225
|
-
export function countGetCalls(expr: Expression, functions: Map<string, FunctionDef>, visited = new Set<string>()): number {
|
|
226
|
-
let count = 0;
|
|
227
|
-
const walk = (e: Expression) => {
|
|
228
|
-
switch (e.type) {
|
|
229
|
-
case 'functionCall':
|
|
230
|
-
if (e.name === 'get' || e.name === 'exists') {
|
|
231
|
-
count++;
|
|
232
|
-
} else if (functions.has(e.name) && !visited.has(e.name)) {
|
|
233
|
-
visited.add(e.name);
|
|
234
|
-
const fn = functions.get(e.name)!;
|
|
235
|
-
walk(fn.body);
|
|
236
|
-
for (const b of fn.lets) walk(b.value);
|
|
237
|
-
}
|
|
238
|
-
e.args.forEach(walk);
|
|
239
|
-
break;
|
|
240
|
-
case 'binaryOp': walk(e.left); walk(e.right); break;
|
|
241
|
-
case 'unaryOp': walk(e.operand); break;
|
|
242
|
-
case 'methodCall': walk(e.object); e.args.forEach(walk); break;
|
|
243
|
-
case 'memberAccess': walk(e.object); break;
|
|
244
|
-
case 'bracketAccess': walk(e.object); walk(e.index); break;
|
|
245
|
-
case 'ternary': walk(e.condition); walk(e.consequent); walk(e.alternate); break;
|
|
246
|
-
case 'inExpr': walk(e.element); walk(e.collection); break;
|
|
247
|
-
case 'isExpr': walk(e.value); break;
|
|
248
|
-
case 'listLiteral': e.elements.forEach(walk); break;
|
|
249
|
-
case 'mapLiteral': e.entries.forEach(en => { walk(en.key); walk(en.value); }); break;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
walk(expr);
|
|
253
|
-
return count;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
221
|
/**
|
|
257
222
|
* Count how many times each user-defined function is called in an expression.
|
|
258
|
-
*
|
|
223
|
+
* Counts raw call-site occurrences at the top level only: it does not expand
|
|
224
|
+
* nested user-function bodies the way countDocumentAccessCalls does.
|
|
259
225
|
*/
|
|
260
226
|
export function countFunctionCallSites(expr: Expression, fnNames: Set<string>): Map<string, number> {
|
|
261
227
|
const counts = new Map<string, number>();
|
|
@@ -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
|
|