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.
Files changed (281) hide show
  1. package/README.md +64 -89
  2. package/dist/ai/blocked.d.ts +52 -0
  3. package/dist/ai/blocked.d.ts.map +1 -0
  4. package/dist/ai/blocked.js +83 -0
  5. package/dist/ai/blocked.js.map +1 -0
  6. package/dist/ai/broker/broker.d.ts +50 -0
  7. package/dist/ai/broker/broker.d.ts.map +1 -1
  8. package/dist/ai/broker/broker.js +157 -21
  9. package/dist/ai/broker/broker.js.map +1 -1
  10. package/dist/ai/broker/gemini-engine.d.ts +56 -0
  11. package/dist/ai/broker/gemini-engine.d.ts.map +1 -0
  12. package/dist/ai/broker/gemini-engine.js +235 -0
  13. package/dist/ai/broker/gemini-engine.js.map +1 -0
  14. package/dist/ai/broker/index.d.ts +2 -1
  15. package/dist/ai/broker/index.d.ts.map +1 -1
  16. package/dist/ai/broker/index.js +2 -1
  17. package/dist/ai/broker/index.js.map +1 -1
  18. package/dist/ai/broker/openai-engine.d.ts +10 -1
  19. package/dist/ai/broker/openai-engine.d.ts.map +1 -1
  20. package/dist/ai/broker/openai-engine.js +26 -4
  21. package/dist/ai/broker/openai-engine.js.map +1 -1
  22. package/dist/ai/broker/synthesizer.d.ts +14 -0
  23. package/dist/ai/broker/synthesizer.d.ts.map +1 -1
  24. package/dist/ai/broker/synthesizer.js +31 -0
  25. package/dist/ai/broker/synthesizer.js.map +1 -1
  26. package/dist/ai/broker/types.d.ts +29 -0
  27. package/dist/ai/broker/types.d.ts.map +1 -1
  28. package/dist/ai/instances.js +1 -1
  29. package/dist/ai/instances.js.map +1 -1
  30. package/dist/ai/internal.d.ts +5 -0
  31. package/dist/ai/internal.d.ts.map +1 -1
  32. package/dist/ai/internal.js +5 -0
  33. package/dist/ai/internal.js.map +1 -1
  34. package/dist/ai/response-helpers.d.ts.map +1 -1
  35. package/dist/ai/response-helpers.js +6 -24
  36. package/dist/ai/response-helpers.js.map +1 -1
  37. package/dist/analytics/index.d.ts +40 -0
  38. package/dist/analytics/index.d.ts.map +1 -0
  39. package/dist/analytics/index.js +25 -0
  40. package/dist/analytics/index.js.map +1 -0
  41. package/dist/app-check/index.d.ts +31 -0
  42. package/dist/app-check/index.d.ts.map +1 -0
  43. package/dist/app-check/index.js +20 -0
  44. package/dist/app-check/index.js.map +1 -0
  45. package/dist/auth/instances.js +1 -1
  46. package/dist/auth/instances.js.map +1 -1
  47. package/dist/database/controls.d.ts +1 -1
  48. package/dist/database/controls.js +1 -1
  49. package/dist/database/instances.js +1 -1
  50. package/dist/database/instances.js.map +1 -1
  51. package/dist/database/sandbox/backend.d.ts +1 -0
  52. package/dist/database/sandbox/backend.d.ts.map +1 -1
  53. package/dist/database/sandbox/backend.js +1 -0
  54. package/dist/database/sandbox/backend.js.map +1 -1
  55. package/dist/database/sandbox/rules-eval.d.ts +6 -2
  56. package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
  57. package/dist/database/sandbox/rules-eval.js +15 -2
  58. package/dist/database/sandbox/rules-eval.js.map +1 -1
  59. package/dist/database/sandbox/write-plane.d.ts +1 -0
  60. package/dist/database/sandbox/write-plane.d.ts.map +1 -1
  61. package/dist/database/sandbox/write-plane.js +7 -2
  62. package/dist/database/sandbox/write-plane.js.map +1 -1
  63. package/dist/database/sandbox-controls.d.ts +12 -5
  64. package/dist/database/sandbox-controls.d.ts.map +1 -1
  65. package/dist/database/sandbox-controls.js +20 -9
  66. package/dist/database/sandbox-controls.js.map +1 -1
  67. package/dist/database/sandbox-namespace.d.ts +19 -1
  68. package/dist/database/sandbox-namespace.d.ts.map +1 -1
  69. package/dist/database/sandbox-namespace.js +24 -1
  70. package/dist/database/sandbox-namespace.js.map +1 -1
  71. package/dist/deferred/entry.d.ts +75 -0
  72. package/dist/deferred/entry.d.ts.map +1 -0
  73. package/dist/deferred/entry.js +143 -0
  74. package/dist/deferred/entry.js.map +1 -0
  75. package/dist/firestore/instances.js +1 -1
  76. package/dist/firestore/instances.js.map +1 -1
  77. package/dist/firestore/lite.d.ts +72 -0
  78. package/dist/firestore/lite.d.ts.map +1 -0
  79. package/dist/firestore/lite.js +22 -0
  80. package/dist/firestore/lite.js.map +1 -0
  81. package/dist/firestore/persistence.d.ts +2 -2
  82. package/dist/firestore/persistence.js +1 -1
  83. package/dist/firestore/sandbox-controls.d.ts +2 -0
  84. package/dist/firestore/sandbox-controls.d.ts.map +1 -1
  85. package/dist/firestore/sandbox-controls.js +8 -0
  86. package/dist/firestore/sandbox-controls.js.map +1 -1
  87. package/dist/functions/index.d.ts +29 -0
  88. package/dist/functions/index.d.ts.map +1 -0
  89. package/dist/functions/index.js +20 -0
  90. package/dist/functions/index.js.map +1 -0
  91. package/dist/messaging/client.d.ts +2 -1
  92. package/dist/messaging/client.d.ts.map +1 -1
  93. package/dist/messaging/client.js +2 -2
  94. package/dist/messaging/client.js.map +1 -1
  95. package/dist/messaging/instance.d.ts +2 -2
  96. package/dist/messaging/instance.d.ts.map +1 -1
  97. package/dist/messaging/instance.js +40 -6
  98. package/dist/messaging/instance.js.map +1 -1
  99. package/dist/messaging/sw.d.ts +2 -1
  100. package/dist/messaging/sw.d.ts.map +1 -1
  101. package/dist/messaging/sw.js +2 -2
  102. package/dist/messaging/sw.js.map +1 -1
  103. package/dist/performance/index.d.ts +22 -0
  104. package/dist/performance/index.d.ts.map +1 -0
  105. package/dist/performance/index.js +19 -0
  106. package/dist/performance/index.js.map +1 -0
  107. package/dist/remote-config/index.d.ts +40 -0
  108. package/dist/remote-config/index.d.ts.map +1 -0
  109. package/dist/remote-config/index.js +25 -0
  110. package/dist/remote-config/index.js.map +1 -0
  111. package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
  112. package/dist/rules/grammar/FirestoreValidator.js +43 -27
  113. package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
  114. package/dist/rules/grammar/document-access-count.d.ts +24 -0
  115. package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
  116. package/dist/rules/grammar/document-access-count.js +100 -0
  117. package/dist/rules/grammar/document-access-count.js.map +1 -0
  118. package/dist/rules/linter/ast-utils.d.ts +2 -6
  119. package/dist/rules/linter/ast-utils.d.ts.map +1 -1
  120. package/dist/rules/linter/ast-utils.js +2 -63
  121. package/dist/rules/linter/ast-utils.js.map +1 -1
  122. package/dist/rules/linter/hallucinations.d.ts +8 -0
  123. package/dist/rules/linter/hallucinations.d.ts.map +1 -1
  124. package/dist/rules/linter/hallucinations.js +121 -31
  125. package/dist/rules/linter/hallucinations.js.map +1 -1
  126. package/dist/rules/linter/linter.d.ts +10 -1
  127. package/dist/rules/linter/linter.d.ts.map +1 -1
  128. package/dist/rules/linter/linter.js +23 -9
  129. package/dist/rules/linter/linter.js.map +1 -1
  130. package/dist/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
  131. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts +1 -1
  132. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -1
  133. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js +5 -2
  134. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js.map +1 -1
  135. package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
  136. package/dist/rules/rtdb/grammar/simulator.js +61 -15
  137. package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
  138. package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
  139. package/dist/rules/rtdb/simulation/handler.js +96 -35
  140. package/dist/rules/rtdb/simulation/handler.js.map +1 -1
  141. package/dist/rules/simulator/document-lookups.d.ts +4 -0
  142. package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
  143. package/dist/rules/simulator/document-lookups.js +42 -3
  144. package/dist/rules/simulator/document-lookups.js.map +1 -1
  145. package/dist/rules/simulator/eval-error.d.ts +11 -0
  146. package/dist/rules/simulator/eval-error.d.ts.map +1 -1
  147. package/dist/rules/simulator/eval-error.js +14 -0
  148. package/dist/rules/simulator/eval-error.js.map +1 -1
  149. package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
  150. package/dist/rules/simulator/evaluation-builtins.js +33 -8
  151. package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
  152. package/dist/rules/simulator/evaluation-context.d.ts +12 -0
  153. package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
  154. package/dist/rules/simulator/evaluator.d.ts +1 -1
  155. package/dist/rules/simulator/evaluator.d.ts.map +1 -1
  156. package/dist/rules/simulator/evaluator.js +16 -4
  157. package/dist/rules/simulator/evaluator.js.map +1 -1
  158. package/dist/rules/simulator/handler.d.ts.map +1 -1
  159. package/dist/rules/simulator/handler.js +52 -5
  160. package/dist/rules/simulator/handler.js.map +1 -1
  161. package/dist/rules/simulator/lookup-budget.d.ts +37 -0
  162. package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
  163. package/dist/rules/simulator/lookup-budget.js +96 -0
  164. package/dist/rules/simulator/lookup-budget.js.map +1 -0
  165. package/dist/rules/stdlib-modules.d.ts +7 -0
  166. package/dist/rules/stdlib-modules.d.ts.map +1 -1
  167. package/dist/rules/stdlib-modules.js +11 -11
  168. package/dist/rules/stdlib-modules.js.map +1 -1
  169. package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
  170. package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
  171. package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
  172. package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
  173. package/dist/sandbox/index.d.ts +1 -1
  174. package/dist/sandbox/index.d.ts.map +1 -1
  175. package/dist/sandbox/index.js +1 -1
  176. package/dist/sandbox/index.js.map +1 -1
  177. package/dist/sandbox/persistence/types.d.ts +1 -1
  178. package/dist/sandbox/remote.d.ts +2 -2
  179. package/dist/sandbox/sandbox-context.d.ts +8 -1
  180. package/dist/sandbox/sandbox-context.d.ts.map +1 -1
  181. package/dist/sandbox/sandbox-context.js +59 -8
  182. package/dist/sandbox/sandbox-context.js.map +1 -1
  183. package/dist/sandbox/types/auth-state.d.ts +1 -0
  184. package/dist/sandbox/types/auth-state.d.ts.map +1 -1
  185. package/dist/sandbox/types/events.d.ts +2 -1
  186. package/dist/sandbox/types/events.d.ts.map +1 -1
  187. package/dist/sandbox/types/operation.d.ts +1 -0
  188. package/dist/sandbox/types/operation.d.ts.map +1 -1
  189. package/dist/storage/enforce.d.ts +20 -1
  190. package/dist/storage/enforce.d.ts.map +1 -1
  191. package/dist/storage/enforce.js +47 -14
  192. package/dist/storage/enforce.js.map +1 -1
  193. package/dist/storage/index.d.ts +1 -1
  194. package/dist/storage/index.d.ts.map +1 -1
  195. package/dist/storage/list.js +1 -1
  196. package/dist/storage/list.js.map +1 -1
  197. package/dist/storage/persistence.d.ts.map +1 -1
  198. package/dist/storage/persistence.js +66 -14
  199. package/dist/storage/persistence.js.map +1 -1
  200. package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
  201. package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
  202. package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
  203. package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
  204. package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
  205. package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
  206. package/dist/storage/sandbox/rules-evaluator.js +105 -33
  207. package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
  208. package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
  209. package/dist/storage/sandbox/rules-methods.js +17 -7
  210. package/dist/storage/sandbox/rules-methods.js.map +1 -1
  211. package/dist/storage/service.d.ts +23 -2
  212. package/dist/storage/service.d.ts.map +1 -1
  213. package/dist/storage/service.js +37 -26
  214. package/dist/storage/service.js.map +1 -1
  215. package/package.json +42 -2
  216. package/src/ai/blocked.ts +97 -0
  217. package/src/ai/broker/broker.ts +153 -22
  218. package/src/ai/broker/gemini-engine.ts +311 -0
  219. package/src/ai/broker/index.ts +2 -0
  220. package/src/ai/broker/openai-engine.ts +31 -3
  221. package/src/ai/broker/synthesizer.ts +34 -0
  222. package/src/ai/broker/types.ts +27 -1
  223. package/src/ai/instances.ts +1 -1
  224. package/src/ai/internal.ts +5 -0
  225. package/src/ai/response-helpers.ts +6 -25
  226. package/src/analytics/index.ts +53 -0
  227. package/src/app/dispatch.test.ts +3 -1
  228. package/src/app-check/index.ts +41 -0
  229. package/src/auth/instances.ts +1 -1
  230. package/src/database/controls.ts +1 -1
  231. package/src/database/instances.ts +1 -1
  232. package/src/database/sandbox/backend.ts +1 -0
  233. package/src/database/sandbox/rules-eval.ts +18 -2
  234. package/src/database/sandbox/write-plane.ts +8 -2
  235. package/src/database/sandbox-controls.ts +29 -9
  236. package/src/database/sandbox-namespace.ts +28 -1
  237. package/src/deferred/entry.ts +167 -0
  238. package/src/firestore/instances.ts +1 -1
  239. package/src/firestore/lite.ts +91 -0
  240. package/src/firestore/persistence.ts +2 -2
  241. package/src/firestore/sandbox-controls.ts +10 -0
  242. package/src/functions/index.ts +38 -0
  243. package/src/messaging/client.ts +3 -2
  244. package/src/messaging/instance.ts +44 -7
  245. package/src/messaging/sw.ts +3 -2
  246. package/src/performance/index.ts +31 -0
  247. package/src/remote-config/index.ts +53 -0
  248. package/src/rules/grammar/FirestoreValidator.ts +44 -28
  249. package/src/rules/grammar/document-access-count.ts +75 -0
  250. package/src/rules/linter/ast-utils.ts +2 -36
  251. package/src/rules/linter/hallucinations.ts +123 -25
  252. package/src/rules/linter/linter.ts +32 -10
  253. package/src/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
  254. package/src/rules/rtdb/grammar/RtdbExpr.ohm.generated.ts +5 -2
  255. package/src/rules/rtdb/grammar/simulator.ts +41 -15
  256. package/src/rules/rtdb/simulation/handler.ts +149 -46
  257. package/src/rules/simulator/document-lookups.ts +46 -3
  258. package/src/rules/simulator/eval-error.ts +16 -0
  259. package/src/rules/simulator/evaluation-builtins.ts +35 -8
  260. package/src/rules/simulator/evaluation-context.ts +12 -0
  261. package/src/rules/simulator/evaluator.ts +14 -3
  262. package/src/rules/simulator/handler.ts +68 -4
  263. package/src/rules/simulator/lookup-budget.ts +101 -0
  264. package/src/rules/stdlib-modules.ts +19 -11
  265. package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
  266. package/src/sandbox/index.ts +5 -1
  267. package/src/sandbox/persistence/types.ts +1 -1
  268. package/src/sandbox/remote.ts +2 -2
  269. package/src/sandbox/sandbox-context.ts +69 -9
  270. package/src/sandbox/types/auth-state.ts +1 -1
  271. package/src/sandbox/types/events.ts +2 -1
  272. package/src/sandbox/types/operation.ts +1 -1
  273. package/src/storage/enforce.ts +53 -14
  274. package/src/storage/index.ts +1 -1
  275. package/src/storage/list.ts +1 -1
  276. package/src/storage/persistence.ts +74 -16
  277. package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
  278. package/src/storage/sandbox/rules-evaluator.ts +109 -32
  279. package/src/storage/sandbox/rules-methods.ts +21 -7
  280. package/src/storage/service.ts +93 -27
  281. package/README.md.orig +0 -53
