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
|
@@ -27,32 +27,84 @@ import { SandboxError } from './types/errors.js';
|
|
|
27
27
|
import type { Sandbox } from './types/service.js';
|
|
28
28
|
import { immutableOperationContext } from './operation-record.js';
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Normalize AuthState during SandboxContextImpl context assembly.
|
|
32
|
+
* Automatically projects top-level `tenant` into `token.firebase.tenant`
|
|
33
|
+
* while preserving custom claims and explicit nested token overrides.
|
|
34
|
+
* Never mutates original input.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeAuthState(auth: AuthState): AuthState {
|
|
37
|
+
if (auth === null) return null;
|
|
38
|
+
|
|
39
|
+
if (auth.tenant === undefined) {
|
|
40
|
+
if (auth.token === undefined) {
|
|
41
|
+
return { uid: auth.uid };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
uid: auth.uid,
|
|
45
|
+
token: structuredClone(auth.token),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const token = auth.token !== undefined ? structuredClone(auth.token) : {};
|
|
50
|
+
const existingFirebase = token.firebase;
|
|
51
|
+
|
|
52
|
+
let normalizedFirebase: Record<string, unknown>;
|
|
53
|
+
if (
|
|
54
|
+
typeof existingFirebase === 'object' &&
|
|
55
|
+
existingFirebase !== null &&
|
|
56
|
+
!Array.isArray(existingFirebase)
|
|
57
|
+
) {
|
|
58
|
+
const fbObj = existingFirebase as Record<string, unknown>;
|
|
59
|
+
normalizedFirebase = {
|
|
60
|
+
...fbObj,
|
|
61
|
+
tenant: fbObj.tenant !== undefined ? fbObj.tenant : auth.tenant,
|
|
62
|
+
};
|
|
63
|
+
} else {
|
|
64
|
+
normalizedFirebase = { tenant: auth.tenant };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
token.firebase = normalizedFirebase;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
uid: auth.uid,
|
|
71
|
+
tenant: auth.tenant,
|
|
72
|
+
token,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
30
76
|
function authLensFor(auth: AuthState): AuthLens {
|
|
31
77
|
if (auth === null) return { mode: 'anon' };
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
const lens: Extract<AuthLens, { mode: 'as' }> = { mode: 'as', uid: auth.uid };
|
|
79
|
+
if (auth.token !== undefined) lens.token = auth.token;
|
|
80
|
+
if (auth.tenant !== undefined) lens.tenant = auth.tenant;
|
|
81
|
+
return lens;
|
|
35
82
|
}
|
|
36
83
|
|
|
37
84
|
export class SandboxContextImpl implements SandboxContext {
|
|
85
|
+
public readonly auth: AuthState;
|
|
86
|
+
public readonly operationContext: OperationContext;
|
|
87
|
+
|
|
38
88
|
constructor(
|
|
39
89
|
public readonly sandbox: Sandbox,
|
|
40
|
-
|
|
90
|
+
auth: AuthState,
|
|
41
91
|
operationContext?: OperationContext,
|
|
42
92
|
) {
|
|
93
|
+
validateAuthState(auth);
|
|
94
|
+
const normalized = normalizeAuthState(auth);
|
|
95
|
+
this.auth = normalized;
|
|
43
96
|
this.operationContext = immutableOperationContext(operationContext ?? {
|
|
44
97
|
source: { kind: 'unattributed' },
|
|
45
|
-
authLens: authLensFor(
|
|
98
|
+
authLens: authLensFor(normalized),
|
|
46
99
|
});
|
|
47
100
|
}
|
|
48
101
|
|
|
49
|
-
public readonly operationContext: OperationContext;
|
|
50
|
-
|
|
51
102
|
withAuth(auth: AuthState): SandboxContext {
|
|
52
103
|
validateAuthState(auth);
|
|
53
|
-
|
|
104
|
+
const normalized = normalizeAuthState(auth);
|
|
105
|
+
return new SandboxContextImpl(this.sandbox, normalized, {
|
|
54
106
|
source: this.operationContext.source,
|
|
55
|
-
authLens: authLensFor(
|
|
107
|
+
authLens: authLensFor(normalized),
|
|
56
108
|
...(this.operationContext.planId === undefined
|
|
57
109
|
? {}
|
|
58
110
|
: { planId: this.operationContext.planId }),
|
|
@@ -106,6 +158,14 @@ export function validateAuthState(auth: unknown): asserts auth is AuthState {
|
|
|
106
158
|
'withAuth() requires `uid` to be a non-empty string.',
|
|
107
159
|
);
|
|
108
160
|
}
|
|
161
|
+
if ('tenant' in obj && obj.tenant !== undefined) {
|
|
162
|
+
if (typeof obj.tenant !== 'string' || obj.tenant.length === 0) {
|
|
163
|
+
throw new SandboxError(
|
|
164
|
+
'invalid-argument',
|
|
165
|
+
'withAuth() requires `tenant` (when present) to be a non-empty string.',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
109
169
|
if ('token' in obj && obj.token !== undefined) {
|
|
110
170
|
if (obj.token === null || typeof obj.token !== 'object' || Array.isArray(obj.token)) {
|
|
111
171
|
throw new SandboxError(
|
|
@@ -379,7 +379,8 @@ export interface ServiceMutationEvent {
|
|
|
379
379
|
* - storage: `object_put` | `object_delete` | `metadata_update`
|
|
380
380
|
* - rtdb: `set` | `update` | `remove` | `transaction`
|
|
381
381
|
* - ai: `generate_content` | `stream_generate_content` |
|
|
382
|
-
* `count_tokens` | `request_rejected`
|
|
382
|
+
* `count_tokens` | `request_rejected` | `response_blocked` |
|
|
383
|
+
* `model_substituted`
|
|
383
384
|
* New ops can be added without a breaking change (consumers switch with a
|
|
384
385
|
* default branch).
|
|
385
386
|
*/
|
|
@@ -15,7 +15,7 @@ export type EventActor =
|
|
|
15
15
|
/** The identity/rules lens an operation actually ran under. */
|
|
16
16
|
export type AuthLens =
|
|
17
17
|
| { mode: 'admin' }
|
|
18
|
-
| { mode: 'as'; uid: string; token?: Record<string, unknown> }
|
|
18
|
+
| { mode: 'as'; uid: string; tenant?: string; token?: Record<string, unknown> }
|
|
19
19
|
| { mode: 'app-session' }
|
|
20
20
|
| { mode: 'anon' };
|
|
21
21
|
|
package/src/storage/enforce.ts
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
* grow their own copy of the same dispatch.
|
|
5
5
|
*
|
|
6
6
|
* Behavior:
|
|
7
|
-
* - No rules configured →
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* pre-Slice-8 surface.
|
|
7
|
+
* - No rules configured → deny. Matches Firebase production fail-closed
|
|
8
|
+
* security invariants: bare `getStorage` with no `rules` option
|
|
9
|
+
* rejects client operations with `storage/unauthorized`.
|
|
11
10
|
* - Rules configured → call `evaluateStorageRules`. Throw
|
|
12
11
|
* `storage/unauthorized` with the evaluator's `reasons` joined
|
|
13
12
|
* into the message on denial.
|
|
@@ -16,9 +15,8 @@
|
|
|
16
15
|
* permission governs both download and list, so `list.ts` enforces a
|
|
17
16
|
* `read` check on the scanned prefix path. A previous v1 scope build
|
|
18
17
|
* silently bypassed list — that contradicted the rules-enforcement
|
|
19
|
-
* contract (a denied tree was still enumerable).
|
|
20
|
-
* configured the check
|
|
21
|
-
* session-archive demo is unaffected.
|
|
18
|
+
* contract (a denied tree was still enumerable). When no rules are
|
|
19
|
+
* configured the check fails closed (default-deny).
|
|
22
20
|
*
|
|
23
21
|
* Studio observability (storage-denial-events): every enforcement
|
|
24
22
|
* decision — allow, deny, AND admin-plane bypass — lands on the
|
|
@@ -44,10 +42,12 @@ import { emitSandboxEvent, makeSandboxOperationEvent } from 'pyric/sandbox/inter
|
|
|
44
42
|
import type { EventProvenance } from 'pyric/sandbox';
|
|
45
43
|
import {
|
|
46
44
|
storageOperationProvenance,
|
|
45
|
+
type CrossServiceIam,
|
|
47
46
|
type StorageService,
|
|
48
47
|
type Target,
|
|
49
48
|
} from './service.js';
|
|
50
49
|
import { evaluateStorageRules } from './sandbox/rules-evaluator.js';
|
|
50
|
+
import { RuleEvalError } from './sandbox/rules-evaluation-error.js';
|
|
51
51
|
import type { EvaluationInput, FirestoreLookup } from './sandbox/rules.js';
|
|
52
52
|
import { unauthorized } from './errors.js';
|
|
53
53
|
|
|
@@ -71,18 +71,16 @@ export function enforceRules(
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
if (!service.rules) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
emitOperation(target, input, 'allow', undefined, 'user', false, boundProvenance);
|
|
78
|
-
return;
|
|
74
|
+
const reasons = ['No Storage rules configured; default deny.'];
|
|
75
|
+
emitOperation(target, input, 'deny', reasons, 'user', false, boundProvenance);
|
|
76
|
+
throw unauthorized(input.request.method, input.request.path, ' — No Storage rules configured; default deny.');
|
|
79
77
|
}
|
|
80
78
|
const evaluationInput = target ? withCanonicalRulesPath(input, target.bucket) : input;
|
|
81
79
|
const result = evaluateStorageRules(
|
|
82
80
|
service.rules,
|
|
83
81
|
evaluationInput,
|
|
84
82
|
undefined,
|
|
85
|
-
firestoreLookupFor(target),
|
|
83
|
+
firestoreLookupFor(target, service.crossServiceIam),
|
|
86
84
|
);
|
|
87
85
|
if (!result.allowed) {
|
|
88
86
|
emitOperation(target, input, 'deny', result.reasons, 'user', true, boundProvenance);
|
|
@@ -164,12 +162,53 @@ function emitOperation(
|
|
|
164
162
|
*
|
|
165
163
|
* Calls without a target get no lookup — a rule that reaches for
|
|
166
164
|
* `firestore.*` there denies "unsupported" rather than false-allowing.
|
|
165
|
+
*
|
|
166
|
+
* `crossServiceIam: 'denied'` models the production project state WITHOUT
|
|
167
|
+
* `roles/firebaserules.firestoreServiceAgent` on the Storage service agent:
|
|
168
|
+
* the lookup capability is still injected (so path validation and laziness
|
|
169
|
+
* behave identically), but every EXECUTED get/exists fails. See
|
|
170
|
+
* {@link crossServiceIamDeniedLookup}.
|
|
167
171
|
*/
|
|
168
|
-
function firestoreLookupFor(
|
|
172
|
+
function firestoreLookupFor(
|
|
173
|
+
target: Target | undefined,
|
|
174
|
+
crossServiceIam: CrossServiceIam,
|
|
175
|
+
): FirestoreLookup | undefined {
|
|
169
176
|
if (!target) return undefined;
|
|
177
|
+
if (crossServiceIam === 'denied') return crossServiceIamDeniedLookup();
|
|
170
178
|
const admin = target.sandbox.admin;
|
|
171
179
|
return {
|
|
172
180
|
get: (path) => admin.getDocument(path) as Record<string, unknown> | null,
|
|
173
181
|
exists: (path) => admin.getDocument(path) !== null,
|
|
174
182
|
};
|
|
175
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The `crossServiceIam: 'denied'` lookup: every executed
|
|
187
|
+
* `firestore.get()/exists()` throws a {@link RuleEvalError} naming the
|
|
188
|
+
* missing service-agent role, so the rule denies with that reason,
|
|
189
|
+
* mirroring production's IAM-disabled boundary captured by conformance
|
|
190
|
+
* observation `stdlib-realstorage-p3-lookup-budget` (row storage-rules#134):
|
|
191
|
+
* every lookup-executing family DENIES while a short-circuited lookup is
|
|
192
|
+
* never invoked and its rule still ALLOWS. The capture pins only that
|
|
193
|
+
* executed-lookup/short-circuit boundary; how an IAM failure interacts
|
|
194
|
+
* with CEL `&&`/`||` error absorption is NOT pinned by it (no absorption
|
|
195
|
+
* family ran IAM-disabled), so this uses the same absorbable
|
|
196
|
+
* {@link RuleEvalError} class as the existing no-capability deny path.
|
|
197
|
+
*
|
|
198
|
+
* Exported for the conformance replay test
|
|
199
|
+
* (`packages/conformance/test/src/storage-stdlib-real-replay.test.ts`),
|
|
200
|
+
* which runs the captured IAM-disabled matrix against the evaluator with
|
|
201
|
+
* THIS production lookup, not a hand-rolled twin.
|
|
202
|
+
*/
|
|
203
|
+
export function crossServiceIamDeniedLookup(): FirestoreLookup {
|
|
204
|
+
const fail = (method: 'get' | 'exists'): never => {
|
|
205
|
+
throw new RuleEvalError(
|
|
206
|
+
`firestore.${method}() failed: cross-service Firestore access is not authorized: ` +
|
|
207
|
+
"the Storage service agent lacks roles/firebaserules.firestoreServiceAgent (crossServiceIam: 'denied')",
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
get: () => fail('get'),
|
|
212
|
+
exists: () => fail('exists'),
|
|
213
|
+
};
|
|
214
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
export { getStorageSandbox, TARGET_SYMBOL } from './service.js';
|
|
25
|
-
export type { FirebaseStorage, StorageOptions, Target, SandboxTarget } from './service.js';
|
|
25
|
+
export type { CrossServiceIam, FirebaseStorage, StorageOptions, Target, SandboxTarget } from './service.js';
|
|
26
26
|
export { getStorage, connectStorageEmulator } from './instances.js';
|
|
27
27
|
|
|
28
28
|
export { StorageError } from './errors.js';
|
package/src/storage/list.ts
CHANGED
|
@@ -52,7 +52,7 @@ export async function listAll(refIn: StorageReference): Promise<ListResult> {
|
|
|
52
52
|
// requires `read` on the scanned ref's path. Prefixes have no
|
|
53
53
|
// backing object, so `resource` is null (a list of an unauthorized
|
|
54
54
|
// tree throws `storage/unauthorized`, same as a denied read). When
|
|
55
|
-
// no rules are configured
|
|
55
|
+
// no rules are configured, enforcement fails closed.
|
|
56
56
|
enforceRules(service, {
|
|
57
57
|
request: {
|
|
58
58
|
auth: storageAuth(target),
|
|
@@ -1,2 +1,34 @@
|
|
|
1
1
|
/** Fatal evaluator misuse caught at the allow boundary and converted to a deny. */
|
|
2
2
|
export class RuleEvalError extends Error {}
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A construct whose production verdict is unknowable locally: either
|
|
6
|
+
* production rejects the ruleset at deploy time (undefined function, wrong
|
|
7
|
+
* arity, unresolved import, unknown namespace method) or the simulator
|
|
8
|
+
* cannot model the construct at all. Its effect is EVALUATION-WIDE in
|
|
9
|
+
* production, where a determining `&&`/`||` operand cannot rescue it, so it is
|
|
10
|
+
* never absorbed and always fails closed.
|
|
11
|
+
*/
|
|
12
|
+
export class RuleUnsupportedError extends RuleEvalError {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A resource-limit exhaustion (the two-document Firestore lookup cap, the
|
|
16
|
+
* max call depth). Production fails the WHOLE evaluation closed on these:
|
|
17
|
+
* they are not CEL error values, so commutative `&&`/`||` absorption must
|
|
18
|
+
* not turn one into an allow (same posture as the Firestore simulator's
|
|
19
|
+
* LookupBudgetError precedent).
|
|
20
|
+
*/
|
|
21
|
+
export class RuleResourceLimitError extends RuleEvalError {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True for the errors that CEL `&&`/`||` absorption may treat as an error
|
|
25
|
+
* VALUE at an operand boundary: genuine rule-evaluation failures, excluding
|
|
26
|
+
* the unsupported/compile-reject and resource-limit classes above.
|
|
27
|
+
*/
|
|
28
|
+
export function isAbsorbableEvalError(err: unknown): err is RuleEvalError {
|
|
29
|
+
return (
|
|
30
|
+
err instanceof RuleEvalError
|
|
31
|
+
&& !(err instanceof RuleUnsupportedError)
|
|
32
|
+
&& !(err instanceof RuleResourceLimitError)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
} from './rules.js';
|
|
13
13
|
import { evalMethodCall } from './rules-methods.js';
|
|
14
14
|
import { formatPath, matchSegments, splitPath } from './rules-path-match.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
RuleEvalError,
|
|
17
|
+
RuleResourceLimitError,
|
|
18
|
+
RuleUnsupportedError,
|
|
19
|
+
isAbsorbableEvalError,
|
|
20
|
+
} from './rules-evaluation-error.js';
|
|
16
21
|
import {
|
|
17
22
|
RuleError,
|
|
18
23
|
describeRulesType as describeType,
|
|
@@ -92,6 +97,10 @@ export function evaluateStorageRules(
|
|
|
92
97
|
);
|
|
93
98
|
continue;
|
|
94
99
|
}
|
|
100
|
+
// Unverified: production's behavior for a non-boolean allow
|
|
101
|
+
// condition (a CEL type error there, rather than this truthiness
|
|
102
|
+
// coercion) has not been captured. The coercion stays as written
|
|
103
|
+
// until a production capture settles it.
|
|
95
104
|
result = truthy(value);
|
|
96
105
|
} catch (err) {
|
|
97
106
|
// Any function-evaluation failure (undefined function, wrong
|
|
@@ -186,12 +195,17 @@ export interface EvalCtx {
|
|
|
186
195
|
/**
|
|
187
196
|
* Walk an `Expr` against the bindings + path params. Missing bindings or
|
|
188
197
|
* members and invalid operations produce `RuleError` values. They propagate
|
|
189
|
-
* unless
|
|
190
|
-
*
|
|
191
|
-
*
|
|
198
|
+
* unless a `&&`/`||` operand that uniquely determines the result absorbs
|
|
199
|
+
* them COMMUTATIVELY, CEL-style (`<error> || true` → true, and
|
|
200
|
+
* `<error> && false` → false, see the binary case). Any `RuleError` that
|
|
201
|
+
* reaches an allow boundary denies with its production-shaped reason.
|
|
192
202
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
203
|
+
* Function-evaluation failures throw `RuleEvalError`; at a `&&`/`||`
|
|
204
|
+
* operand boundary the ABSORBABLE ones are converted to error values so
|
|
205
|
+
* they participate in the same absorption, while unsupported/compile-reject
|
|
206
|
+
* (`RuleUnsupportedError`) and resource-limit (`RuleResourceLimitError`)
|
|
207
|
+
* failures re-throw and fail the evaluation closed. The allow boundary
|
|
208
|
+
* catches whatever still throws and denies instead of falling through to a
|
|
195
209
|
* potentially truthy value.
|
|
196
210
|
*/
|
|
197
211
|
export function evalExpr(expr: Expr, ctx: EvalCtx): unknown {
|
|
@@ -236,25 +250,37 @@ export function evalExpr(expr: Expr, ctx: EvalCtx): unknown {
|
|
|
236
250
|
case 'path':
|
|
237
251
|
// A path literal is only meaningful as a `firestore.get()/exists()`
|
|
238
252
|
// argument (handled directly there). Reaching it anywhere else means
|
|
239
|
-
// the rule used it out of position
|
|
240
|
-
|
|
253
|
+
// the rule used it out of position, a compile-reject-class failure
|
|
254
|
+
// (never absorbed): deny rather than coerce.
|
|
255
|
+
throw new RuleUnsupportedError('a Firestore path literal is only valid as an argument to firestore.get()/exists()');
|
|
241
256
|
case 'unary': {
|
|
242
|
-
// An error survives negation (production: `!(resource.name == 'x')` with
|
|
243
|
-
// `name` absent DENIES). Propagate rather than flipping it to `true`.
|
|
244
257
|
const a = evalExpr(expr.arg, ctx);
|
|
258
|
+
if (expr.op === '!') {
|
|
259
|
+
if (isErr(a)) {
|
|
260
|
+
throw new RuleEvalError(a.message);
|
|
261
|
+
}
|
|
262
|
+
if (typeof a !== 'boolean') {
|
|
263
|
+
throw new RuleEvalError(`Unary '!' expects a boolean, got ${describeType(a)}.`);
|
|
264
|
+
}
|
|
265
|
+
return !a;
|
|
266
|
+
}
|
|
245
267
|
if (isErr(a)) return a;
|
|
246
268
|
if (expr.op === '-') {
|
|
247
269
|
if (a instanceof RulesFloat) return new RulesFloat(-a.value);
|
|
248
270
|
if (typeof a !== 'number') return new RuleError(`Unary '-' applied to ${describeType(a)}.`);
|
|
249
271
|
return -a;
|
|
250
272
|
}
|
|
251
|
-
return
|
|
273
|
+
return undefined;
|
|
252
274
|
}
|
|
253
275
|
case 'ternary': {
|
|
254
276
|
const c = evalExpr(expr.cond, ctx);
|
|
255
277
|
// An error condition denies the whole conditional; it must not fall
|
|
256
278
|
// through to the alternate branch and potentially allow.
|
|
257
279
|
if (isErr(c)) return c;
|
|
280
|
+
// Unverified: production's behavior for a non-boolean ternary
|
|
281
|
+
// condition (a CEL type error there, rather than this truthiness
|
|
282
|
+
// coercion) has not been captured. The coercion stays as written
|
|
283
|
+
// until a production capture settles it.
|
|
258
284
|
return truthy(c) ? evalExpr(expr.then, ctx) : evalExpr(expr.else, ctx);
|
|
259
285
|
}
|
|
260
286
|
case 'in': {
|
|
@@ -318,23 +344,12 @@ export function evalExpr(expr: Expr, ctx: EvalCtx): unknown {
|
|
|
318
344
|
return new RuleError(`Slice applied to ${describeType(t)} (expected a list or string).`);
|
|
319
345
|
}
|
|
320
346
|
case 'binary': {
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
if (expr.op === '||') {
|
|
329
|
-
const l = evalExpr(expr.left, ctx);
|
|
330
|
-
if (isErr(l)) {
|
|
331
|
-
// Production: `<error> || true` ALLOWS — a true disjunct rescues the
|
|
332
|
-
// error; `<error> || false` stays an error.
|
|
333
|
-
const r = evalExpr(expr.right, ctx);
|
|
334
|
-
return truthy(r) ? r : l;
|
|
335
|
-
}
|
|
336
|
-
return truthy(l) ? l : evalExpr(expr.right, ctx);
|
|
337
|
-
}
|
|
347
|
+
// RULES-B3: && and || are COMMUTATIVE error-absorbing operators in
|
|
348
|
+
// CEL, not JS left-to-right short-circuit. The two operators differ
|
|
349
|
+
// only in which operand value uniquely determines the result: false
|
|
350
|
+
// for &&, true for ||. Both are evaluated by the one helper below.
|
|
351
|
+
if (expr.op === '&&') return evalAbsorbingOperator(expr.left, expr.right, false, ctx);
|
|
352
|
+
if (expr.op === '||') return evalAbsorbingOperator(expr.left, expr.right, true, ctx);
|
|
338
353
|
const l = evalExpr(expr.left, ctx);
|
|
339
354
|
if (isErr(l)) return l;
|
|
340
355
|
const r = evalExpr(expr.right, ctx);
|
|
@@ -372,6 +387,64 @@ export function evalExpr(expr: Expr, ctx: EvalCtx): unknown {
|
|
|
372
387
|
}
|
|
373
388
|
}
|
|
374
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Evaluate one commutative error-absorbing operator, `&&` or `||`. The two
|
|
392
|
+
* differ only in `determining`, the operand value that fixes the result on
|
|
393
|
+
* its own: `false` for `&&`, `true` for `||`.
|
|
394
|
+
*
|
|
395
|
+
* If either operand evaluates to `determining`, that is the result and an
|
|
396
|
+
* error in the other operand is absorbed, whichever side it sits on. So
|
|
397
|
+
* `error && false` evaluates to false and `error || true` to true, while
|
|
398
|
+
* `error && true` and `error || false` propagate the error and deny.
|
|
399
|
+
* Laziness is preserved in the no-error path: a determining left operand
|
|
400
|
+
* skips the right one entirely.
|
|
401
|
+
*/
|
|
402
|
+
function evalAbsorbingOperator(
|
|
403
|
+
left: Expr,
|
|
404
|
+
right: Expr,
|
|
405
|
+
determining: boolean,
|
|
406
|
+
ctx: EvalCtx,
|
|
407
|
+
): boolean | RuleError {
|
|
408
|
+
const l = evalLogicalOperand(left, ctx);
|
|
409
|
+
if (l === determining) return determining; // left determines; right unevaluated
|
|
410
|
+
const r = evalLogicalOperand(right, ctx);
|
|
411
|
+
if (r === determining) return determining; // right determines and absorbs any left error
|
|
412
|
+
if (isErr(l)) return l; // left errored and nothing determined
|
|
413
|
+
return r; // left was non-determining; right decides
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Evaluate one `&&`/`||` operand tri-state: `true`, `false`, or a
|
|
418
|
+
* {@link RuleError} value the operator may absorb commutatively.
|
|
419
|
+
*
|
|
420
|
+
* - A thrown ABSORBABLE {@link RuleEvalError} (for example
|
|
421
|
+
* `firestore.get()` without an injected capability, or `!` on an error)
|
|
422
|
+
* is converted to an error VALUE here so a determining sibling operand
|
|
423
|
+
* can absorb it: production evaluates these positions to a
|
|
424
|
+
* position-local error.
|
|
425
|
+
* - {@link RuleUnsupportedError} (compile-reject or unmodelable) and
|
|
426
|
+
* {@link RuleResourceLimitError} (lookup cap, call depth) re-throw:
|
|
427
|
+
* production fails those closed for the WHOLE evaluation, so no
|
|
428
|
+
* determining operand may rescue them (the lookup-budget precedent).
|
|
429
|
+
* - A non-boolean, non-error operand is a CEL TYPE error (RULES-B6,
|
|
430
|
+
* captured by rules-firestore-strict-boolean-control-flow): it becomes
|
|
431
|
+
* an absorbable error value, never a truthy/falsy coercion.
|
|
432
|
+
*/
|
|
433
|
+
function evalLogicalOperand(expr: Expr, ctx: EvalCtx): boolean | RuleError {
|
|
434
|
+
let v: unknown;
|
|
435
|
+
try {
|
|
436
|
+
v = evalExpr(expr, ctx);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (isAbsorbableEvalError(err)) return new RuleError(err.message);
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
if (isErr(v)) return v;
|
|
442
|
+
if (typeof v !== 'boolean') {
|
|
443
|
+
return new RuleError(`Expected a boolean '&&'/'||' operand, got ${describeType(v)}.`);
|
|
444
|
+
}
|
|
445
|
+
return v;
|
|
446
|
+
}
|
|
447
|
+
|
|
375
448
|
/**
|
|
376
449
|
* Evaluate a user-defined function call. Arguments are evaluated in the
|
|
377
450
|
* CALLER's context, then bound to the function's parameters; the body
|
|
@@ -382,22 +455,26 @@ export function evalExpr(expr: Expr, ctx: EvalCtx): unknown {
|
|
|
382
455
|
*/
|
|
383
456
|
function evalCall(expr: Extract<Expr, { kind: 'call' }>, ctx: EvalCtx): unknown {
|
|
384
457
|
const fn = ctx.funcs.get(expr.name);
|
|
458
|
+
// Undefined functions, unresolved imports, and arity mismatches are
|
|
459
|
+
// COMPILE-reject failures in production (the ruleset never deploys), so
|
|
460
|
+
// they are RuleUnsupportedError: unabsorbable by &&/||, always deny.
|
|
385
461
|
if (!fn) {
|
|
386
|
-
throw new
|
|
462
|
+
throw new RuleUnsupportedError(`undefined function ${expr.name}()`);
|
|
387
463
|
}
|
|
388
464
|
if (fn.unresolvedImport !== undefined) {
|
|
389
|
-
throw new
|
|
465
|
+
throw new RuleUnsupportedError(
|
|
390
466
|
`function ${expr.name}() is imported from '${fn.unresolvedImport}', but import module resolution is not implemented`,
|
|
391
467
|
);
|
|
392
468
|
}
|
|
393
469
|
if (fn.params.length !== expr.args.length) {
|
|
394
|
-
throw new
|
|
470
|
+
throw new RuleUnsupportedError(
|
|
395
471
|
`function ${expr.name}() expects ${fn.params.length} argument(s), got ${expr.args.length}`,
|
|
396
472
|
);
|
|
397
473
|
}
|
|
398
474
|
const depth = ctx.depth + 1;
|
|
399
475
|
if (depth > MAX_CALL_DEPTH) {
|
|
400
|
-
|
|
476
|
+
// Resource-limit class: fails the evaluation closed, never absorbed.
|
|
477
|
+
throw new RuleResourceLimitError(
|
|
401
478
|
`function ${expr.name}() exceeded max call depth ${MAX_CALL_DEPTH}`,
|
|
402
479
|
);
|
|
403
480
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { RulesFloat } from '../../rules/simulator/wrappers/float.js';
|
|
2
2
|
import type { Expr } from './rules.js';
|
|
3
3
|
import { evalExpr, type EvalCtx } from './rules-evaluator.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
RuleEvalError,
|
|
6
|
+
RuleResourceLimitError,
|
|
7
|
+
RuleUnsupportedError,
|
|
8
|
+
} from './rules-evaluation-error.js';
|
|
5
9
|
import {
|
|
6
10
|
RuleError,
|
|
7
11
|
describeRulesType as describeType,
|
|
@@ -86,7 +90,10 @@ export function evalMethodCall(expr: Extract<Expr, { kind: 'methodcall' }>, ctx:
|
|
|
86
90
|
return evalMapGet(expr, ctx);
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
// An unknown method name is either unmodeled here or rejected by
|
|
94
|
+
// production's compiler. Its verdict is unknowable locally, so it is
|
|
95
|
+
// unabsorbable (fails closed even under a determining &&/|| operand).
|
|
96
|
+
throw new RuleUnsupportedError(`unsupported method .${expr.method}()`);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
/**
|
|
@@ -111,7 +118,8 @@ function evalFirestoreBuiltin(
|
|
|
111
118
|
ctx: EvalCtx,
|
|
112
119
|
): unknown {
|
|
113
120
|
if (expr.method !== 'get' && expr.method !== 'exists') {
|
|
114
|
-
|
|
121
|
+
// Unknown namespace method, compile-reject class, never absorbed.
|
|
122
|
+
throw new RuleUnsupportedError(`unsupported method firestore.${expr.method}()`);
|
|
115
123
|
}
|
|
116
124
|
if (!ctx.firestoreLookup) {
|
|
117
125
|
// No sandbox-backed capability injected — keep the deny-with-reason
|
|
@@ -121,16 +129,20 @@ function evalFirestoreBuiltin(
|
|
|
121
129
|
);
|
|
122
130
|
}
|
|
123
131
|
if (expr.args.length !== 1) {
|
|
124
|
-
|
|
132
|
+
// Wrong call shape: production rejects at compile; never absorbed.
|
|
133
|
+
throw new RuleUnsupportedError(`firestore.${expr.method}() expects a single path argument`);
|
|
125
134
|
}
|
|
126
135
|
const arg = expr.args[0];
|
|
127
136
|
if (arg.kind !== 'path') {
|
|
128
|
-
throw new
|
|
137
|
+
throw new RuleUnsupportedError(`firestore.${expr.method}() requires a /databases/.../documents/... path literal`);
|
|
129
138
|
}
|
|
130
139
|
const docPath = buildFirestoreDocPath(arg, ctx);
|
|
131
140
|
if (!ctx.firestoreAccesses.has(docPath)) {
|
|
132
141
|
if (ctx.firestoreAccesses.size >= 2) {
|
|
133
|
-
|
|
142
|
+
// Resource-limit class, the same posture as the Firestore lookup
|
|
143
|
+
// budget: production fails the whole evaluation closed, so a
|
|
144
|
+
// determining &&/|| operand must NOT absorb this into an allow.
|
|
145
|
+
throw new RuleResourceLimitError('firestore access limit exceeded: at most two distinct documents');
|
|
134
146
|
}
|
|
135
147
|
ctx.firestoreAccesses.add(docPath);
|
|
136
148
|
}
|
|
@@ -140,7 +152,9 @@ function evalFirestoreBuiltin(
|
|
|
140
152
|
const fields = ctx.firestoreLookup.get(docPath);
|
|
141
153
|
if (fields === null) {
|
|
142
154
|
// A missing get is a Rules error VALUE: it denies at the allow boundary,
|
|
143
|
-
// but participates in CEL error absorption
|
|
155
|
+
// but participates in COMMUTATIVE CEL error absorption in `&&`/`||`
|
|
156
|
+
// (`error || true` allows, and `error && false` evaluates to false,
|
|
157
|
+
// see the tri-state operand handling in rules-evaluator.ts).
|
|
144
158
|
return new RuleError(`firestore.get() targeted a nonexistent document: ${docPath}`);
|
|
145
159
|
}
|
|
146
160
|
return { data: fields };
|