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/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),
|
|
@@ -176,10 +176,61 @@ export interface StorageBackend {
|
|
|
176
176
|
* production, callers pass nothing and inherit the default
|
|
177
177
|
* `pyric-storage`.
|
|
178
178
|
*/
|
|
179
|
-
export function openStorageBackend(
|
|
179
|
+
export async function openStorageBackend(
|
|
180
180
|
dbName: string = DEFAULT_DB_NAME,
|
|
181
181
|
): Promise<StorageBackend> {
|
|
182
|
-
|
|
182
|
+
try {
|
|
183
|
+
const db = await openDatabase(dbName);
|
|
184
|
+
return new IndexedDbStorageBackend(db);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return new InMemoryStorageBackend();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class InMemoryStorageBackend implements StorageBackend {
|
|
191
|
+
private readonly blobs = new Map<string, Blob>();
|
|
192
|
+
private readonly metadata = new Map<string, StoredMetadata>();
|
|
193
|
+
|
|
194
|
+
async put(path: string, blob: Blob, metadata: StoredMetadata): Promise<void> {
|
|
195
|
+
this.blobs.set(path, blob);
|
|
196
|
+
this.metadata.set(path, metadata);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getBlob(path: string): Promise<Blob | undefined> {
|
|
200
|
+
return this.blobs.get(path);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getMetadata(path: string): Promise<StoredMetadata | undefined> {
|
|
204
|
+
return this.metadata.get(path);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async putMetadata(path: string, metadata: StoredMetadata): Promise<void> {
|
|
208
|
+
this.metadata.set(path, metadata);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async delete(path: string): Promise<void> {
|
|
212
|
+
this.blobs.delete(path);
|
|
213
|
+
this.metadata.delete(path);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async listByPrefix(prefix: string): Promise<StoredMetadata[]> {
|
|
217
|
+
const results: StoredMetadata[] = [];
|
|
218
|
+
const keys = Array.from(this.metadata.keys()).sort();
|
|
219
|
+
for (const key of keys) {
|
|
220
|
+
if (key.startsWith(prefix)) {
|
|
221
|
+
const meta = this.metadata.get(key);
|
|
222
|
+
if (meta) results.push(meta);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async reset(): Promise<void> {
|
|
229
|
+
this.blobs.clear();
|
|
230
|
+
this.metadata.clear();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
close(): void {}
|
|
183
234
|
}
|
|
184
235
|
|
|
185
236
|
// ─── Internal ──────────────────────────────────────────────────────
|
|
@@ -246,21 +297,28 @@ class IndexedDbStorageBackend implements StorageBackend {
|
|
|
246
297
|
|
|
247
298
|
function openDatabase(dbName: string): Promise<IDBDatabase> {
|
|
248
299
|
return new Promise((resolve, reject) => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!db.objectStoreNames.contains(BLOBS_STORE)) {
|
|
253
|
-
db.createObjectStore(BLOBS_STORE);
|
|
254
|
-
}
|
|
255
|
-
if (!db.objectStoreNames.contains(METADATA_STORE)) {
|
|
256
|
-
db.createObjectStore(METADATA_STORE);
|
|
300
|
+
try {
|
|
301
|
+
if (typeof indexedDB === 'undefined' || !indexedDB.open) {
|
|
302
|
+
return reject(new Error('IndexedDB not available'));
|
|
257
303
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
304
|
+
const req = indexedDB.open(dbName, SCHEMA_VERSION);
|
|
305
|
+
req.addEventListener('upgradeneeded', () => {
|
|
306
|
+
const db = req.result;
|
|
307
|
+
if (!db.objectStoreNames.contains(BLOBS_STORE)) {
|
|
308
|
+
db.createObjectStore(BLOBS_STORE);
|
|
309
|
+
}
|
|
310
|
+
if (!db.objectStoreNames.contains(METADATA_STORE)) {
|
|
311
|
+
db.createObjectStore(METADATA_STORE);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
req.addEventListener('success', () => resolve(req.result));
|
|
315
|
+
req.addEventListener('error', () => reject(req.error));
|
|
316
|
+
req.addEventListener('blocked', () =>
|
|
317
|
+
reject(new Error(`IndexedDB open blocked for db "${dbName}"`)),
|
|
318
|
+
);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
reject(err);
|
|
321
|
+
}
|
|
264
322
|
});
|
|
265
323
|
}
|
|
266
324
|
|
|
@@ -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 };
|
package/src/storage/service.ts
CHANGED
|
@@ -92,6 +92,20 @@ export interface FirebaseStorage {
|
|
|
92
92
|
/** Storage handle returned by Firebase-shaped app overloads. */
|
|
93
93
|
export type AppFirebaseStorage = FirebaseStorage & { readonly app: FirebaseApp };
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Cross-service IAM posture for `firestore.get()/exists()` in Storage rules.
|
|
97
|
+
*
|
|
98
|
+
* Production Storage rules can read Firestore ONLY when the project's
|
|
99
|
+
* Storage service agent holds `roles/firebaserules.firestoreServiceAgent`.
|
|
100
|
+
* `'granted'` (the default, the common configured-project state) serves
|
|
101
|
+
* lookups from the same-sandbox Firestore store; `'denied'` makes every
|
|
102
|
+
* EXECUTED lookup fail exactly like production without the role (error →
|
|
103
|
+
* rule denies), while short-circuited lookups are never executed and stay
|
|
104
|
+
* unaffected. Captured boundary: conformance observation
|
|
105
|
+
* `stdlib-realstorage-p3-lookup-budget` (registry row storage-rules#134).
|
|
106
|
+
*/
|
|
107
|
+
export type CrossServiceIam = 'granted' | 'denied';
|
|
108
|
+
|
|
95
109
|
/**
|
|
96
110
|
* Internal sandbox service — owns the IDB connection + parsed rules.
|
|
97
111
|
* Only constructed inside the sandbox `getStorageSandbox` path.
|
|
@@ -100,6 +114,7 @@ export class StorageService {
|
|
|
100
114
|
constructor(
|
|
101
115
|
readonly backend: StorageBackend,
|
|
102
116
|
readonly rules: StorageRules | null = null,
|
|
117
|
+
readonly crossServiceIam: CrossServiceIam = 'granted',
|
|
103
118
|
) {}
|
|
104
119
|
}
|
|
105
120
|
|
|
@@ -124,7 +139,7 @@ export interface StorageOptions {
|
|
|
124
139
|
* origin-scoped, so without this every project served on the same
|
|
125
140
|
* localhost port shared one storage database (issue #359). Ignored when
|
|
126
141
|
* an explicit `dbName` is given; only honored on the FIRST call per
|
|
127
|
-
* `Sandbox`. Hosts pass their project identity here (`pyric
|
|
142
|
+
* `Sandbox`. Hosts pass their project identity here (`pyric sandbox` passes
|
|
128
143
|
* the served project's key; app handles pass `options.projectId`).
|
|
129
144
|
*/
|
|
130
145
|
projectId?: string;
|
|
@@ -134,12 +149,36 @@ export interface StorageOptions {
|
|
|
134
149
|
* `Sandbox`.
|
|
135
150
|
*/
|
|
136
151
|
rules?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Model the production Storage→Firestore IAM dependency (see
|
|
154
|
+
* {@link CrossServiceIam}). Defaults to `'granted'`. Like `rules`,
|
|
155
|
+
* only honored on the FIRST storage call per `Sandbox`; a LATER,
|
|
156
|
+
* DIFFERING value throws instead of silently flipping the mode.
|
|
157
|
+
*/
|
|
158
|
+
crossServiceIam?: CrossServiceIam;
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
// ─── Caches ─────────────────────────────────────────────────────────
|
|
140
162
|
|
|
141
|
-
/**
|
|
142
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Everything one `Sandbox` opened its storage service WITH, in one record.
|
|
165
|
+
* These four facts are written together on the first storage call and read
|
|
166
|
+
* together afterwards, so they live in one map rather than four parallel
|
|
167
|
+
* ones that can drift out of step.
|
|
168
|
+
*/
|
|
169
|
+
interface OpenStorageService {
|
|
170
|
+
/** The service itself, resolving once its IndexedDB backend is open. */
|
|
171
|
+
readonly service: Promise<StorageService>;
|
|
172
|
+
/** The rules source it was opened with, `null` when opened without rules. */
|
|
173
|
+
readonly rulesSource: string | null;
|
|
174
|
+
/** The module-resolution record for that source, `null` without rules. */
|
|
175
|
+
readonly rulesResolution: StorageRulesResolution | null;
|
|
176
|
+
/** The cross-service IAM mode it was opened with. */
|
|
177
|
+
readonly crossServiceIam: CrossServiceIam;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** One open storage service, with its opening configuration, per `Sandbox`. */
|
|
181
|
+
const OPEN_SERVICES = new WeakMap<Sandbox, OpenStorageService>();
|
|
143
182
|
|
|
144
183
|
/** One sandbox `FirebaseStorage` handle per `SandboxContext`. */
|
|
145
184
|
const SANDBOX_HANDLES = new WeakMap<SandboxContext, FirebaseStorage>();
|
|
@@ -155,15 +194,32 @@ const SANDBOX_HANDLES = new WeakMap<SandboxContext, FirebaseStorage>();
|
|
|
155
194
|
const BARE_SANDBOX_HANDLES = new WeakMap<Sandbox, FirebaseStorage>();
|
|
156
195
|
|
|
157
196
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
197
|
+
* Reject a late option that differs from the one the service is already
|
|
198
|
+
* open with. Both `rules` and `crossServiceIam` are honored only on the
|
|
199
|
+
* FIRST storage call per `Sandbox`. Silently discarding a later, differing
|
|
200
|
+
* value would wipe the rules, or reopen the false-ALLOW surface the IAM
|
|
201
|
+
* mode models, without a word. Re-supplying the IDENTICAL value stays fine
|
|
202
|
+
* (idempotent multi-handle construction, for example per-user contexts),
|
|
203
|
+
* and so does omitting it.
|
|
164
204
|
*/
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
function rejectDifferingLateConfig(
|
|
206
|
+
option: string,
|
|
207
|
+
descriptor: string,
|
|
208
|
+
supplied: string | undefined,
|
|
209
|
+
openedWith: string | null,
|
|
210
|
+
caller: string,
|
|
211
|
+
): void {
|
|
212
|
+
if (supplied === undefined || supplied === openedWith) return;
|
|
213
|
+
const openedDescription = openedWith === null
|
|
214
|
+
? `without ${option}`
|
|
215
|
+
: `with a different ${descriptor}`;
|
|
216
|
+
throw new Error(
|
|
217
|
+
`pyric/storage: ${caller} received a ${descriptor}, but this sandbox's storage service ` +
|
|
218
|
+
`is already open ${openedDescription}. ${option} is honored only on the FIRST storage ` +
|
|
219
|
+
'call per Sandbox, so this value would be silently discarded. Configure it on the first ' +
|
|
220
|
+
'storage call for this sandbox.',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
167
223
|
|
|
168
224
|
/**
|
|
169
225
|
* Get (or open) the ONE per-sandbox `StorageService`. Loud on the
|
|
@@ -177,18 +233,23 @@ function ensureService(
|
|
|
177
233
|
options: StorageOptions,
|
|
178
234
|
caller: string,
|
|
179
235
|
): Promise<StorageService> {
|
|
180
|
-
const existing =
|
|
236
|
+
const existing = OPEN_SERVICES.get(sandbox);
|
|
181
237
|
if (existing) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
rejectDifferingLateConfig(
|
|
239
|
+
'rules',
|
|
240
|
+
'rules source',
|
|
241
|
+
options.rules,
|
|
242
|
+
existing.rulesSource,
|
|
243
|
+
caller,
|
|
244
|
+
);
|
|
245
|
+
rejectDifferingLateConfig(
|
|
246
|
+
'crossServiceIam',
|
|
247
|
+
'crossServiceIam mode',
|
|
248
|
+
options.crossServiceIam,
|
|
249
|
+
existing.crossServiceIam,
|
|
250
|
+
caller,
|
|
251
|
+
);
|
|
252
|
+
return existing.service;
|
|
192
253
|
}
|
|
193
254
|
let rules: StorageRules | null = null;
|
|
194
255
|
let resolution: StorageRulesResolution | null = null;
|
|
@@ -221,14 +282,18 @@ function ensureService(
|
|
|
221
282
|
}
|
|
222
283
|
// Explicit dbName wins; otherwise scope the default by project identity so
|
|
223
284
|
// two projects on one origin never share a storage database (issue #359).
|
|
285
|
+
const crossServiceIam = options.crossServiceIam ?? 'granted';
|
|
224
286
|
const servicePromise = openStorageBackend(
|
|
225
287
|
options.dbName ?? storageDbName(options.projectId),
|
|
226
288
|
).then(
|
|
227
|
-
(backend) => new StorageService(backend, rules),
|
|
289
|
+
(backend) => new StorageService(backend, rules, crossServiceIam),
|
|
228
290
|
);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
291
|
+
OPEN_SERVICES.set(sandbox, {
|
|
292
|
+
service: servicePromise,
|
|
293
|
+
rulesSource: options.rules ?? null,
|
|
294
|
+
rulesResolution: resolution,
|
|
295
|
+
crossServiceIam,
|
|
296
|
+
});
|
|
232
297
|
// Join the sandbox's persistable-service REGISTRY so `sandbox.resetAll()`
|
|
233
298
|
// reaches storage (issue #359: Studio's reset cleared Firestore + auth but
|
|
234
299
|
// never storage — storage was invisible to the sandbox). Storage does NOT
|
|
@@ -293,6 +358,7 @@ export function getStorageSandbox(
|
|
|
293
358
|
kind: 'sandbox',
|
|
294
359
|
sandbox,
|
|
295
360
|
context: ctx,
|
|
361
|
+
currentAuth: fromBareSandbox ? () => sandbox.currentUser : undefined,
|
|
296
362
|
bucket,
|
|
297
363
|
servicePromise,
|
|
298
364
|
};
|
|
@@ -427,5 +493,5 @@ export function getStorageService(storage: FirebaseStorage): Promise<StorageServ
|
|
|
427
493
|
export function getStorageRulesResolution(
|
|
428
494
|
storage: FirebaseStorage,
|
|
429
495
|
): StorageRulesResolution | null {
|
|
430
|
-
return
|
|
496
|
+
return OPEN_SERVICES.get(targetOf(storage).sandbox)?.rulesResolution ?? null;
|
|
431
497
|
}
|