@@ -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 this is a no-op (open-by-default).
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
- return openDatabase(dbName).then((db) => new IndexedDbStorageBackend(db));
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
- const req = indexedDB.open(dbName, SCHEMA_VERSION);
250
- req.addEventListener('upgradeneeded', () => {
251
- const db = req.result;
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
- req.addEventListener('success', () => resolve(req.result));
260
- req.addEventListener('error', () => reject(req.error));
261
- req.addEventListener('blocked', () =>
262
- reject(new Error(`IndexedDB open blocked for db "${dbName}"`)),
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 { RuleEvalError } from './rules-evaluation-error.js';
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 evaluation short-circuits around them or an explicitly modeled
190
- * boolean case absorbs them (for example, `<error> || true`). Any `RuleError`
191
- * that reaches an allow boundary denies with its production-shaped reason.
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
- * User-defined function failures throw `RuleEvalError`; the allow boundary
194
- * likewise catches them and denies instead of falling through to a
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 — deny rather than coerce.
240
- throw new RuleEvalError('a Firestore path literal is only valid as an argument to firestore.get()/exists()');
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 !truthy(a);
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
- // Short-circuit && / || so half-undefined chains don't trip
322
- // (e.g. `request.auth != null && request.auth.uid == 'a'`).
323
- if (expr.op === '&&') {
324
- const l = evalExpr(expr.left, ctx);
325
- if (isErr(l)) return l;
326
- return truthy(l) ? evalExpr(expr.right, ctx) : l;
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 RuleEvalError(`undefined function ${expr.name}()`);
462
+ throw new RuleUnsupportedError(`undefined function ${expr.name}()`);
387
463
  }
388
464
  if (fn.unresolvedImport !== undefined) {
389
- throw new RuleEvalError(
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 RuleEvalError(
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
- throw new RuleEvalError(
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 { RuleEvalError } from './rules-evaluation-error.js';
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
- throw new RuleEvalError(`unsupported method .${expr.method}()`);
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
- throw new RuleEvalError(`unsupported method firestore.${expr.method}()`);
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
- throw new RuleEvalError(`firestore.${expr.method}() expects a single path argument`);
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 RuleEvalError(`firestore.${expr.method}() requires a /databases/.../documents/... path literal`);
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
- throw new RuleEvalError('firestore access limit exceeded: at most two distinct documents');
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 (`error || true` allows).
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 };
@@ -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 dev` passes
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
- /** One `StorageService` per `Sandbox`. */
142
- const SERVICES = new WeakMap<Sandbox, Promise<StorageService>>();
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
- * The rules SOURCE each sandbox's service was opened with (`null` when
159
- * opened without rules). Late-config detection: rules are honored only on
160
- * the FIRST storage call per `Sandbox`, so silently discarding a LATER,
161
- * DIFFERENT `rules` option would be a silent rules wipe — {@link
162
- * ensureService} throws instead. Re-supplying the IDENTICAL source stays
163
- * fine (idempotent multi-handle construction, e.g. per-user contexts).
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
- const SERVICE_RULES_SOURCE = new WeakMap<Sandbox, string | null>();
166
- const SERVICE_RULES_RESOLUTION = new WeakMap<Sandbox, StorageRulesResolution | null>();
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 = SERVICES.get(sandbox);
236
+ const existing = OPEN_SERVICES.get(sandbox);
181
237
  if (existing) {
182
- const openedWith = SERVICE_RULES_SOURCE.get(sandbox) ?? null;
183
- if (options.rules !== undefined && options.rules !== openedWith) {
184
- throw new Error(
185
- `pyric/storage: ${caller} received a rules source, but this sandbox's storage ` +
186
- `service is already open ${openedWith === null ? 'without rules' : 'with a different rules source'} ` +
187
- '— rules are honored only on the FIRST storage call per Sandbox, so these rules ' +
188
- 'would be silently discarded. Configure rules on the first storage call for this sandbox.',
189
- );
190
- }
191
- return existing;
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
- SERVICES.set(sandbox, servicePromise);
230
- SERVICE_RULES_SOURCE.set(sandbox, options.rules ?? null);
231
- SERVICE_RULES_RESOLUTION.set(sandbox, resolution);
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 SERVICE_RULES_RESOLUTION.get(targetOf(storage).sandbox) ?? null;
496
+ return OPEN_SERVICES.get(targetOf(storage).sandbox)?.rulesResolution ?? null;
431
497
  }