pyric 0.0.1 → 0.1.0-alpha.8

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 (787) hide show
  1. package/README.md +138 -2
  2. package/README.md.orig +54 -0
  3. package/dist/app/index.d.ts +81 -0
  4. package/dist/app/index.d.ts.map +1 -0
  5. package/dist/app/index.js +78 -0
  6. package/dist/app/index.js.map +1 -0
  7. package/dist/auth/index.d.ts +346 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +701 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/prod-backend.d.ts +39 -0
  12. package/dist/auth/prod-backend.d.ts.map +1 -0
  13. package/dist/auth/prod-backend.js +179 -0
  14. package/dist/auth/prod-backend.js.map +1 -0
  15. package/dist/auth/providers.d.ts +107 -0
  16. package/dist/auth/providers.d.ts.map +1 -0
  17. package/dist/auth/providers.js +138 -0
  18. package/dist/auth/providers.js.map +1 -0
  19. package/dist/auth/sandbox-backend.d.ts +769 -0
  20. package/dist/auth/sandbox-backend.d.ts.map +1 -0
  21. package/dist/auth/sandbox-backend.js +1598 -0
  22. package/dist/auth/sandbox-backend.js.map +1 -0
  23. package/dist/auth/target.d.ts +40 -0
  24. package/dist/auth/target.d.ts.map +1 -0
  25. package/dist/auth/target.js +28 -0
  26. package/dist/auth/target.js.map +1 -0
  27. package/dist/auth/types.d.ts +252 -0
  28. package/dist/auth/types.d.ts.map +1 -0
  29. package/dist/auth/types.js +26 -0
  30. package/dist/auth/types.js.map +1 -0
  31. package/dist/database/constraints/atoms.d.ts +29 -0
  32. package/dist/database/constraints/atoms.d.ts.map +1 -0
  33. package/dist/database/constraints/atoms.js +43 -0
  34. package/dist/database/constraints/atoms.js.map +1 -0
  35. package/dist/database/constraints/compose.d.ts +16 -0
  36. package/dist/database/constraints/compose.d.ts.map +1 -0
  37. package/dist/database/constraints/compose.js +15 -0
  38. package/dist/database/constraints/compose.js.map +1 -0
  39. package/dist/database/constraints/data.d.ts +30 -0
  40. package/dist/database/constraints/data.d.ts.map +1 -0
  41. package/dist/database/constraints/data.js +46 -0
  42. package/dist/database/constraints/data.js.map +1 -0
  43. package/dist/database/constraints/document.d.ts +42 -0
  44. package/dist/database/constraints/document.d.ts.map +1 -0
  45. package/dist/database/constraints/document.js +86 -0
  46. package/dist/database/constraints/document.js.map +1 -0
  47. package/dist/database/constraints/game.d.ts +30 -0
  48. package/dist/database/constraints/game.d.ts.map +1 -0
  49. package/dist/database/constraints/game.js +49 -0
  50. package/dist/database/constraints/game.js.map +1 -0
  51. package/dist/database/constraints/index.d.ts +12 -0
  52. package/dist/database/constraints/index.d.ts.map +1 -0
  53. package/dist/database/constraints/index.js +9 -0
  54. package/dist/database/constraints/index.js.map +1 -0
  55. package/dist/database/constraints/policies.d.ts +16 -0
  56. package/dist/database/constraints/policies.d.ts.map +1 -0
  57. package/dist/database/constraints/policies.js +17 -0
  58. package/dist/database/constraints/policies.js.map +1 -0
  59. package/dist/database/constraints/ruleset.d.ts +5 -0
  60. package/dist/database/constraints/ruleset.d.ts.map +1 -0
  61. package/dist/database/constraints/ruleset.js +132 -0
  62. package/dist/database/constraints/ruleset.js.map +1 -0
  63. package/dist/database/constraints/schema.d.ts +17 -0
  64. package/dist/database/constraints/schema.d.ts.map +1 -0
  65. package/dist/database/constraints/schema.js +74 -0
  66. package/dist/database/constraints/schema.js.map +1 -0
  67. package/dist/database/constraints/types.d.ts +22 -0
  68. package/dist/database/constraints/types.d.ts.map +1 -0
  69. package/dist/database/constraints/types.js +2 -0
  70. package/dist/database/constraints/types.js.map +1 -0
  71. package/dist/database/crawl/handler.d.ts +7 -0
  72. package/dist/database/crawl/handler.d.ts.map +1 -0
  73. package/dist/database/crawl/handler.js +91 -0
  74. package/dist/database/crawl/handler.js.map +1 -0
  75. package/dist/database/crawl/semaphore.d.ts +9 -0
  76. package/dist/database/crawl/semaphore.d.ts.map +1 -0
  77. package/dist/database/crawl/semaphore.js +27 -0
  78. package/dist/database/crawl/semaphore.js.map +1 -0
  79. package/dist/database/crawl/spec.d.ts +33 -0
  80. package/dist/database/crawl/spec.d.ts.map +1 -0
  81. package/dist/database/crawl/spec.js +12 -0
  82. package/dist/database/crawl/spec.js.map +1 -0
  83. package/dist/database/data/handler.d.ts +9 -0
  84. package/dist/database/data/handler.d.ts.map +1 -0
  85. package/dist/database/data/handler.js +96 -0
  86. package/dist/database/data/handler.js.map +1 -0
  87. package/dist/database/data/spec.d.ts +77 -0
  88. package/dist/database/data/spec.d.ts.map +1 -0
  89. package/dist/database/data/spec.js +17 -0
  90. package/dist/database/data/spec.js.map +1 -0
  91. package/dist/database/data/validated.d.ts +8 -0
  92. package/dist/database/data/validated.d.ts.map +1 -0
  93. package/dist/database/data/validated.js +99 -0
  94. package/dist/database/data/validated.js.map +1 -0
  95. package/dist/database/grammar/RtdbExpr.ohm +98 -0
  96. package/dist/database/grammar/RtdbExpr.ohm.generated.d.ts +2 -0
  97. package/dist/database/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -0
  98. package/dist/database/grammar/RtdbExpr.ohm.generated.js +103 -0
  99. package/dist/database/grammar/RtdbExpr.ohm.generated.js.map +1 -0
  100. package/dist/database/grammar/RtdbExprParser.d.ts +5 -0
  101. package/dist/database/grammar/RtdbExprParser.d.ts.map +1 -0
  102. package/dist/database/grammar/RtdbExprParser.js +53 -0
  103. package/dist/database/grammar/RtdbExprParser.js.map +1 -0
  104. package/dist/database/grammar/linter.d.ts +3 -0
  105. package/dist/database/grammar/linter.d.ts.map +1 -0
  106. package/dist/database/grammar/linter.js +66 -0
  107. package/dist/database/grammar/linter.js.map +1 -0
  108. package/dist/database/grammar/simulator.d.ts +31 -0
  109. package/dist/database/grammar/simulator.d.ts.map +1 -0
  110. package/dist/database/grammar/simulator.js +255 -0
  111. package/dist/database/grammar/simulator.js.map +1 -0
  112. package/dist/database/grammar/validator.d.ts +3 -0
  113. package/dist/database/grammar/validator.d.ts.map +1 -0
  114. package/dist/database/grammar/validator.js +84 -0
  115. package/dist/database/grammar/validator.js.map +1 -0
  116. package/dist/database/host.d.ts +50 -0
  117. package/dist/database/host.d.ts.map +1 -0
  118. package/dist/database/host.js +41 -0
  119. package/dist/database/host.js.map +1 -0
  120. package/dist/database/index.d.ts +40 -0
  121. package/dist/database/index.d.ts.map +1 -0
  122. package/dist/database/index.js +47 -0
  123. package/dist/database/index.js.map +1 -0
  124. package/dist/database/initialize-from-app.d.ts +28 -0
  125. package/dist/database/initialize-from-app.d.ts.map +1 -0
  126. package/dist/database/initialize-from-app.js +23 -0
  127. package/dist/database/initialize-from-app.js.map +1 -0
  128. package/dist/database/ir/handler.d.ts +6 -0
  129. package/dist/database/ir/handler.d.ts.map +1 -0
  130. package/dist/database/ir/handler.js +52 -0
  131. package/dist/database/ir/handler.js.map +1 -0
  132. package/dist/database/ir/spec.d.ts +25 -0
  133. package/dist/database/ir/spec.d.ts.map +1 -0
  134. package/dist/database/ir/spec.js +11 -0
  135. package/dist/database/ir/spec.js.map +1 -0
  136. package/dist/database/mapper.d.ts +9 -0
  137. package/dist/database/mapper.d.ts.map +1 -0
  138. package/dist/database/mapper.js +115 -0
  139. package/dist/database/mapper.js.map +1 -0
  140. package/dist/database/modular.d.ts +604 -0
  141. package/dist/database/modular.d.ts.map +1 -0
  142. package/dist/database/modular.js +1226 -0
  143. package/dist/database/modular.js.map +1 -0
  144. package/dist/database/replay.d.ts +33 -0
  145. package/dist/database/replay.d.ts.map +1 -0
  146. package/dist/database/replay.js +124 -0
  147. package/dist/database/replay.js.map +1 -0
  148. package/dist/database/resolver.d.ts +4 -0
  149. package/dist/database/resolver.d.ts.map +1 -0
  150. package/dist/database/resolver.js +66 -0
  151. package/dist/database/resolver.js.map +1 -0
  152. package/dist/database/sandbox/backend.d.ts +403 -0
  153. package/dist/database/sandbox/backend.d.ts.map +1 -0
  154. package/dist/database/sandbox/backend.js +1467 -0
  155. package/dist/database/sandbox/backend.js.map +1 -0
  156. package/dist/database/sandbox/data-tree.d.ts +123 -0
  157. package/dist/database/sandbox/data-tree.d.ts.map +1 -0
  158. package/dist/database/sandbox/data-tree.js +310 -0
  159. package/dist/database/sandbox/data-tree.js.map +1 -0
  160. package/dist/database/sandbox/normalize.d.ts +77 -0
  161. package/dist/database/sandbox/normalize.d.ts.map +1 -0
  162. package/dist/database/sandbox/normalize.js +165 -0
  163. package/dist/database/sandbox/normalize.js.map +1 -0
  164. package/dist/database/sandbox/push-id.d.ts +32 -0
  165. package/dist/database/sandbox/push-id.d.ts.map +1 -0
  166. package/dist/database/sandbox/push-id.js +82 -0
  167. package/dist/database/sandbox/push-id.js.map +1 -0
  168. package/dist/database/sandbox/query.d.ts +175 -0
  169. package/dist/database/sandbox/query.d.ts.map +1 -0
  170. package/dist/database/sandbox/query.js +269 -0
  171. package/dist/database/sandbox/query.js.map +1 -0
  172. package/dist/database/sandbox/rules-eval.d.ts +69 -0
  173. package/dist/database/sandbox/rules-eval.d.ts.map +1 -0
  174. package/dist/database/sandbox/rules-eval.js +124 -0
  175. package/dist/database/sandbox/rules-eval.js.map +1 -0
  176. package/dist/database/sandbox/sentinels.d.ts +71 -0
  177. package/dist/database/sandbox/sentinels.d.ts.map +1 -0
  178. package/dist/database/sandbox/sentinels.js +92 -0
  179. package/dist/database/sandbox/sentinels.js.map +1 -0
  180. package/dist/database/simulation/handler.d.ts +6 -0
  181. package/dist/database/simulation/handler.d.ts.map +1 -0
  182. package/dist/database/simulation/handler.js +236 -0
  183. package/dist/database/simulation/handler.js.map +1 -0
  184. package/dist/database/simulation/spec.d.ts +69 -0
  185. package/dist/database/simulation/spec.d.ts.map +1 -0
  186. package/dist/database/simulation/spec.js +25 -0
  187. package/dist/database/simulation/spec.js.map +1 -0
  188. package/dist/database/tools.d.ts +22 -0
  189. package/dist/database/tools.d.ts.map +1 -0
  190. package/dist/database/tools.js +272 -0
  191. package/dist/database/tools.js.map +1 -0
  192. package/dist/database/types.d.ts +210 -0
  193. package/dist/database/types.d.ts.map +1 -0
  194. package/dist/database/types.js +26 -0
  195. package/dist/database/types.js.map +1 -0
  196. package/dist/database/write/handler.d.ts +7 -0
  197. package/dist/database/write/handler.d.ts.map +1 -0
  198. package/dist/database/write/handler.js +42 -0
  199. package/dist/database/write/handler.js.map +1 -0
  200. package/dist/database/write/spec.d.ts +18 -0
  201. package/dist/database/write/spec.d.ts.map +1 -0
  202. package/dist/database/write/spec.js +7 -0
  203. package/dist/database/write/spec.js.map +1 -0
  204. package/dist/firestore/index.d.ts +533 -0
  205. package/dist/firestore/index.d.ts.map +1 -0
  206. package/dist/firestore/index.js +1536 -0
  207. package/dist/firestore/index.js.map +1 -0
  208. package/dist/firestore/tools.d.ts +67 -0
  209. package/dist/firestore/tools.d.ts.map +1 -0
  210. package/dist/firestore/tools.js +318 -0
  211. package/dist/firestore/tools.js.map +1 -0
  212. package/dist/firestore-values/index.d.ts +63 -0
  213. package/dist/firestore-values/index.d.ts.map +1 -0
  214. package/dist/firestore-values/index.js +148 -0
  215. package/dist/firestore-values/index.js.map +1 -0
  216. package/dist/project-scope.d.ts +5 -0
  217. package/dist/project-scope.d.ts.map +1 -0
  218. package/dist/project-scope.js +2 -0
  219. package/dist/project-scope.js.map +1 -0
  220. package/dist/rules/extract.d.ts +28 -0
  221. package/dist/rules/extract.d.ts.map +1 -0
  222. package/dist/rules/extract.js +25 -0
  223. package/dist/rules/extract.js.map +1 -0
  224. package/dist/rules/generators/assembler.d.ts +19 -0
  225. package/dist/rules/generators/assembler.d.ts.map +1 -0
  226. package/dist/rules/generators/assembler.js +292 -0
  227. package/dist/rules/generators/assembler.js.map +1 -0
  228. package/dist/rules/generators/expressions.d.ts +52 -0
  229. package/dist/rules/generators/expressions.d.ts.map +1 -0
  230. package/dist/rules/generators/expressions.js +117 -0
  231. package/dist/rules/generators/expressions.js.map +1 -0
  232. package/dist/rules/generators/grid.d.ts +22 -0
  233. package/dist/rules/generators/grid.d.ts.map +1 -0
  234. package/dist/rules/generators/grid.js +52 -0
  235. package/dist/rules/generators/grid.js.map +1 -0
  236. package/dist/rules/grammar/FirestoreAST.d.ts +129 -0
  237. package/dist/rules/grammar/FirestoreAST.d.ts.map +1 -0
  238. package/dist/rules/grammar/FirestoreAST.js +3 -0
  239. package/dist/rules/grammar/FirestoreAST.js.map +1 -0
  240. package/dist/rules/grammar/FirestoreAssembler.d.ts +5 -0
  241. package/dist/rules/grammar/FirestoreAssembler.d.ts.map +1 -0
  242. package/dist/rules/grammar/FirestoreAssembler.js +158 -0
  243. package/dist/rules/grammar/FirestoreAssembler.js.map +1 -0
  244. package/dist/rules/grammar/FirestoreParser.d.ts +46 -0
  245. package/dist/rules/grammar/FirestoreParser.d.ts.map +1 -0
  246. package/dist/rules/grammar/FirestoreParser.js +409 -0
  247. package/dist/rules/grammar/FirestoreParser.js.map +1 -0
  248. package/dist/rules/grammar/FirestoreRules.ohm +229 -0
  249. package/dist/rules/grammar/FirestoreRules.ohm.generated.d.ts +2 -0
  250. package/dist/rules/grammar/FirestoreRules.ohm.generated.d.ts.map +1 -0
  251. package/dist/rules/grammar/FirestoreRules.ohm.generated.js +234 -0
  252. package/dist/rules/grammar/FirestoreRules.ohm.generated.js.map +1 -0
  253. package/dist/rules/grammar/FirestoreValidator.d.ts +10 -0
  254. package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -0
  255. package/dist/rules/grammar/FirestoreValidator.js +482 -0
  256. package/dist/rules/grammar/FirestoreValidator.js.map +1 -0
  257. package/dist/rules/index.d.ts +67 -0
  258. package/dist/rules/index.d.ts.map +1 -0
  259. package/dist/rules/index.js +80 -0
  260. package/dist/rules/index.js.map +1 -0
  261. package/dist/rules/indexes/extract/annotation-collect.d.ts +42 -0
  262. package/dist/rules/indexes/extract/annotation-collect.d.ts.map +1 -0
  263. package/dist/rules/indexes/extract/annotation-collect.js +69 -0
  264. package/dist/rules/indexes/extract/annotation-collect.js.map +1 -0
  265. package/dist/rules/indexes/extract/annotations.d.ts +50 -0
  266. package/dist/rules/indexes/extract/annotations.d.ts.map +1 -0
  267. package/dist/rules/indexes/extract/annotations.js +144 -0
  268. package/dist/rules/indexes/extract/annotations.js.map +1 -0
  269. package/dist/rules/indexes/extract/ast.d.ts +54 -0
  270. package/dist/rules/indexes/extract/ast.d.ts.map +1 -0
  271. package/dist/rules/indexes/extract/ast.js +119 -0
  272. package/dist/rules/indexes/extract/ast.js.map +1 -0
  273. package/dist/rules/indexes/extract/classify.d.ts +41 -0
  274. package/dist/rules/indexes/extract/classify.d.ts.map +1 -0
  275. package/dist/rules/indexes/extract/classify.js +138 -0
  276. package/dist/rules/indexes/extract/classify.js.map +1 -0
  277. package/dist/rules/indexes/extract/composite.d.ts +38 -0
  278. package/dist/rules/indexes/extract/composite.d.ts.map +1 -0
  279. package/dist/rules/indexes/extract/composite.js +85 -0
  280. package/dist/rules/indexes/extract/composite.js.map +1 -0
  281. package/dist/rules/indexes/extract/dataflow.d.ts +45 -0
  282. package/dist/rules/indexes/extract/dataflow.d.ts.map +1 -0
  283. package/dist/rules/indexes/extract/dataflow.js +200 -0
  284. package/dist/rules/indexes/extract/dataflow.js.map +1 -0
  285. package/dist/rules/indexes/extract/enumerate.d.ts +41 -0
  286. package/dist/rules/indexes/extract/enumerate.d.ts.map +1 -0
  287. package/dist/rules/indexes/extract/enumerate.js +163 -0
  288. package/dist/rules/indexes/extract/enumerate.js.map +1 -0
  289. package/dist/rules/indexes/extract/extractor.d.ts +3 -0
  290. package/dist/rules/indexes/extract/extractor.d.ts.map +1 -0
  291. package/dist/rules/indexes/extract/extractor.js +274 -0
  292. package/dist/rules/indexes/extract/extractor.js.map +1 -0
  293. package/dist/rules/indexes/extract/types.d.ts +211 -0
  294. package/dist/rules/indexes/extract/types.d.ts.map +1 -0
  295. package/dist/rules/indexes/extract/types.js +14 -0
  296. package/dist/rules/indexes/extract/types.js.map +1 -0
  297. package/dist/rules/indexes/extractHandler.d.ts +16 -0
  298. package/dist/rules/indexes/extractHandler.d.ts.map +1 -0
  299. package/dist/rules/indexes/extractHandler.js +100 -0
  300. package/dist/rules/indexes/extractHandler.js.map +1 -0
  301. package/dist/rules/indexes/extractTool.d.ts +15 -0
  302. package/dist/rules/indexes/extractTool.d.ts.map +1 -0
  303. package/dist/rules/indexes/extractTool.js +55 -0
  304. package/dist/rules/indexes/extractTool.js.map +1 -0
  305. package/dist/rules/indexes/types.d.ts +71 -0
  306. package/dist/rules/indexes/types.d.ts.map +1 -0
  307. package/dist/rules/indexes/types.js +15 -0
  308. package/dist/rules/indexes/types.js.map +1 -0
  309. package/dist/rules/inspect/handler.d.ts +6 -0
  310. package/dist/rules/inspect/handler.d.ts.map +1 -0
  311. package/dist/rules/inspect/handler.js +144 -0
  312. package/dist/rules/inspect/handler.js.map +1 -0
  313. package/dist/rules/inspect/spec.d.ts +37 -0
  314. package/dist/rules/inspect/spec.d.ts.map +1 -0
  315. package/dist/rules/inspect/spec.js +2 -0
  316. package/dist/rules/inspect/spec.js.map +1 -0
  317. package/dist/rules/inspect/tools.d.ts +21 -0
  318. package/dist/rules/inspect/tools.d.ts.map +1 -0
  319. package/dist/rules/inspect/tools.js +21 -0
  320. package/dist/rules/inspect/tools.js.map +1 -0
  321. package/dist/rules/linter/ast-utils.d.ts +97 -0
  322. package/dist/rules/linter/ast-utils.d.ts.map +1 -0
  323. package/dist/rules/linter/ast-utils.js +491 -0
  324. package/dist/rules/linter/ast-utils.js.map +1 -0
  325. package/dist/rules/linter/hallucinations.d.ts +34 -0
  326. package/dist/rules/linter/hallucinations.d.ts.map +1 -0
  327. package/dist/rules/linter/hallucinations.js +334 -0
  328. package/dist/rules/linter/hallucinations.js.map +1 -0
  329. package/dist/rules/linter/linter.d.ts +84 -0
  330. package/dist/rules/linter/linter.d.ts.map +1 -0
  331. package/dist/rules/linter/linter.js +784 -0
  332. package/dist/rules/linter/linter.js.map +1 -0
  333. package/dist/rules/modules/resolver-browser.d.ts +41 -0
  334. package/dist/rules/modules/resolver-browser.d.ts.map +1 -0
  335. package/dist/rules/modules/resolver-browser.js +63 -0
  336. package/dist/rules/modules/resolver-browser.js.map +1 -0
  337. package/dist/rules/modules/resolver-core.d.ts +51 -0
  338. package/dist/rules/modules/resolver-core.d.ts.map +1 -0
  339. package/dist/rules/modules/resolver-core.js +338 -0
  340. package/dist/rules/modules/resolver-core.js.map +1 -0
  341. package/dist/rules/modules/resolver.d.ts +17 -0
  342. package/dist/rules/modules/resolver.d.ts.map +1 -0
  343. package/dist/rules/modules/resolver.js +49 -0
  344. package/dist/rules/modules/resolver.js.map +1 -0
  345. package/dist/rules/modules/stdlib/atomic.rules +52 -0
  346. package/dist/rules/modules/stdlib/atomic.test.json +152 -0
  347. package/dist/rules/modules/stdlib/auth.rules +7 -0
  348. package/dist/rules/modules/stdlib/auth.test.json +53 -0
  349. package/dist/rules/modules/stdlib/content.rules +50 -0
  350. package/dist/rules/modules/stdlib/content.test.json +238 -0
  351. package/dist/rules/modules/stdlib/counters.rules +41 -0
  352. package/dist/rules/modules/stdlib/counters.test.json +254 -0
  353. package/dist/rules/modules/stdlib/geometry.rules +43 -0
  354. package/dist/rules/modules/stdlib/geometry.test.json +200 -0
  355. package/dist/rules/modules/stdlib/joining.rules +49 -0
  356. package/dist/rules/modules/stdlib/joining.test.json +287 -0
  357. package/dist/rules/modules/stdlib/lifecycle.rules +51 -0
  358. package/dist/rules/modules/stdlib/lifecycle.test.json +288 -0
  359. package/dist/rules/modules/stdlib/lobby.rules +44 -0
  360. package/dist/rules/modules/stdlib/lobby.test.json +32 -0
  361. package/dist/rules/modules/stdlib/membership.rules +40 -0
  362. package/dist/rules/modules/stdlib/membership.test.json +107 -0
  363. package/dist/rules/modules/stdlib/spaces.rules +46 -0
  364. package/dist/rules/modules/stdlib/spaces.test.json +392 -0
  365. package/dist/rules/modules/stdlib/state.rules +28 -0
  366. package/dist/rules/modules/stdlib/state.test.json +55 -0
  367. package/dist/rules/modules/stdlib/timing.rules +20 -0
  368. package/dist/rules/modules/stdlib/timing.test.json +106 -0
  369. package/dist/rules/modules/stdlib/transitions.rules +34 -0
  370. package/dist/rules/modules/stdlib/transitions.test.json +86 -0
  371. package/dist/rules/modules/stdlib/turns.rules +27 -0
  372. package/dist/rules/modules/stdlib/turns.test.json +64 -0
  373. package/dist/rules/modules/stdlib/validation.rules +31 -0
  374. package/dist/rules/modules/stdlib/validation.test.json +184 -0
  375. package/dist/rules/modules/stdlib-content.d.ts +11 -0
  376. package/dist/rules/modules/stdlib-content.d.ts.map +1 -0
  377. package/dist/rules/modules/stdlib-content.js +590 -0
  378. package/dist/rules/modules/stdlib-content.js.map +1 -0
  379. package/dist/rules/node.d.ts +17 -0
  380. package/dist/rules/node.d.ts.map +1 -0
  381. package/dist/rules/node.js +21 -0
  382. package/dist/rules/node.js.map +1 -0
  383. package/dist/rules/rtdb.d.ts +28 -0
  384. package/dist/rules/rtdb.d.ts.map +1 -0
  385. package/dist/rules/rtdb.js +15 -0
  386. package/dist/rules/rtdb.js.map +1 -0
  387. package/dist/rules/simulator/evaluator.d.ts +179 -0
  388. package/dist/rules/simulator/evaluator.d.ts.map +1 -0
  389. package/dist/rules/simulator/evaluator.js +1221 -0
  390. package/dist/rules/simulator/evaluator.js.map +1 -0
  391. package/dist/rules/simulator/expression/eval-errors.d.ts +86 -0
  392. package/dist/rules/simulator/expression/eval-errors.d.ts.map +1 -0
  393. package/dist/rules/simulator/expression/eval-errors.js +68 -0
  394. package/dist/rules/simulator/expression/eval-errors.js.map +1 -0
  395. package/dist/rules/simulator/expression/evaluator.d.ts +43 -0
  396. package/dist/rules/simulator/expression/evaluator.d.ts.map +1 -0
  397. package/dist/rules/simulator/expression/evaluator.js +298 -0
  398. package/dist/rules/simulator/expression/evaluator.js.map +1 -0
  399. package/dist/rules/simulator/expression/lexer.d.ts +36 -0
  400. package/dist/rules/simulator/expression/lexer.d.ts.map +1 -0
  401. package/dist/rules/simulator/expression/lexer.js +318 -0
  402. package/dist/rules/simulator/expression/lexer.js.map +1 -0
  403. package/dist/rules/simulator/expression/parser.d.ts +39 -0
  404. package/dist/rules/simulator/expression/parser.d.ts.map +1 -0
  405. package/dist/rules/simulator/expression/parser.js +329 -0
  406. package/dist/rules/simulator/expression/parser.js.map +1 -0
  407. package/dist/rules/simulator/expression/types.d.ts +137 -0
  408. package/dist/rules/simulator/expression/types.d.ts.map +1 -0
  409. package/dist/rules/simulator/expression/types.js +90 -0
  410. package/dist/rules/simulator/expression/types.js.map +1 -0
  411. package/dist/rules/simulator/expression/walk-data.d.ts +52 -0
  412. package/dist/rules/simulator/expression/walk-data.d.ts.map +1 -0
  413. package/dist/rules/simulator/expression/walk-data.js +167 -0
  414. package/dist/rules/simulator/expression/walk-data.js.map +1 -0
  415. package/dist/rules/simulator/firestore-set.d.ts +25 -0
  416. package/dist/rules/simulator/firestore-set.d.ts.map +1 -0
  417. package/dist/rules/simulator/firestore-set.js +89 -0
  418. package/dist/rules/simulator/firestore-set.js.map +1 -0
  419. package/dist/rules/simulator/handler.d.ts +36 -0
  420. package/dist/rules/simulator/handler.d.ts.map +1 -0
  421. package/dist/rules/simulator/handler.js +583 -0
  422. package/dist/rules/simulator/handler.js.map +1 -0
  423. package/dist/rules/simulator/mapdiff.d.ts +29 -0
  424. package/dist/rules/simulator/mapdiff.d.ts.map +1 -0
  425. package/dist/rules/simulator/mapdiff.js +72 -0
  426. package/dist/rules/simulator/mapdiff.js.map +1 -0
  427. package/dist/rules/simulator/project-after-state.d.ts +33 -0
  428. package/dist/rules/simulator/project-after-state.d.ts.map +1 -0
  429. package/dist/rules/simulator/project-after-state.js +88 -0
  430. package/dist/rules/simulator/project-after-state.js.map +1 -0
  431. package/dist/rules/simulator/query-proof.d.ts +80 -0
  432. package/dist/rules/simulator/query-proof.d.ts.map +1 -0
  433. package/dist/rules/simulator/query-proof.js +142 -0
  434. package/dist/rules/simulator/query-proof.js.map +1 -0
  435. package/dist/rules/simulator/value-equality.d.ts +8 -0
  436. package/dist/rules/simulator/value-equality.d.ts.map +1 -0
  437. package/dist/rules/simulator/value-equality.js +40 -0
  438. package/dist/rules/simulator/value-equality.js.map +1 -0
  439. package/dist/rules/simulator/wrappers/base.d.ts +129 -0
  440. package/dist/rules/simulator/wrappers/base.d.ts.map +1 -0
  441. package/dist/rules/simulator/wrappers/base.js +90 -0
  442. package/dist/rules/simulator/wrappers/base.js.map +1 -0
  443. package/dist/rules/simulator/wrappers/bytes.d.ts +45 -0
  444. package/dist/rules/simulator/wrappers/bytes.d.ts.map +1 -0
  445. package/dist/rules/simulator/wrappers/bytes.js +130 -0
  446. package/dist/rules/simulator/wrappers/bytes.js.map +1 -0
  447. package/dist/rules/simulator/wrappers/duration.d.ts +76 -0
  448. package/dist/rules/simulator/wrappers/duration.d.ts.map +1 -0
  449. package/dist/rules/simulator/wrappers/duration.js +176 -0
  450. package/dist/rules/simulator/wrappers/duration.js.map +1 -0
  451. package/dist/rules/simulator/wrappers/float.d.ts +52 -0
  452. package/dist/rules/simulator/wrappers/float.d.ts.map +1 -0
  453. package/dist/rules/simulator/wrappers/float.js +88 -0
  454. package/dist/rules/simulator/wrappers/float.js.map +1 -0
  455. package/dist/rules/simulator/wrappers/latlng.d.ts +40 -0
  456. package/dist/rules/simulator/wrappers/latlng.d.ts.map +1 -0
  457. package/dist/rules/simulator/wrappers/latlng.js +100 -0
  458. package/dist/rules/simulator/wrappers/latlng.js.map +1 -0
  459. package/dist/rules/simulator/wrappers/path.d.ts +75 -0
  460. package/dist/rules/simulator/wrappers/path.d.ts.map +1 -0
  461. package/dist/rules/simulator/wrappers/path.js +158 -0
  462. package/dist/rules/simulator/wrappers/path.js.map +1 -0
  463. package/dist/rules/simulator/wrappers/reference.d.ts +82 -0
  464. package/dist/rules/simulator/wrappers/reference.d.ts.map +1 -0
  465. package/dist/rules/simulator/wrappers/reference.js +145 -0
  466. package/dist/rules/simulator/wrappers/reference.js.map +1 -0
  467. package/dist/rules/simulator/wrappers/timestamp.d.ts +86 -0
  468. package/dist/rules/simulator/wrappers/timestamp.d.ts.map +1 -0
  469. package/dist/rules/simulator/wrappers/timestamp.js +207 -0
  470. package/dist/rules/simulator/wrappers/timestamp.js.map +1 -0
  471. package/dist/rules/simulator/wrappers/vector.d.ts +43 -0
  472. package/dist/rules/simulator/wrappers/vector.d.ts.map +1 -0
  473. package/dist/rules/simulator/wrappers/vector.js +80 -0
  474. package/dist/rules/simulator/wrappers/vector.js.map +1 -0
  475. package/dist/rules/simulator-tools-impl.d.ts +64 -0
  476. package/dist/rules/simulator-tools-impl.d.ts.map +1 -0
  477. package/dist/rules/simulator-tools-impl.js +462 -0
  478. package/dist/rules/simulator-tools-impl.js.map +1 -0
  479. package/dist/rules/simulator.d.ts +20 -0
  480. package/dist/rules/simulator.d.ts.map +1 -0
  481. package/dist/rules/simulator.js +24 -0
  482. package/dist/rules/simulator.js.map +1 -0
  483. package/dist/rules/stdlib-modules.d.ts +73 -0
  484. package/dist/rules/stdlib-modules.d.ts.map +1 -0
  485. package/dist/rules/stdlib-modules.js +849 -0
  486. package/dist/rules/stdlib-modules.js.map +1 -0
  487. package/dist/rules/stdlib-tools.d.ts +19 -0
  488. package/dist/rules/stdlib-tools.d.ts.map +1 -0
  489. package/dist/rules/stdlib-tools.js +164 -0
  490. package/dist/rules/stdlib-tools.js.map +1 -0
  491. package/dist/rules/test/handler.d.ts +9 -0
  492. package/dist/rules/test/handler.d.ts.map +1 -0
  493. package/dist/rules/test/handler.js +124 -0
  494. package/dist/rules/test/handler.js.map +1 -0
  495. package/dist/rules/test/spec.d.ts +517 -0
  496. package/dist/rules/test/spec.d.ts.map +1 -0
  497. package/dist/rules/test/spec.js +227 -0
  498. package/dist/rules/test/spec.js.map +1 -0
  499. package/dist/rules/tools.d.ts +41 -0
  500. package/dist/rules/tools.d.ts.map +1 -0
  501. package/dist/rules/tools.js +124 -0
  502. package/dist/rules/tools.js.map +1 -0
  503. package/dist/rules/write/handler.d.ts +6 -0
  504. package/dist/rules/write/handler.d.ts.map +1 -0
  505. package/dist/rules/write/handler.js +126 -0
  506. package/dist/rules/write/handler.js.map +1 -0
  507. package/dist/rules/write/spec.d.ts +28 -0
  508. package/dist/rules/write/spec.d.ts.map +1 -0
  509. package/dist/rules/write/spec.js +2 -0
  510. package/dist/rules/write/spec.js.map +1 -0
  511. package/dist/sandbox/admin-compat.d.ts +20 -0
  512. package/dist/sandbox/admin-compat.d.ts.map +1 -0
  513. package/dist/sandbox/admin-compat.js +19 -0
  514. package/dist/sandbox/admin-compat.js.map +1 -0
  515. package/dist/sandbox/admin-firestore/error-translation.d.ts +86 -0
  516. package/dist/sandbox/admin-firestore/error-translation.d.ts.map +1 -0
  517. package/dist/sandbox/admin-firestore/error-translation.js +204 -0
  518. package/dist/sandbox/admin-firestore/error-translation.js.map +1 -0
  519. package/dist/sandbox/admin-firestore/index.d.ts +169 -0
  520. package/dist/sandbox/admin-firestore/index.d.ts.map +1 -0
  521. package/dist/sandbox/admin-firestore/index.js +456 -0
  522. package/dist/sandbox/admin-firestore/index.js.map +1 -0
  523. package/dist/sandbox/admin-firestore/remote.d.ts +97 -0
  524. package/dist/sandbox/admin-firestore/remote.d.ts.map +1 -0
  525. package/dist/sandbox/admin-firestore/remote.js +735 -0
  526. package/dist/sandbox/admin-firestore/remote.js.map +1 -0
  527. package/dist/sandbox/branches/index.d.ts +141 -0
  528. package/dist/sandbox/branches/index.d.ts.map +1 -0
  529. package/dist/sandbox/branches/index.js +327 -0
  530. package/dist/sandbox/branches/index.js.map +1 -0
  531. package/dist/sandbox/firestore/admin-compat/batch.d.ts +34 -0
  532. package/dist/sandbox/firestore/admin-compat/batch.d.ts.map +1 -0
  533. package/dist/sandbox/firestore/admin-compat/batch.js +73 -0
  534. package/dist/sandbox/firestore/admin-compat/batch.js.map +1 -0
  535. package/dist/sandbox/firestore/admin-compat/doc-ref.d.ts +38 -0
  536. package/dist/sandbox/firestore/admin-compat/doc-ref.d.ts.map +1 -0
  537. package/dist/sandbox/firestore/admin-compat/doc-ref.js +154 -0
  538. package/dist/sandbox/firestore/admin-compat/doc-ref.js.map +1 -0
  539. package/dist/sandbox/firestore/admin-compat/firestore.d.ts +45 -0
  540. package/dist/sandbox/firestore/admin-compat/firestore.d.ts.map +1 -0
  541. package/dist/sandbox/firestore/admin-compat/firestore.js +114 -0
  542. package/dist/sandbox/firestore/admin-compat/firestore.js.map +1 -0
  543. package/dist/sandbox/firestore/admin-compat/index.d.ts +45 -0
  544. package/dist/sandbox/firestore/admin-compat/index.d.ts.map +1 -0
  545. package/dist/sandbox/firestore/admin-compat/index.js +27 -0
  546. package/dist/sandbox/firestore/admin-compat/index.js.map +1 -0
  547. package/dist/sandbox/firestore/admin-compat/paths.d.ts +37 -0
  548. package/dist/sandbox/firestore/admin-compat/paths.d.ts.map +1 -0
  549. package/dist/sandbox/firestore/admin-compat/paths.js +49 -0
  550. package/dist/sandbox/firestore/admin-compat/paths.js.map +1 -0
  551. package/dist/sandbox/firestore/admin-compat/query.d.ts +261 -0
  552. package/dist/sandbox/firestore/admin-compat/query.d.ts.map +1 -0
  553. package/dist/sandbox/firestore/admin-compat/query.js +704 -0
  554. package/dist/sandbox/firestore/admin-compat/query.js.map +1 -0
  555. package/dist/sandbox/firestore/admin-compat/read-translation.d.ts +8 -0
  556. package/dist/sandbox/firestore/admin-compat/read-translation.d.ts.map +1 -0
  557. package/dist/sandbox/firestore/admin-compat/read-translation.js +59 -0
  558. package/dist/sandbox/firestore/admin-compat/read-translation.js.map +1 -0
  559. package/dist/sandbox/firestore/admin-compat/snapshots.d.ts +39 -0
  560. package/dist/sandbox/firestore/admin-compat/snapshots.d.ts.map +1 -0
  561. package/dist/sandbox/firestore/admin-compat/snapshots.js +50 -0
  562. package/dist/sandbox/firestore/admin-compat/snapshots.js.map +1 -0
  563. package/dist/sandbox/firestore/admin-compat/transaction.d.ts +35 -0
  564. package/dist/sandbox/firestore/admin-compat/transaction.d.ts.map +1 -0
  565. package/dist/sandbox/firestore/admin-compat/transaction.js +88 -0
  566. package/dist/sandbox/firestore/admin-compat/transaction.js.map +1 -0
  567. package/dist/sandbox/firestore/admin-compat/types.d.ts +297 -0
  568. package/dist/sandbox/firestore/admin-compat/types.d.ts.map +1 -0
  569. package/dist/sandbox/firestore/admin-compat/types.js +127 -0
  570. package/dist/sandbox/firestore/admin-compat/types.js.map +1 -0
  571. package/dist/sandbox/firestore/admin-compat/value-order.d.ts +49 -0
  572. package/dist/sandbox/firestore/admin-compat/value-order.d.ts.map +1 -0
  573. package/dist/sandbox/firestore/admin-compat/value-order.js +172 -0
  574. package/dist/sandbox/firestore/admin-compat/value-order.js.map +1 -0
  575. package/dist/sandbox/firestore/auto-id.d.ts +26 -0
  576. package/dist/sandbox/firestore/auto-id.d.ts.map +1 -0
  577. package/dist/sandbox/firestore/auto-id.js +36 -0
  578. package/dist/sandbox/firestore/auto-id.js.map +1 -0
  579. package/dist/sandbox/firestore/converters/bytes-geopoint.d.ts +48 -0
  580. package/dist/sandbox/firestore/converters/bytes-geopoint.d.ts.map +1 -0
  581. package/dist/sandbox/firestore/converters/bytes-geopoint.js +85 -0
  582. package/dist/sandbox/firestore/converters/bytes-geopoint.js.map +1 -0
  583. package/dist/sandbox/firestore/converters/fieldvalue.d.ts +69 -0
  584. package/dist/sandbox/firestore/converters/fieldvalue.d.ts.map +1 -0
  585. package/dist/sandbox/firestore/converters/fieldvalue.js +166 -0
  586. package/dist/sandbox/firestore/converters/fieldvalue.js.map +1 -0
  587. package/dist/sandbox/firestore/converters/reference.d.ts +31 -0
  588. package/dist/sandbox/firestore/converters/reference.d.ts.map +1 -0
  589. package/dist/sandbox/firestore/converters/reference.js +51 -0
  590. package/dist/sandbox/firestore/converters/reference.js.map +1 -0
  591. package/dist/sandbox/firestore/converters/timestamp.d.ts +32 -0
  592. package/dist/sandbox/firestore/converters/timestamp.d.ts.map +1 -0
  593. package/dist/sandbox/firestore/converters/timestamp.js +63 -0
  594. package/dist/sandbox/firestore/converters/timestamp.js.map +1 -0
  595. package/dist/sandbox/firestore/converters/user-timestamp.d.ts +46 -0
  596. package/dist/sandbox/firestore/converters/user-timestamp.d.ts.map +1 -0
  597. package/dist/sandbox/firestore/converters/user-timestamp.js +65 -0
  598. package/dist/sandbox/firestore/converters/user-timestamp.js.map +1 -0
  599. package/dist/sandbox/firestore/converters/vector.d.ts +24 -0
  600. package/dist/sandbox/firestore/converters/vector.d.ts.map +1 -0
  601. package/dist/sandbox/firestore/converters/vector.js +53 -0
  602. package/dist/sandbox/firestore/converters/vector.js.map +1 -0
  603. package/dist/sandbox/firestore/errors.d.ts +82 -0
  604. package/dist/sandbox/firestore/errors.d.ts.map +1 -0
  605. package/dist/sandbox/firestore/errors.js +54 -0
  606. package/dist/sandbox/firestore/errors.js.map +1 -0
  607. package/dist/sandbox/firestore/event-log.d.ts +99 -0
  608. package/dist/sandbox/firestore/event-log.d.ts.map +1 -0
  609. package/dist/sandbox/firestore/event-log.js +67 -0
  610. package/dist/sandbox/firestore/event-log.js.map +1 -0
  611. package/dist/sandbox/firestore/field-merge.d.ts +64 -0
  612. package/dist/sandbox/firestore/field-merge.d.ts.map +1 -0
  613. package/dist/sandbox/firestore/field-merge.js +221 -0
  614. package/dist/sandbox/firestore/field-merge.js.map +1 -0
  615. package/dist/sandbox/firestore/list-query-proof.d.ts +77 -0
  616. package/dist/sandbox/firestore/list-query-proof.d.ts.map +1 -0
  617. package/dist/sandbox/firestore/list-query-proof.js +170 -0
  618. package/dist/sandbox/firestore/list-query-proof.js.map +1 -0
  619. package/dist/sandbox/firestore/local-environment.d.ts +806 -0
  620. package/dist/sandbox/firestore/local-environment.d.ts.map +1 -0
  621. package/dist/sandbox/firestore/local-environment.js +3028 -0
  622. package/dist/sandbox/firestore/local-environment.js.map +1 -0
  623. package/dist/sandbox/firestore/local-state.d.ts +196 -0
  624. package/dist/sandbox/firestore/local-state.d.ts.map +1 -0
  625. package/dist/sandbox/firestore/local-state.js +413 -0
  626. package/dist/sandbox/firestore/local-state.js.map +1 -0
  627. package/dist/sandbox/firestore/overlay-backing.d.ts +44 -0
  628. package/dist/sandbox/firestore/overlay-backing.d.ts.map +1 -0
  629. package/dist/sandbox/firestore/overlay-backing.js +82 -0
  630. package/dist/sandbox/firestore/overlay-backing.js.map +1 -0
  631. package/dist/sandbox/firestore/sentinel-capture.d.ts +30 -0
  632. package/dist/sandbox/firestore/sentinel-capture.d.ts.map +1 -0
  633. package/dist/sandbox/firestore/sentinel-capture.js +61 -0
  634. package/dist/sandbox/firestore/sentinel-capture.js.map +1 -0
  635. package/dist/sandbox/firestore/snapshot-listeners.d.ts +284 -0
  636. package/dist/sandbox/firestore/snapshot-listeners.d.ts.map +1 -0
  637. package/dist/sandbox/firestore/snapshot-listeners.js +194 -0
  638. package/dist/sandbox/firestore/snapshot-listeners.js.map +1 -0
  639. package/dist/sandbox/firestore/topk.d.ts +12 -0
  640. package/dist/sandbox/firestore/topk.d.ts.map +1 -0
  641. package/dist/sandbox/firestore/topk.js +42 -0
  642. package/dist/sandbox/firestore/topk.js.map +1 -0
  643. package/dist/sandbox/firestore/transaction-merge.d.ts +55 -0
  644. package/dist/sandbox/firestore/transaction-merge.d.ts.map +1 -0
  645. package/dist/sandbox/firestore/transaction-merge.js +107 -0
  646. package/dist/sandbox/firestore/transaction-merge.js.map +1 -0
  647. package/dist/sandbox/firestore/transaction-types.d.ts +152 -0
  648. package/dist/sandbox/firestore/transaction-types.d.ts.map +1 -0
  649. package/dist/sandbox/firestore/transaction-types.js +10 -0
  650. package/dist/sandbox/firestore/transaction-types.js.map +1 -0
  651. package/dist/sandbox/firestore/transaction.d.ts +79 -0
  652. package/dist/sandbox/firestore/transaction.d.ts.map +1 -0
  653. package/dist/sandbox/firestore/transaction.js +95 -0
  654. package/dist/sandbox/firestore/transaction.js.map +1 -0
  655. package/dist/sandbox/firestore/value-equality.d.ts +2 -0
  656. package/dist/sandbox/firestore/value-equality.d.ts.map +1 -0
  657. package/dist/sandbox/firestore/value-equality.js +35 -0
  658. package/dist/sandbox/firestore/value-equality.js.map +1 -0
  659. package/dist/sandbox/firestore/value-resolver.d.ts +172 -0
  660. package/dist/sandbox/firestore/value-resolver.d.ts.map +1 -0
  661. package/dist/sandbox/firestore/value-resolver.js +221 -0
  662. package/dist/sandbox/firestore/value-resolver.js.map +1 -0
  663. package/dist/sandbox/firestore/wire-encoder.d.ts +14 -0
  664. package/dist/sandbox/firestore/wire-encoder.d.ts.map +1 -0
  665. package/dist/sandbox/firestore/wire-encoder.js +130 -0
  666. package/dist/sandbox/firestore/wire-encoder.js.map +1 -0
  667. package/dist/sandbox/index.d.ts +50 -0
  668. package/dist/sandbox/index.d.ts.map +1 -0
  669. package/dist/sandbox/index.js +63 -0
  670. package/dist/sandbox/index.js.map +1 -0
  671. package/dist/sandbox/internal/index.d.ts +36 -0
  672. package/dist/sandbox/internal/index.d.ts.map +1 -0
  673. package/dist/sandbox/internal/index.js +72 -0
  674. package/dist/sandbox/internal/index.js.map +1 -0
  675. package/dist/sandbox/internal/sandbox-impl.d.ts +343 -0
  676. package/dist/sandbox/internal/sandbox-impl.d.ts.map +1 -0
  677. package/dist/sandbox/internal/sandbox-impl.js +718 -0
  678. package/dist/sandbox/internal/sandbox-impl.js.map +1 -0
  679. package/dist/sandbox/persistence/backends.d.ts +37 -0
  680. package/dist/sandbox/persistence/backends.d.ts.map +1 -0
  681. package/dist/sandbox/persistence/backends.js +202 -0
  682. package/dist/sandbox/persistence/backends.js.map +1 -0
  683. package/dist/sandbox/persistence/chunk-format.d.ts +58 -0
  684. package/dist/sandbox/persistence/chunk-format.d.ts.map +1 -0
  685. package/dist/sandbox/persistence/chunk-format.js +163 -0
  686. package/dist/sandbox/persistence/chunk-format.js.map +1 -0
  687. package/dist/sandbox/persistence/controller.d.ts +39 -0
  688. package/dist/sandbox/persistence/controller.d.ts.map +1 -0
  689. package/dist/sandbox/persistence/controller.js +605 -0
  690. package/dist/sandbox/persistence/controller.js.map +1 -0
  691. package/dist/sandbox/persistence/index.d.ts +13 -0
  692. package/dist/sandbox/persistence/index.d.ts.map +1 -0
  693. package/dist/sandbox/persistence/index.js +5 -0
  694. package/dist/sandbox/persistence/index.js.map +1 -0
  695. package/dist/sandbox/persistence/serialize.d.ts +53 -0
  696. package/dist/sandbox/persistence/serialize.d.ts.map +1 -0
  697. package/dist/sandbox/persistence/serialize.js +98 -0
  698. package/dist/sandbox/persistence/serialize.js.map +1 -0
  699. package/dist/sandbox/persistence/types.d.ts +118 -0
  700. package/dist/sandbox/persistence/types.d.ts.map +1 -0
  701. package/dist/sandbox/persistence/types.js +15 -0
  702. package/dist/sandbox/persistence/types.js.map +1 -0
  703. package/dist/sandbox/remote.d.ts +118 -0
  704. package/dist/sandbox/remote.d.ts.map +1 -0
  705. package/dist/sandbox/remote.js +60 -0
  706. package/dist/sandbox/remote.js.map +1 -0
  707. package/dist/sandbox/replay/index.d.ts +106 -0
  708. package/dist/sandbox/replay/index.d.ts.map +1 -0
  709. package/dist/sandbox/replay/index.js +246 -0
  710. package/dist/sandbox/replay/index.js.map +1 -0
  711. package/dist/sandbox/sandbox-context.d.ts +40 -0
  712. package/dist/sandbox/sandbox-context.d.ts.map +1 -0
  713. package/dist/sandbox/sandbox-context.js +72 -0
  714. package/dist/sandbox/sandbox-context.js.map +1 -0
  715. package/dist/sandbox/tab-sync/index.d.ts +120 -0
  716. package/dist/sandbox/tab-sync/index.d.ts.map +1 -0
  717. package/dist/sandbox/tab-sync/index.js +272 -0
  718. package/dist/sandbox/tab-sync/index.js.map +1 -0
  719. package/dist/sandbox/types.d.ts +1235 -0
  720. package/dist/sandbox/types.d.ts.map +1 -0
  721. package/dist/sandbox/types.js +46 -0
  722. package/dist/sandbox/types.js.map +1 -0
  723. package/dist/storage/admin/api.d.ts +206 -0
  724. package/dist/storage/admin/api.d.ts.map +1 -0
  725. package/dist/storage/admin/api.js +349 -0
  726. package/dist/storage/admin/api.js.map +1 -0
  727. package/dist/storage/admin/handler.d.ts +29 -0
  728. package/dist/storage/admin/handler.d.ts.map +1 -0
  729. package/dist/storage/admin/handler.js +82 -0
  730. package/dist/storage/admin/handler.js.map +1 -0
  731. package/dist/storage/admin/spec.d.ts +64 -0
  732. package/dist/storage/admin/spec.d.ts.map +1 -0
  733. package/dist/storage/admin/spec.js +8 -0
  734. package/dist/storage/admin/spec.js.map +1 -0
  735. package/dist/storage/admin/tools.d.ts +23 -0
  736. package/dist/storage/admin/tools.d.ts.map +1 -0
  737. package/dist/storage/admin/tools.js +90 -0
  738. package/dist/storage/admin/tools.js.map +1 -0
  739. package/dist/storage/download.d.ts +27 -0
  740. package/dist/storage/download.d.ts.map +1 -0
  741. package/dist/storage/download.js +136 -0
  742. package/dist/storage/download.js.map +1 -0
  743. package/dist/storage/enforce.d.ts +26 -0
  744. package/dist/storage/enforce.d.ts.map +1 -0
  745. package/dist/storage/enforce.js +16 -0
  746. package/dist/storage/enforce.js.map +1 -0
  747. package/dist/storage/errors.d.ts +46 -0
  748. package/dist/storage/errors.d.ts.map +1 -0
  749. package/dist/storage/errors.js +62 -0
  750. package/dist/storage/errors.js.map +1 -0
  751. package/dist/storage/index.d.ts +49 -0
  752. package/dist/storage/index.d.ts.map +1 -0
  753. package/dist/storage/index.js +60 -0
  754. package/dist/storage/index.js.map +1 -0
  755. package/dist/storage/internal.d.ts +18 -0
  756. package/dist/storage/internal.d.ts.map +1 -0
  757. package/dist/storage/internal.js +18 -0
  758. package/dist/storage/internal.js.map +1 -0
  759. package/dist/storage/list.d.ts +17 -0
  760. package/dist/storage/list.d.ts.map +1 -0
  761. package/dist/storage/list.js +91 -0
  762. package/dist/storage/list.js.map +1 -0
  763. package/dist/storage/metadata.d.ts +86 -0
  764. package/dist/storage/metadata.d.ts.map +1 -0
  765. package/dist/storage/metadata.js +167 -0
  766. package/dist/storage/metadata.js.map +1 -0
  767. package/dist/storage/persistence.d.ts +134 -0
  768. package/dist/storage/persistence.d.ts.map +1 -0
  769. package/dist/storage/persistence.js +132 -0
  770. package/dist/storage/persistence.js.map +1 -0
  771. package/dist/storage/reference.d.ts +63 -0
  772. package/dist/storage/reference.d.ts.map +1 -0
  773. package/dist/storage/reference.js +156 -0
  774. package/dist/storage/reference.js.map +1 -0
  775. package/dist/storage/rules.d.ts +176 -0
  776. package/dist/storage/rules.d.ts.map +1 -0
  777. package/dist/storage/rules.js +621 -0
  778. package/dist/storage/rules.js.map +1 -0
  779. package/dist/storage/service.d.ts +139 -0
  780. package/dist/storage/service.d.ts.map +1 -0
  781. package/dist/storage/service.js +241 -0
  782. package/dist/storage/service.js.map +1 -0
  783. package/dist/storage/upload.d.ts +43 -0
  784. package/dist/storage/upload.d.ts.map +1 -0
  785. package/dist/storage/upload.js +235 -0
  786. package/dist/storage/upload.js.map +1 -0
  787. package/package.json +110 -3
@@ -0,0 +1,1598 @@
1
+ /**
2
+ * Sandbox backend for `pyric/auth`.
3
+ *
4
+ * Owns four pieces of per-sandbox state:
5
+ * 1. In-memory user database — emails / passwords / customClaims
6
+ * seeded via `sandbox.seedUsers(auth, …)` plus accounts created
7
+ * through `createUserWithEmailAndPassword`.
8
+ * 2. Listener registries — one for `onAuthStateChanged`, one for
9
+ * `onIdTokenChanged`. Both fire on user change; the
10
+ * `onIdTokenChanged` registry ALSO fires on a forced ID-token
11
+ * refresh (matches prod — see oracle observation
12
+ * `auth-onidtokenchanged-force-refresh.json`).
13
+ * 3. Mock-result registry — pre-staged `UserCredential`s consumed by
14
+ * `signInWithPopup` / `signInWithCredential`. Keyed by
15
+ * `providerId`; one slot per provider.
16
+ * 4. Token cache — current ID token string + IdTokenResult per uid.
17
+ * A fresh token is minted on each setCurrentUser transition into
18
+ * a non-null user (new session = new token) and on each
19
+ * `getIdToken(forceRefresh=true)`. Subsequent
20
+ * `getIdToken(false)` reads return the cached token (matches
21
+ * prod — see `auth-getidtoken-force-refresh.json`).
22
+ *
23
+ * The "current user" itself lives on `sandbox.currentUser`. We don't
24
+ * shadow it — the source of truth is the sandbox so future
25
+ * `getFirestore(sandbox)` overloads (deferred follow-up PR) can read
26
+ * the same value per-call.
27
+ *
28
+ * `signInAnonymously` / `signInWithEmailAndPassword` / `signOut` /
29
+ * `sandbox.setUser` all flow through {@link setCurrentUser}, which
30
+ * writes `sandbox.currentUser` and fans out to both listener
31
+ * registries. The sandbox's own `currentUser` setter is idempotent,
32
+ * so calling it with the same identity twice is a no-op.
33
+ */
34
+ import { FirebaseError } from 'firebase/app';
35
+ import { emitSandboxEvent, makeServiceMutationEvent } from 'pyric/sandbox/internal';
36
+ import { USER_INTERNAL, } from './types.js';
37
+ /** Seed record for `sandbox.seedUsers`. */
38
+ /** Stand-in password for exported provider-flow identities that never had
39
+ * one — keeps `exportUsers` → `seedUsers` round-trips lossless without
40
+ * widening the `SeedUser` shape. Not a secret: sandbox-only. */
41
+ export const NO_PASSWORD_SENTINEL = '__pyric_no_password__';
42
+ /**
43
+ * Per-sandbox backend state. One instance per `getAuth(sandbox)` —
44
+ * memoized so repeat calls return the same backing store (matches
45
+ * `firebase/auth`'s `getAuth(app)` idempotency).
46
+ */
47
+ export class SandboxBackend {
48
+ /** Email → stored user. Lowercase keys so lookups are case-
49
+ * insensitive (matches upstream). */
50
+ usersByEmail = new Map();
51
+ /** UID → stored user. Lets `setUser` resolve customClaims for an
52
+ * arbitrary uid without scanning the email index. */
53
+ usersByUid = new Map();
54
+ /** onAuthStateChanged registrations. Array, NOT a Set, so duplicate
55
+ * observer functions register independently — upstream backs each
56
+ * registry with an array (`util/src/subscribe.ts:191` pushes; dups
57
+ * allowed) and `unsubscribeOne(i)` removes by *index*, so the same
58
+ * fn subscribed twice fires twice and one unsubscribe removes one
59
+ * registration (AUTH-B4). Each record carries its own initial-fire
60
+ * bookkeeping so a shared per-observer dedup can't suppress a
61
+ * resubscribe or a second registration of the same fn (AUTH-B3). */
62
+ authStateSubs = [];
63
+ /** onIdTokenChanged registrations. Same array semantics. */
64
+ idTokenSubs = [];
65
+ /** `subscribeUsers` subscribers — coarse "user DB changed"
66
+ * callbacks; listeners re-list via `listUsers`. */
67
+ userDbSubs = new Set();
68
+ /** Monotonic uid counter for `createUser` calls without a uid. */
69
+ nextAdminUserId = 1;
70
+ /** Pre-staged sign-in results, keyed by providerId. The one-shot tier
71
+ * of the popup/redirect resolver precedence (see `index.ts`
72
+ * `signInWithPopup`): consumed when no resolver is injected, so
73
+ * headless conformance fixtures stay deterministic. */
74
+ mockResults = new Map();
75
+ /** Injected popup/redirect resolver — the analog of browser
76
+ * `getAuth` wiring `browserPopupRedirectResolver`. Null until a host
77
+ * (the playground) installs one via `sandbox.setAuthFlowResolver`. */
78
+ resolver = null;
79
+ /** Pending `getRedirectResult` payload — set by `signInWithRedirect`,
80
+ * returned-and-cleared by `getRedirectResult` (one-shot, matches prod). */
81
+ redirectResult = null;
82
+ /** Monotonic uid counter for anonymous users. */
83
+ nextAnonymousId = 1;
84
+ /** Underlying sandbox — written to on every sign-in/out. */
85
+ sandbox;
86
+ /** Cached `User` for `auth.currentUser` snapshots. Recomputed when
87
+ * the sandbox's currentUser changes; null on signed-out. */
88
+ cachedUser = null;
89
+ /** The uid `onAuthStateChanged` was last notified with. Mirrors
90
+ * upstream's `lastNotifiedUid` (`auth_impl.ts:720`): auth-state only
91
+ * re-fires when this changes, so a same-uid profile-shape change or a
92
+ * same-uid re-sign-in does NOT re-fire auth-state (AUTH-B7). */
93
+ lastNotifiedUid = null;
94
+ /** True while {@link setCurrentUser} is driving a transition through
95
+ * the sandbox setter. The synchronous `onCurrentUserChanged`
96
+ * subscriber checks this so it doesn't double-notify — setCurrentUser
97
+ * owns the fan-out decision (id-token always, auth-state on uid
98
+ * change). The subscriber only notifies for EXTERNAL mutations
99
+ * (direct sandbox writes, `reset()`, another handle). */
100
+ applyingTransition = false;
101
+ /** Per-uid current ID token + result. Refreshed on each
102
+ * `getIdToken(forceRefresh=true)` and on each new sign-in
103
+ * transition (so a sign-out / sign-in round-trip mints a fresh
104
+ * token, matching prod's "new session = new token" semantics).
105
+ * Subsequent `getIdToken(false)` reads return the cached value. */
106
+ tokenCache = new Map();
107
+ /** Monotonic counter used to disambiguate freshly-minted tokens
108
+ * for the same uid + claims map. Without this, two refreshes that
109
+ * happen within the same millisecond would hash to the same
110
+ * string (claims map + iat tie-breaker not enough). */
111
+ nextTokenSerial = 1;
112
+ /**
113
+ * Current persistence mode — mirrors the real Firebase SDK's
114
+ * `setPersistence` semantics. Default is `'LOCAL'` (Firebase default:
115
+ * `browserLocalPersistence` keeps you signed in across reloads and
116
+ * browser restarts).
117
+ *
118
+ * Stored here on the backend so the persistence controller can read it
119
+ * without knowing about auth internals. The `auth/index.ts` top-level
120
+ * `setPersistence` function writes this via `setPersistenceMode`; the
121
+ * controller reads it via `getPersistenceMode` on every session save.
122
+ *
123
+ * Subscribers to persistence-mode changes (the set holding the
124
+ * controller's session-save callback) are notified by `setPersistenceMode`
125
+ * so a `setPersistence` call that changes the mode immediately migrates
126
+ * the session to the new store without waiting for the next sign-in.
127
+ */
128
+ persistenceMode = 'LOCAL';
129
+ /** Per-uid provider of the *current/most-recent* sign-in session —
130
+ * backs `IdTokenResult.signInProvider` and the synthesized
131
+ * `firebase.sign_in_provider` token claim. Written by each
132
+ * sign-in path via {@link setCurrentUser}'s `signInProvider`
133
+ * argument; `null` for identities driven directly through the
134
+ * test driver (`sandbox.setUser`), which has no prod analog. */
135
+ signInProviderByUid = new Map();
136
+ /**
137
+ * Sign-in provider enablement — mirrors a real Firebase project's
138
+ * Authentication → Sign-in method toggles. GATED at every provider
139
+ * entry point OF THE ENFORCING BACKEND (owner decision — full
140
+ * fidelity): `resolveFlow` (`signInWithPopup`/`signInWithRedirect`),
141
+ * `signInWithCredential`, `createUserWithEmailAndPassword`/
142
+ * `signInWithEmailAndPassword` (the `'password'` provider), and
143
+ * `signInAnonymously` (the `'anonymous'` provider). A disabled
144
+ * provider throws the same `auth/operation-not-allowed` prod throws
145
+ * — a code deliberately distinct from `auth/argument-error` (which
146
+ * keeps meaning "no resolver/mock wired" for an ENABLED-but-unmocked
147
+ * OAuth provider).
148
+ *
149
+ * DELEGATION ESCAPE HATCH ({@link providerEnforcementDelegated}): a
150
+ * backend that is merely a UI vehicle for a REMOTE authority — the
151
+ * served worker path's page-local sandbox, whose popup picker
152
+ * resolves identities that a SharedWorker then gates + signs in —
153
+ * can delegate enforcement to that authority. With delegation on,
154
+ * {@link assertProviderEnabled} is a no-op HERE and the authority's
155
+ * own (undelegated) backend gate is the one that decides.
156
+ *
157
+ * DEFAULTS DIVERGE FROM A FRESH REAL PROJECT (owner decision,
158
+ * documented): prod ships every provider OFF until an admin flips it
159
+ * on in the console. The sandbox ships `'password'` and `'anonymous'`
160
+ * ON — everything else (`google.com`, `github.com`, `apple.com`,
161
+ * `microsoft.com`, custom OAuth ids, …) OFF — so the enforcement is
162
+ * identical to prod but the DEFAULTS are friendlier: every existing
163
+ * sandbox flow that never called `sandbox.setAuthProviderConfig`
164
+ * keeps signing in via email/password and anonymous auth unchanged
165
+ * after this landed.
166
+ */
167
+ providerConfig = new Map([
168
+ ['password', true],
169
+ ['anonymous', true],
170
+ ]);
171
+ /** Subscribers to provider-config changes — the config analog of
172
+ * {@link userDbSubs}. Same coarse contract: no payload, re-read via
173
+ * {@link listProviderConfig} on each callback. */
174
+ providerConfigSubs = new Set();
175
+ /** When true, THIS backend's provider gate is delegated to a remote
176
+ * authority (see the {@link providerConfig} docstring's delegation
177
+ * section) — {@link assertProviderEnabled} becomes a no-op. Not
178
+ * persisted: a runtime wiring decision, not project config. */
179
+ providerEnforcementDelegated = false;
180
+ constructor(sandbox) {
181
+ this.sandbox = sandbox;
182
+ // Initialize cachedUser from whatever the sandbox currently has —
183
+ // a fresh sandbox starts at null, but if another Auth handle
184
+ // (or a manual mutation) already set a user, mirror it.
185
+ this.cachedUser = this.buildUserFromState(sandbox.currentUser);
186
+ // Subscribe to sandbox-level changes — covers cases where a
187
+ // future bridge (or another `getAuth` handle) mutates
188
+ // currentUser directly. Fans out to both listener registries.
189
+ //
190
+ // Critically, this must NOT clobber a richer `User` that
191
+ // `setCurrentUser` already cached for the SAME identity. Sign-in
192
+ // strategies (popup / redirect / credential / `setUser`) hand us a
193
+ // full `User` (email, displayName, anonymous flag) and stash it on
194
+ // `cachedUser` *before* writing `sandbox.currentUser`; that write
195
+ // fires this subscriber synchronously. Rebuilding from the bare
196
+ // `AuthState` here would drop every field `AuthState` can't carry
197
+ // (it only holds uid + claims) and mint a fresh object, breaking
198
+ // `cred.user === auth.currentUser`. So we only rebuild when the
199
+ // change came from *outside* this backend — i.e. the cached user's
200
+ // uid no longer matches the new state (a direct sandbox mutation,
201
+ // `reset()`, or another handle's sign-in). Matches upstream, where
202
+ // `_updateCurrentUser(userCredential.user)` makes the credential's
203
+ // user the live `currentUser` — same fields, same reference
204
+ // (`core/strategies/anonymous.ts:60-68`, `auth_impl.ts:714-719`).
205
+ sandbox.onCurrentUserChanged((state) => {
206
+ // `setCurrentUser` owns the fan-out for transitions it drives (it
207
+ // calls `notifyAuthListeners` itself with the right id-token /
208
+ // auth-state split). Skip here so we don't double-notify.
209
+ if (this.applyingTransition)
210
+ return;
211
+ const stateUid = state === null ? null : state.uid;
212
+ const cachedUid = this.cachedUser?.uid ?? null;
213
+ if (stateUid !== cachedUid) {
214
+ this.cachedUser = this.buildUserFromState(state);
215
+ }
216
+ // External mutation (direct sandbox write / `reset()` / another
217
+ // handle): notify with the same id-token-always / auth-state-on-
218
+ // uid-change rule.
219
+ this.notifyAuthListeners();
220
+ });
221
+ }
222
+ /** Snapshot of the current user — backs `auth.currentUser`. */
223
+ getCurrentUser() {
224
+ return this.cachedUser;
225
+ }
226
+ // ─── Listener registries ────────────────────────────────────────────
227
+ /**
228
+ * Subscribe to user change. Fires *immediately* on subscribe with
229
+ * the current value (mirrors `firebase/auth`'s
230
+ * `onAuthStateChanged` semantics).
231
+ *
232
+ * `kind: 'auth-state'` and `kind: 'id-token'` share the identity-
233
+ * transition fan-out path; additionally, `getIdToken(true)` fans
234
+ * out to the `'id-token'` registry only (without re-firing
235
+ * `'auth-state'`). Matches prod: `onIdTokenChanged` sees both
236
+ * identity changes AND forced token refreshes.
237
+ */
238
+ subscribe(kind, observer) {
239
+ const list = kind === 'auth-state' ? this.authStateSubs : this.idTokenSubs;
240
+ // One record per subscribe() — duplicate fns register independently
241
+ // (AUTH-B4). Each carries its own initial-fire bookkeeping, so a
242
+ // resubscribe or a second registration of the same fn is NOT
243
+ // suppressed by a sibling's delivery (AUTH-B3).
244
+ const reg = { observer, hasFired: false, lastValue: null };
245
+ list.push(reg);
246
+ // Fire immediately with the current value — upstream semantics
247
+ // (`auth_impl.ts:728-742`: each registration schedules its own
248
+ // `promise.then(() => cb(currentUser))`). Use a microtask so a
249
+ // synchronous unsubscribe from inside the observer doesn't race with
250
+ // fanOut's iteration, AND so a setCurrentUser that fires
251
+ // synchronously between subscribe and this microtask can deliver
252
+ // first; we then skip the replay only when THIS registration already
253
+ // saw the current value (avoids the same-value double-fire in the
254
+ // mount-time `onAuthStateChanged(auth, fn); signInAnonymously(auth)`
255
+ // pattern — a sandbox-only race prod can't exhibit, COMPAT row 31).
256
+ queueMicrotask(() => {
257
+ if (!list.includes(reg))
258
+ return;
259
+ if (reg.hasFired && reg.lastValue === this.cachedUser)
260
+ return;
261
+ this.deliverOne(reg, this.cachedUser);
262
+ });
263
+ return () => {
264
+ const i = list.indexOf(reg);
265
+ if (i !== -1)
266
+ list.splice(i, 1);
267
+ };
268
+ }
269
+ /**
270
+ * Notify the requested registries of the current user. Snapshots
271
+ * the registration list before iteration so unsubscribes during
272
+ * emission don't skip remaining subscribers (upstream semantics).
273
+ *
274
+ * `kind: 'both'` (default) fires both auth-state and id-token
275
+ * registries — used for identity transitions.
276
+ * `kind: 'id-token'` fires ONLY the id-token registry — used for
277
+ * forced token refreshes, where the user identity hasn't changed
278
+ * but the token has. Matches prod: `onIdTokenChanged` sees refresh
279
+ * events; `onAuthStateChanged` does not.
280
+ */
281
+ fanOut(kind = 'both') {
282
+ const user = this.cachedUser;
283
+ if (kind === 'both' || kind === 'auth-state') {
284
+ for (const reg of [...this.authStateSubs])
285
+ this.deliverOne(reg, user);
286
+ }
287
+ if (kind === 'both' || kind === 'id-token') {
288
+ for (const reg of [...this.idTokenSubs])
289
+ this.deliverOne(reg, user);
290
+ }
291
+ }
292
+ /**
293
+ * Mirror of upstream `notifyAuthListeners` (`auth_impl.ts:714-723`):
294
+ * - `onIdTokenChanged` fires on EVERY notify (every identity update,
295
+ * including a same-uid re-sign-in that mints a fresh token —
296
+ * AUTH-B8);
297
+ * - `onAuthStateChanged` fires ONLY when the uid changed vs the last
298
+ * notification (`lastNotifiedUid`) — a same-uid profile-shape
299
+ * change does NOT re-fire auth-state (AUTH-B7).
300
+ * Call this after `cachedUser` is updated.
301
+ */
302
+ notifyAuthListeners() {
303
+ this.fanOut('id-token');
304
+ const currentUid = this.cachedUser?.uid ?? null;
305
+ if (this.lastNotifiedUid !== currentUid) {
306
+ this.lastNotifiedUid = currentUid;
307
+ this.fanOut('auth-state');
308
+ }
309
+ }
310
+ deliverOne(reg, user) {
311
+ reg.hasFired = true;
312
+ reg.lastValue = user;
313
+ const observer = reg.observer;
314
+ try {
315
+ if (typeof observer === 'function') {
316
+ observer(user);
317
+ }
318
+ else if (observer.next) {
319
+ observer.next(user);
320
+ }
321
+ }
322
+ catch {
323
+ // Observational — swallow so a faulty subscriber can't
324
+ // destabilize the auth state machine.
325
+ }
326
+ }
327
+ // ─── User DB ────────────────────────────────────────────────────────
328
+ /** Build a fresh {@link StoredUser} with admin-field defaults. */
329
+ makeStored(init) {
330
+ return {
331
+ email: null,
332
+ password: null,
333
+ displayName: null,
334
+ phoneNumber: null,
335
+ photoUrl: null,
336
+ customClaims: {},
337
+ isAnonymous: false,
338
+ providerUserInfo: [],
339
+ disabled: false,
340
+ emailVerified: false,
341
+ createdAt: new Date().toISOString(),
342
+ lastLoginAt: null,
343
+ ...init,
344
+ };
345
+ }
346
+ /** Fan a coarse "user DB changed" notification out to
347
+ * `subscribeUsers` listeners. Snapshotted iteration; throwing
348
+ * listeners are isolated (same policy as the auth observers). */
349
+ notifyUsersChanged() {
350
+ for (const cb of [...this.userDbSubs]) {
351
+ try {
352
+ cb();
353
+ }
354
+ catch {
355
+ // Observational — swallow.
356
+ }
357
+ }
358
+ }
359
+ /**
360
+ * Emit an auth {@link ServiceMutationEvent} onto the sandbox's unified
361
+ * `onEvent`/`history()` stream (Pyric Studio keystone). Best-effort and
362
+ * fully isolated — a throw from the emit path (or a sandbox that somehow
363
+ * isn't a real `SandboxImpl`) must NOT break the auth operation that
364
+ * triggered it, so we swallow. `auth` defaults to the current sandbox
365
+ * identity; admin ops (createUser/updateUser/deleteUser) pass `null`
366
+ * since they have no acting session. `actor` defaults to `{ kind: 'app' }`
367
+ * via `stampProvenance`.
368
+ */
369
+ emitAuthEvent(op, fields = {}) {
370
+ try {
371
+ emitSandboxEvent(this.sandbox, makeServiceMutationEvent({
372
+ service: 'auth',
373
+ op,
374
+ path: fields.path,
375
+ auth: fields.auth ?? this.sandbox.currentUser,
376
+ before: fields.before,
377
+ after: fields.after,
378
+ detail: fields.detail,
379
+ }), { service: 'auth' });
380
+ }
381
+ catch {
382
+ // Observational — never let event emission destabilize auth.
383
+ }
384
+ }
385
+ /** Subscribe to user-DB mutations (seed/create/update/delete/clear,
386
+ * provider links, lastLoginAt bumps). Coarse: no payload, no
387
+ * initial fire — re-list via `listUsers` on each callback. */
388
+ subscribeUsers(cb) {
389
+ this.userDbSubs.add(cb);
390
+ return () => { this.userDbSubs.delete(cb); };
391
+ }
392
+ // ─── Provider config (sign-in method enablement) ─────────────────────
393
+ /** Whether `providerId` is currently enabled. Unknown providers
394
+ * (never toggled) default to `false` — matches a fresh real project,
395
+ * EXCEPT `'password'`/`'anonymous'`, which start `true` (see the
396
+ * {@link providerConfig} docstring for the rationale). */
397
+ isProviderEnabled(providerId) {
398
+ return this.providerConfig.get(providerId) ?? false;
399
+ }
400
+ /** Every provider this backend has an explicit enablement for —
401
+ * seeded defaults (`password`, `anonymous`) plus anything a host
402
+ * toggled via {@link setProviderConfig}. */
403
+ listProviderConfig() {
404
+ return [...this.providerConfig.entries()].map(([providerId, enabled]) => ({ providerId, enabled }));
405
+ }
406
+ /** Toggle a provider on/off. No-op (no notify) when the value is
407
+ * already what was requested — mirrors {@link setPersistenceMode}'s
408
+ * dedup so a redundant toggle doesn't churn subscribers / flushes. */
409
+ setProviderConfig(providerId, enabled) {
410
+ const before = this.providerConfig.get(providerId) ?? false;
411
+ if (before === enabled)
412
+ return;
413
+ this.providerConfig.set(providerId, enabled);
414
+ this.notifyProviderConfigChanged();
415
+ // No acting session for a config toggle (an admin-console-equivalent
416
+ // action) — same rationale as the user-admin ops (createUser/
417
+ // updateUser/…), which also pass `auth: null`. Rides the sandbox's
418
+ // unified event stream so a host subscribed to `onEvent`/`history()`
419
+ // (Pyric Studio's Action Center, and the worker's event feed that
420
+ // `subscribeUsers` already rides) sees provider-config changes too.
421
+ this.emitAuthEvent('provider_config_update', {
422
+ path: providerId,
423
+ auth: null,
424
+ before: { providerId, enabled: before },
425
+ after: { providerId, enabled },
426
+ });
427
+ }
428
+ notifyProviderConfigChanged() {
429
+ for (const cb of [...this.providerConfigSubs]) {
430
+ try {
431
+ cb();
432
+ }
433
+ catch {
434
+ // Observational — swallow.
435
+ }
436
+ }
437
+ }
438
+ /** Subscribe to provider-config mutations. Coarse: no payload, no
439
+ * initial fire — re-read via {@link listProviderConfig} on each
440
+ * callback (same contract as {@link subscribeUsers}). */
441
+ subscribeProviderConfig(cb) {
442
+ this.providerConfigSubs.add(cb);
443
+ return () => { this.providerConfigSubs.delete(cb); };
444
+ }
445
+ /**
446
+ * Mark this backend's provider gate as delegated to (or reclaimed
447
+ * from) a remote authority. See the {@link providerConfig} docstring:
448
+ * the served worker path sets this on the PAGE-LOCAL sandbox so the
449
+ * popup picker opens regardless of local toggles — the SharedWorker's
450
+ * `auth.acceptIdentity` gate (against the worker's own, undelegated
451
+ * backend) is the enforcement point.
452
+ */
453
+ setProviderEnforcementDelegated(delegated) {
454
+ this.providerEnforcementDelegated = delegated;
455
+ }
456
+ /**
457
+ * Gate a provider entry point. Throws real Firebase's
458
+ * `auth/operation-not-allowed` (exactly the code/shape prod throws
459
+ * for a disabled sign-in method) when `providerId` is off. Called by
460
+ * every provider-flow entry point in `index.ts` BEFORE any other
461
+ * work, so a disabled provider never touches the user DB / mock
462
+ * registry / resolver. A no-op when enforcement is delegated to a
463
+ * remote authority ({@link setProviderEnforcementDelegated}).
464
+ */
465
+ assertProviderEnabled(providerId) {
466
+ if (this.providerEnforcementDelegated)
467
+ return;
468
+ if (!this.isProviderEnabled(providerId)) {
469
+ throw makeAuthError('auth/operation-not-allowed', `${providerId} sign-in is disabled for this sandbox project. Enable it with ` +
470
+ `sandbox.setAuthProviderConfig(auth, '${providerId}', true).`);
471
+ }
472
+ }
473
+ /** Snapshot the full provider-config map for the persistable-service
474
+ * `snapshot()` hook — plain JSON, ready to embed under the `auth`
475
+ * service's `providers` key. */
476
+ exportProviderConfig() {
477
+ return Object.fromEntries(this.providerConfig);
478
+ }
479
+ /**
480
+ * Restore provider config from a persisted blob — a REPLACE, not a
481
+ * merge (same policy as `seedUsers`'s persistence path: the map
482
+ * becomes EXACTLY what `data` holds). When `data` is missing/invalid
483
+ * — including every blob written BEFORE this feature landed, which
484
+ * has no `providers` key at all — falls back to the documented
485
+ * defaults (`password`/`anonymous` enabled) rather than leaving the
486
+ * map empty, so an old `--persist` file doesn't silently lock
487
+ * existing sandbox flows out of email/password + anonymous sign-in.
488
+ */
489
+ restoreProviderConfig(data) {
490
+ this.providerConfig.clear();
491
+ if (data && typeof data === 'object') {
492
+ for (const [providerId, enabled] of Object.entries(data)) {
493
+ if (typeof enabled === 'boolean')
494
+ this.providerConfig.set(providerId, enabled);
495
+ }
496
+ }
497
+ else {
498
+ this.providerConfig.set('password', true);
499
+ this.providerConfig.set('anonymous', true);
500
+ }
501
+ this.notifyProviderConfigChanged();
502
+ }
503
+ // ─── Persistence mode ───────────────────────────────────────────────
504
+ /**
505
+ * Read the current persistence mode. Used by the session-persistence
506
+ * controller to decide which web-storage slot (local / session) to
507
+ * write the signed-in uid into.
508
+ *
509
+ * The default value is `'LOCAL'` — matches Firebase's default of
510
+ * `browserLocalPersistence` (you stay signed in across page reloads
511
+ * and browser restarts unless you explicitly call `setPersistence`).
512
+ */
513
+ getPersistenceMode() {
514
+ return this.persistenceMode;
515
+ }
516
+ /**
517
+ * Record the new persistence mode. Called by the auth `setPersistence`
518
+ * function when the sandbox target receives a mode change.
519
+ *
520
+ * Changing the mode notifies the session-change subscribers so the
521
+ * persistence controller can IMMEDIATELY migrate the stored uid to
522
+ * the new web-storage slot — matching the real SDK's behavior where
523
+ * `setPersistence` carries the active session across modes before
524
+ * returning.
525
+ *
526
+ * Why notify on mode change (not just on sign-in/out): the session
527
+ * controller subscribes to "something about this session changed"
528
+ * events and re-reads both `currentUid()` and `mode()` on each fire.
529
+ * Notifying here lets the controller react to a mode change with the
530
+ * same code path as a sign-in/out, rather than a separate migration
531
+ * hook.
532
+ */
533
+ setPersistenceMode(mode) {
534
+ if (this.persistenceMode === mode)
535
+ return; // no-op — nothing to migrate
536
+ this.persistenceMode = mode;
537
+ // Notify session-change subscribers so the controller migrates the
538
+ // stored uid to the new store immediately. The controller reads
539
+ // mode() on the fire, so it will see the already-updated value.
540
+ this.notifySessionChanged();
541
+ }
542
+ /**
543
+ * Subscribers that need to react to ANY session state change —
544
+ * sign-in, sign-out, or persistence-mode change. The controller
545
+ * installs one subscriber here to drive session saves.
546
+ *
547
+ * Separate from `currentUserSubs` (on the sandbox) because we need
548
+ * to fire on mode changes (which don't change the currentUser) and
549
+ * because the backend owns the mode — it's cleaner to keep both
550
+ * signals on the same object.
551
+ */
552
+ sessionChangeSubs = new Set();
553
+ /** Notify all session-change subscribers. Used by `setCurrentUser`
554
+ * (sign-in / sign-out) and by `setPersistenceMode` (mode change). */
555
+ notifySessionChanged() {
556
+ for (const cb of [...this.sessionChangeSubs]) {
557
+ try {
558
+ cb();
559
+ }
560
+ catch {
561
+ // Observational — swallow.
562
+ }
563
+ }
564
+ }
565
+ /**
566
+ * Subscribe to session-level changes: sign-in, sign-out, and
567
+ * persistence-mode changes. The persistence controller installs one
568
+ * subscription here and re-saves the session on every fire.
569
+ *
570
+ * This is the mechanism the `PersistableService.session.subscribe`
571
+ * hook exposes — the controller doesn't need to know it's subscribing
572
+ * to two separate event sources (sign-in/out AND mode); they both
573
+ * flow through this one subscription.
574
+ */
575
+ subscribeSession(cb) {
576
+ this.sessionChangeSubs.add(cb);
577
+ return () => { this.sessionChangeSubs.delete(cb); };
578
+ }
579
+ seedUsers(users) {
580
+ for (const u of users) {
581
+ // Re-seeding an existing uid OVERWRITES it (documented idempotency,
582
+ // `index.ts` seedUsers docstring). If the new email differs, drop
583
+ // the stale email→record mapping so the old email no longer signs
584
+ // in (AUTH-B9) — otherwise both emails would resolve, the old one
585
+ // to a now-orphaned record.
586
+ const prior = this.usersByUid.get(u.uid);
587
+ if (prior?.email && prior.email.toLowerCase() !== u.email.toLowerCase()) {
588
+ this.usersByEmail.delete(prior.email.toLowerCase());
589
+ }
590
+ const record = this.makeStored({
591
+ uid: u.uid,
592
+ email: u.email,
593
+ password: u.password,
594
+ displayName: u.displayName ?? null,
595
+ customClaims: u.customClaims ?? {},
596
+ providerUserInfo: [{ providerId: u.providerId ?? 'password' }],
597
+ });
598
+ this.usersByEmail.set(u.email.toLowerCase(), record);
599
+ this.usersByUid.set(u.uid, record);
600
+ }
601
+ if (users.length > 0)
602
+ this.notifyUsersChanged();
603
+ }
604
+ /**
605
+ * Export the user DB as `SeedUser`s — the exact shape `seedUsers`
606
+ * accepts, so export → seed round-trips (the persistence substrate,
607
+ * the design rationale section 3c). Identities with an email but no
608
+ * password (provider-flow users created via `createSignInCredential`)
609
+ * export with {@link NO_PASSWORD_SENTINEL} so they survive the
610
+ * round-trip (same trick hosts already use when seeding popup
611
+ * identities). Anonymous users (no email) are NOT exported — they are
612
+ * ephemeral by design; documented divergence from prod's persisted
613
+ * anonymous sessions.
614
+ */
615
+ exportUsers() {
616
+ const out = [];
617
+ for (const u of this.usersByUid.values()) {
618
+ if (u.email === null)
619
+ continue; // anonymous — not round-trippable
620
+ const seed = {
621
+ uid: u.uid,
622
+ email: u.email,
623
+ password: u.password ?? NO_PASSWORD_SENTINEL,
624
+ providerId: u.providerUserInfo[0]?.providerId ?? 'password',
625
+ };
626
+ if (u.displayName !== null)
627
+ seed.displayName = u.displayName;
628
+ if (Object.keys(u.customClaims).length > 0)
629
+ seed.customClaims = u.customClaims;
630
+ out.push(seed);
631
+ }
632
+ return out;
633
+ }
634
+ /**
635
+ * Re-establish a signed-in session for an EXISTING identity — the
636
+ * substrate behind web-storage session persistence (`setPersistence`
637
+ * parity at the serve layer). Fires the auth-state listeners exactly
638
+ * like a real restored session. Throws `auth/user-not-found` for
639
+ * unknown uids and `auth/user-disabled` for disabled accounts (a
640
+ * restore is a sign-in).
641
+ */
642
+ restoreSession(uid) {
643
+ const stored = this.usersByUid.get(uid);
644
+ if (!stored) {
645
+ throw makeAuthError('auth/user-not-found', `restoreSession: no identity with uid ${uid}.`);
646
+ }
647
+ if (stored.disabled) {
648
+ throw makeAuthError('auth/user-disabled', `restoreSession: user ${uid} is disabled.`);
649
+ }
650
+ const providerId = stored.isAnonymous
651
+ ? 'anonymous'
652
+ : (stored.providerUserInfo[0]?.providerId ?? 'password');
653
+ const user = this.buildUserFromStored(stored);
654
+ this.setCurrentUser(user, providerId);
655
+ return user;
656
+ }
657
+ findByEmail(email) {
658
+ return this.usersByEmail.get(email.toLowerCase());
659
+ }
660
+ createEmailPasswordUser(email, password) {
661
+ // Validate format BEFORE checking duplicates so an attacker can't
662
+ // enumerate seeded emails by varying the malformed-vs-valid shape.
663
+ // Matches prod's order — the format error fires before any lookup
664
+ // against the user DB.
665
+ validateEmailFormat(email);
666
+ validatePasswordStrength(password);
667
+ const key = email.toLowerCase();
668
+ if (this.usersByEmail.has(key)) {
669
+ throw makeAuthError('auth/email-already-in-use', `An account already exists for ${email}.`);
670
+ }
671
+ const uid = `email-${key}-${this.usersByEmail.size + 1}`;
672
+ const record = this.makeStored({
673
+ uid,
674
+ email,
675
+ password,
676
+ providerUserInfo: [{ providerId: 'password' }],
677
+ });
678
+ this.usersByEmail.set(key, record);
679
+ this.usersByUid.set(uid, record);
680
+ this.notifyUsersChanged();
681
+ // App-driven create (createUserWithEmailAndPassword) — the acting
682
+ // identity is whoever is currently signed in (usually null/anonymous).
683
+ this.emitAuthEvent('user_create', { path: uid, after: this.toRecord(record) });
684
+ return record;
685
+ }
686
+ // ─── Identity mutation ──────────────────────────────────────────────
687
+ /**
688
+ * Set the active user. Owns the fan-out decision directly (rather than
689
+ * leaning on the sandbox setter's structural-equality dedup), so it can
690
+ * mirror upstream `notifyAuthListeners`: `onIdTokenChanged` fires on
691
+ * every sign-in (incl. a same-uid re-sign-in that mints a fresh token —
692
+ * AUTH-B8), while `onAuthStateChanged` fires only on a uid change
693
+ * (AUTH-B7). The `applyingTransition` guard suppresses the synchronous
694
+ * `onCurrentUserChanged` subscriber so we don't double-notify.
695
+ *
696
+ * `signInProvider` (when passed) records the provider of THIS
697
+ * sign-in session — it feeds `IdTokenResult.signInProvider` and the
698
+ * synthesized `firebase.sign_in_provider` claim. Omitted by the
699
+ * test driver (`sandbox.setUser`), which leaves any previous value
700
+ * in place.
701
+ */
702
+ setCurrentUser(user, signInProvider) {
703
+ if (user === null) {
704
+ // Sign-out. Drop the cached token so a later re-sign-in for the
705
+ // same uid mints a fresh one ("new session = new token"). Fire
706
+ // listeners only if we were actually signed in (signing out an
707
+ // already-null session is a no-op, matching prod).
708
+ const previousUser = this.cachedUser;
709
+ this.cachedUser = null;
710
+ this.applyingTransition = true;
711
+ try {
712
+ this.sandbox.currentUser = null;
713
+ }
714
+ finally {
715
+ this.applyingTransition = false;
716
+ }
717
+ if (previousUser) {
718
+ this.tokenCache.delete(previousUser.uid);
719
+ this.notifyAuthListeners();
720
+ this.notifySessionChanged();
721
+ // Sign-out: the acting identity WAS the user that just left; no
722
+ // `after` state (signed out). Only emit on a real transition
723
+ // (signing out an already-null session is a prod no-op above).
724
+ this.emitAuthEvent('sign_out', {
725
+ path: previousUser.uid,
726
+ auth: { uid: previousUser.uid },
727
+ });
728
+ }
729
+ return;
730
+ }
731
+ // Record this session's provider BEFORE any token mint below so
732
+ // the freshly-minted token carries the right sign_in_provider.
733
+ if (signInProvider !== undefined) {
734
+ this.signInProviderByUid.set(user.uid, signInProvider);
735
+ }
736
+ // Look up custom claims for this uid (if we know them). Falls
737
+ // back to the empty claims map for anonymous / freshly-minted
738
+ // users.
739
+ const stored = this.usersByUid.get(user.uid);
740
+ const claims = stored?.customClaims ?? {};
741
+ // A `signInProvider` argument marks an actual sign-in (the test
742
+ // driver omits it) — bump the record's lastLoginAt.
743
+ if (signInProvider !== undefined && stored) {
744
+ stored.lastLoginAt = new Date().toISOString();
745
+ this.notifyUsersChanged();
746
+ }
747
+ // Stash the (possibly richer) User snapshot so listeners fire with
748
+ // the full display name / email, not just the AuthState round-trip.
749
+ this.cachedUser = user;
750
+ // Every sign-in mints a fresh token — a new session, even if the uid
751
+ // is unchanged (AUTH-B8: a same-uid re-sign-in still rotates the
752
+ // token, which `onIdTokenChanged` then observes). Matches prod's
753
+ // "new session = new token".
754
+ this.tokenCache.set(user.uid, this.mintToken(user.uid, claims));
755
+ // Push to the sandbox under the guard so the synchronous subscriber
756
+ // doesn't notify — we drive the fan-out below with the correct
757
+ // id-token / auth-state split.
758
+ const nextState = { uid: user.uid, token: claims };
759
+ this.applyingTransition = true;
760
+ try {
761
+ this.sandbox.currentUser = nextState;
762
+ }
763
+ finally {
764
+ this.applyingTransition = false;
765
+ }
766
+ this.notifyAuthListeners();
767
+ // Notify session-change subscribers so the persistence controller
768
+ // can save the new signed-in uid to the appropriate web-storage
769
+ // slot. This fires AFTER notifyAuthListeners so the auth state is
770
+ // fully settled before the controller reads currentUid() + mode().
771
+ this.notifySessionChanged();
772
+ // Sign-in state change. `signInProvider !== undefined` marks a REAL
773
+ // sign-in (anonymous / email-password / popup / redirect / restore) —
774
+ // the test driver (`sandbox.setUser`) omits it and is not a sign-in
775
+ // worth surfacing on the activity stream. The acting identity IS the
776
+ // user that just signed in.
777
+ if (signInProvider !== undefined) {
778
+ this.emitAuthEvent('sign_in', {
779
+ path: user.uid,
780
+ auth: nextState,
781
+ after: { uid: user.uid, isAnonymous: user.isAnonymous, email: user.email },
782
+ detail: { providerId: signInProvider ?? 'anonymous' },
783
+ });
784
+ }
785
+ }
786
+ // ─── Mock-result registry ───────────────────────────────────────────
787
+ setMockResult(providerId, result) {
788
+ this.mockResults.set(providerId, result);
789
+ }
790
+ consumeMockResult(providerId) {
791
+ // One-shot per stage — clear after read so the next call
792
+ // requires a fresh `mockSignInResult`. Matches `firebase/auth`'s
793
+ // "one popup per call" semantics.
794
+ const result = this.mockResults.get(providerId);
795
+ if (result)
796
+ this.mockResults.delete(providerId);
797
+ return result;
798
+ }
799
+ // ─── Popup/redirect resolver + redirect-result slot ─────────────────
800
+ setResolver(resolver) {
801
+ this.resolver = resolver;
802
+ }
803
+ getResolver() {
804
+ return this.resolver;
805
+ }
806
+ /** Stash the credential a `signInWithRedirect` produced; `getRedirectResult`
807
+ * returns-and-clears it. */
808
+ setRedirectResult(result) {
809
+ this.redirectResult = result;
810
+ }
811
+ takeRedirectResult() {
812
+ const r = this.redirectResult;
813
+ this.redirectResult = null;
814
+ return r;
815
+ }
816
+ /**
817
+ * Record a provider-flow sign-in (popup / redirect / credential)
818
+ * against the user DB so the identity shows up in `listIdentities`
819
+ * with its real provider. Upserts: unknown uids get a fresh record
820
+ * (no password — these identities can't sign in via
821
+ * `signInWithEmailAndPassword`); known uids get the provider
822
+ * appended to `providerUserInfo` if it isn't already linked.
823
+ */
824
+ recordProviderSignIn(user, providerId) {
825
+ let changed = false;
826
+ let stored = this.usersByUid.get(user.uid);
827
+ if (!stored) {
828
+ stored = this.makeStored({
829
+ uid: user.uid,
830
+ email: user.email,
831
+ displayName: user.displayName,
832
+ isAnonymous: user.isAnonymous,
833
+ });
834
+ this.usersByUid.set(user.uid, stored);
835
+ if (user.email)
836
+ this.usersByEmail.set(user.email.toLowerCase(), stored);
837
+ changed = true;
838
+ }
839
+ if (!stored.providerUserInfo.some((p) => p.providerId === providerId)) {
840
+ stored.providerUserInfo.push({ providerId });
841
+ changed = true;
842
+ }
843
+ if (changed)
844
+ this.notifyUsersChanged();
845
+ }
846
+ /**
847
+ * Mint a sign-in credential for a host-driven flow (the account
848
+ * picker behind `AuthFlowResolver`). Token + claims synthesis is
849
+ * backend-owned: the returned `User`'s `getIdToken` /
850
+ * `getIdTokenResult` route through this backend's token cache, so
851
+ * helper-driven sign-ins get real (cached, refreshable,
852
+ * provider-labeled) sandbox tokens instead of host-synthesized
853
+ * strings.
854
+ *
855
+ * Two request shapes:
856
+ * - `{providerId, uid}` — credential for an EXISTING identity
857
+ * (the picker's "pick" action). Throws `invalid-argument` for
858
+ * unknown uids.
859
+ * - `{providerId, spec}` — upsert a new identity (the picker's
860
+ * "add account"). If `spec.email` already belongs to a stored
861
+ * user, that identity is reused and the provider linked
862
+ * (Google-style same-email account reuse); otherwise a fresh
863
+ * record is created with uid `spec.uid ?? '<providerId>:<email>'`
864
+ * and NO password (provider identities can't sign in via
865
+ * `signInWithEmailAndPassword`).
866
+ *
867
+ * In both shapes the provider is linked into `providerUserInfo`.
868
+ * The credential does NOT sign the user in — hand it to the
869
+ * resolver's promise; `signInWithPopup`/`signInWithRedirect`
870
+ * complete the sign-in.
871
+ */
872
+ createSignInCredential(req) {
873
+ const { providerId } = req;
874
+ let stored;
875
+ if ('uid' in req) {
876
+ const found = this.usersByUid.get(req.uid);
877
+ if (!found) {
878
+ throw makeAuthError('auth/user-not-found', `createSignInCredential: no identity with uid ${req.uid}. Use the {spec} shape to create one.`);
879
+ }
880
+ stored = found;
881
+ }
882
+ else {
883
+ const { spec } = req;
884
+ const byEmail = this.usersByEmail.get(spec.email.toLowerCase());
885
+ if (byEmail) {
886
+ stored = byEmail;
887
+ }
888
+ else {
889
+ stored = this.makeStored({
890
+ uid: spec.uid ?? `${providerId}:${spec.email}`,
891
+ email: spec.email,
892
+ displayName: spec.displayName ?? null,
893
+ customClaims: spec.customClaims ?? {},
894
+ });
895
+ this.usersByUid.set(stored.uid, stored);
896
+ this.usersByEmail.set(spec.email.toLowerCase(), stored);
897
+ this.notifyUsersChanged();
898
+ }
899
+ }
900
+ if (!stored.providerUserInfo.some((p) => p.providerId === providerId)) {
901
+ stored.providerUserInfo.push({ providerId });
902
+ this.notifyUsersChanged();
903
+ }
904
+ return {
905
+ user: this.buildUserFromStored(stored),
906
+ providerId,
907
+ operationType: 'signIn',
908
+ };
909
+ }
910
+ /** Snapshot of every known identity (seeded + created), for a host
911
+ * account-picker UI. No `firebase/auth` equivalent — sandbox-only.
912
+ *
913
+ * `providerId` is the primary provider label (first linked
914
+ * provider, or `'anonymous'` for anonymous users);
915
+ * `providerUserInfo` is the full emulator-shaped array. */
916
+ listIdentities() {
917
+ return [...this.usersByUid.values()].map((u) => ({
918
+ uid: u.uid,
919
+ email: u.email,
920
+ displayName: u.displayName,
921
+ providerId: u.isAnonymous
922
+ ? 'anonymous'
923
+ : u.providerUserInfo[0]?.providerId ?? 'password',
924
+ providerUserInfo: [...u.providerUserInfo],
925
+ isAnonymous: u.isAnonymous,
926
+ customClaims: u.customClaims,
927
+ }));
928
+ }
929
+ // ─── User-admin surface (emulator-REST-shaped) ──────────────────────
930
+ /** Public snapshot of a stored user. Defensive copies — mutating a
931
+ * returned record never touches the DB. */
932
+ toRecord(u) {
933
+ return {
934
+ uid: u.uid,
935
+ email: u.email,
936
+ displayName: u.displayName,
937
+ phoneNumber: u.phoneNumber,
938
+ photoUrl: u.photoUrl,
939
+ customClaims: { ...u.customClaims },
940
+ providerUserInfo: u.providerUserInfo.map((p) => ({ ...p })),
941
+ isAnonymous: u.isAnonymous,
942
+ disabled: u.disabled,
943
+ emailVerified: u.emailVerified,
944
+ createdAt: u.createdAt,
945
+ lastLoginAt: u.lastLoginAt,
946
+ };
947
+ }
948
+ /** Every user in the DB (seeded, created, signed-in-via-provider,
949
+ * anonymous), as admin records. */
950
+ listUsers() {
951
+ return [...this.usersByUid.values()].map((u) => this.toRecord(u));
952
+ }
953
+ /** Sanitize an admin-supplied linked-provider list (create + update):
954
+ * dedup by providerId, reject empty ids, and refuse the two ids that
955
+ * are NOT linkable entries — `password` is credential-derived (a
956
+ * password on the account links it; it leads the list when present)
957
+ * and `anonymous` surfaces on the token, never on the record. */
958
+ sanitizeLinkedProviders(entries, hasPassword, op) {
959
+ const seen = new Set();
960
+ const next = [];
961
+ for (const entry of entries) {
962
+ const providerId = entry?.providerId?.trim();
963
+ if (!providerId) {
964
+ throw makeAuthError('auth/argument-error', `${op}: providerUserInfo entries need a non-empty providerId.`);
965
+ }
966
+ if (providerId === 'password' || providerId === 'anonymous')
967
+ continue;
968
+ if (seen.has(providerId))
969
+ continue;
970
+ seen.add(providerId);
971
+ next.push({ providerId });
972
+ }
973
+ if (hasPassword)
974
+ next.unshift({ providerId: 'password' });
975
+ return next;
976
+ }
977
+ /** Admin user creation. Does NOT sign the user in (unlike
978
+ * `createUserWithEmailAndPassword`) — matches the emulator's
979
+ * add-user flow / admin SDK semantics. */
980
+ createUser(req) {
981
+ const uid = req.uid ?? `user-${this.nextAdminUserId++}`;
982
+ if (this.usersByUid.has(uid)) {
983
+ throw makeAuthError('auth/uid-already-exists', `createUser: a user with uid ${uid} already exists.`);
984
+ }
985
+ if (req.email !== undefined) {
986
+ validateEmailFormat(req.email);
987
+ if (this.usersByEmail.has(req.email.toLowerCase())) {
988
+ throw makeAuthError('auth/email-already-in-use', `An account already exists for ${req.email}.`);
989
+ }
990
+ }
991
+ if (req.password !== undefined) {
992
+ validatePasswordStrength(req.password);
993
+ if (req.email === undefined) {
994
+ throw makeAuthError('auth/invalid-email', 'createUser: a password requires an email.');
995
+ }
996
+ }
997
+ const record = this.makeStored({
998
+ uid,
999
+ email: req.email ?? null,
1000
+ password: req.password ?? null,
1001
+ displayName: req.displayName ?? null,
1002
+ phoneNumber: req.phoneNumber ?? null,
1003
+ photoUrl: req.photoUrl ?? null,
1004
+ customClaims: req.customClaims ?? {},
1005
+ disabled: req.disabled ?? false,
1006
+ emailVerified: req.emailVerified ?? false,
1007
+ providerUserInfo: this.sanitizeLinkedProviders(req.providerUserInfo ?? [], req.password !== undefined, 'createUser'),
1008
+ });
1009
+ this.usersByUid.set(uid, record);
1010
+ if (record.email)
1011
+ this.usersByEmail.set(record.email.toLowerCase(), record);
1012
+ this.notifyUsersChanged();
1013
+ const created = this.toRecord(record);
1014
+ this.emitAuthEvent('user_create', { path: uid, auth: null, after: created });
1015
+ return created;
1016
+ }
1017
+ /** Admin user update. `undefined` fields untouched; `customClaims`
1018
+ * replaces the whole map (admin `setCustomUserClaims` semantics).
1019
+ * Claims changes reach an ACTIVE session only on the next token
1020
+ * refresh / sign-in — same propagation story as prod. */
1021
+ updateUser(uid, update) {
1022
+ const stored = this.usersByUid.get(uid);
1023
+ if (!stored) {
1024
+ throw makeAuthError('auth/user-not-found', `No user found for uid ${uid}.`);
1025
+ }
1026
+ const before = this.toRecord(stored);
1027
+ if (update.email !== undefined) {
1028
+ validateEmailFormat(update.email);
1029
+ const key = update.email.toLowerCase();
1030
+ const existing = this.usersByEmail.get(key);
1031
+ if (existing && existing !== stored) {
1032
+ throw makeAuthError('auth/email-already-in-use', `An account already exists for ${update.email}.`);
1033
+ }
1034
+ if (stored.email)
1035
+ this.usersByEmail.delete(stored.email.toLowerCase());
1036
+ stored.email = update.email;
1037
+ this.usersByEmail.set(key, stored);
1038
+ }
1039
+ if (update.password !== undefined) {
1040
+ validatePasswordStrength(update.password);
1041
+ stored.password = update.password;
1042
+ // Setting a password links the password provider — matches the
1043
+ // Identity Toolkit (an account gains providerUserInfo entry
1044
+ // when a password is set on it).
1045
+ if (!stored.providerUserInfo.some((p) => p.providerId === 'password')) {
1046
+ stored.providerUserInfo.push({ providerId: 'password' });
1047
+ }
1048
+ }
1049
+ if (update.providerUserInfo !== undefined) {
1050
+ stored.providerUserInfo = this.sanitizeLinkedProviders(update.providerUserInfo, stored.password !== null, 'updateUser');
1051
+ }
1052
+ if (update.displayName !== undefined)
1053
+ stored.displayName = update.displayName;
1054
+ if (update.customClaims !== undefined) {
1055
+ stored.customClaims = { ...update.customClaims };
1056
+ }
1057
+ if (update.disabled !== undefined)
1058
+ stored.disabled = update.disabled;
1059
+ if (update.emailVerified !== undefined)
1060
+ stored.emailVerified = update.emailVerified;
1061
+ this.notifyUsersChanged();
1062
+ const after = this.toRecord(stored);
1063
+ this.emitAuthEvent('user_update', { path: uid, auth: null, before, after });
1064
+ return after;
1065
+ }
1066
+ /** Admin user deletion. The active session (if it's this user) is
1067
+ * NOT terminated — matches prod, where admin deletion doesn't kill
1068
+ * live client sessions until the token is revoked/refreshed. */
1069
+ deleteUser(uid) {
1070
+ const stored = this.usersByUid.get(uid);
1071
+ if (!stored) {
1072
+ throw makeAuthError('auth/user-not-found', `No user found for uid ${uid}.`);
1073
+ }
1074
+ const before = this.toRecord(stored);
1075
+ this.usersByUid.delete(uid);
1076
+ if (stored.email && this.usersByEmail.get(stored.email.toLowerCase()) === stored) {
1077
+ this.usersByEmail.delete(stored.email.toLowerCase());
1078
+ }
1079
+ this.tokenCache.delete(uid);
1080
+ this.signInProviderByUid.delete(uid);
1081
+ this.notifyUsersChanged();
1082
+ this.emitAuthEvent('user_delete', { path: uid, auth: null, before });
1083
+ }
1084
+ /** Drop every user record (the emulator's "delete all accounts").
1085
+ * Active sessions are untouched, like {@link deleteUser}. */
1086
+ clearUsers() {
1087
+ if (this.usersByUid.size === 0 && this.usersByEmail.size === 0)
1088
+ return;
1089
+ const count = this.usersByUid.size;
1090
+ this.usersByUid.clear();
1091
+ this.usersByEmail.clear();
1092
+ this.notifyUsersChanged();
1093
+ this.emitAuthEvent('users_clear', { path: '*', auth: null, detail: { count } });
1094
+ }
1095
+ /** Throw `auth/user-disabled` if the uid resolves to a disabled
1096
+ * record. Called by every provider-flow sign-in path; the
1097
+ * email/password path runs the same check inside
1098
+ * {@link validatePassword}. */
1099
+ assertSignInAllowed(uid) {
1100
+ if (this.usersByUid.get(uid)?.disabled) {
1101
+ throw makeAuthError('auth/user-disabled', 'The user account has been disabled by an administrator.');
1102
+ }
1103
+ }
1104
+ // ─── User factories ─────────────────────────────────────────────────
1105
+ /**
1106
+ * Mint a `User` shell for the given stored record. Used by all
1107
+ * email/password and popup sign-in paths.
1108
+ */
1109
+ buildUserFromStored(stored) {
1110
+ return this.makeUser({
1111
+ uid: stored.uid,
1112
+ email: stored.email,
1113
+ displayName: stored.displayName,
1114
+ isAnonymous: stored.isAnonymous,
1115
+ claims: stored.customClaims,
1116
+ // Carry the profile fields through so a rebuilt/restored user (session
1117
+ // restore, snapshot round-trip) keeps its photoURL / emailVerified /
1118
+ // phoneNumber. Additive: these were null/undefined before this landed.
1119
+ photoURL: stored.photoUrl,
1120
+ emailVerified: stored.emailVerified,
1121
+ phoneNumber: stored.phoneNumber,
1122
+ });
1123
+ }
1124
+ /** Mint a fresh anonymous user with an auto-generated uid. Records
1125
+ * the identity in the user DB (empty `providerUserInfo`, matching
1126
+ * the emulator) so `listIdentities` / future user-admin surfaces
1127
+ * see anonymous accounts too. */
1128
+ mintAnonymousUser() {
1129
+ const uid = `anonymous-${this.nextAnonymousId++}`;
1130
+ const record = this.makeStored({ uid, isAnonymous: true });
1131
+ this.usersByUid.set(uid, record);
1132
+ this.notifyUsersChanged();
1133
+ this.emitAuthEvent('user_create', { path: uid, after: this.toRecord(record), detail: { isAnonymous: true } });
1134
+ return this.makeUser({
1135
+ uid,
1136
+ email: null,
1137
+ displayName: null,
1138
+ isAnonymous: true,
1139
+ claims: {},
1140
+ });
1141
+ }
1142
+ // ─── Detached sessions (per-connection identity substrate) ──────────
1143
+ /**
1144
+ * Mint a session identity WITHOUT making it the global current user —
1145
+ * the substrate for per-connection identity at the serve layer (one
1146
+ * SharedWorker sandbox, N tab sessions; issue #754). Performs a real
1147
+ * sign-in's bookkeeping — provider record, `lastLoginAt` bump, a fresh
1148
+ * token (new session = new token), the `sign_in` activity event — but
1149
+ * leaves `sandbox.currentUser`, the auth-state listeners, and the
1150
+ * session-change subscribers untouched.
1151
+ *
1152
+ * Returns the `User` plus the {@link AuthState} a data context should
1153
+ * carry (`sandbox.withAuth(state)`) so rules evaluate exactly as they
1154
+ * would for a globally signed-in user (`request.auth.uid` + custom
1155
+ * claims on `request.auth.token`).
1156
+ *
1157
+ * GATING: `'anonymous'` / `'password'` / `'createPassword'` run through
1158
+ * {@link assertProviderEnabled} — this is the path the served worker's
1159
+ * per-port sessions actually use (`sandbox.mintSession`, NOT the
1160
+ * `index.ts` free functions), so without this the provider toggle would
1161
+ * have zero effect in `pyric dev`/Studio's served mode, the primary
1162
+ * product surface. `'uid'` (session RESTORE for an existing identity —
1163
+ * a page reload re-establishing a previously-valid session) is
1164
+ * deliberately NOT gated, matching {@link restoreSession}: disabling a
1165
+ * provider after the fact doesn't retroactively invalidate an already-
1166
+ * authenticated session, same as real Firebase.
1167
+ */
1168
+ mintDetachedSession(request) {
1169
+ switch (request.kind) {
1170
+ case 'anonymous':
1171
+ this.assertProviderEnabled('anonymous');
1172
+ return this.establishDetachedSession(this.mintAnonymousUser(), 'anonymous');
1173
+ case 'password':
1174
+ this.assertProviderEnabled('password');
1175
+ return this.establishDetachedSession(this.buildUserFromStored(this.validatePassword(request.email, request.password)), 'password');
1176
+ case 'createPassword':
1177
+ this.assertProviderEnabled('password');
1178
+ return this.establishDetachedSession(this.buildUserFromStored(this.createEmailPasswordUser(request.email, request.password)), 'password');
1179
+ case 'uid': {
1180
+ // restoreSession semantics minus the global set: an EXISTING
1181
+ // identity (per-tab session restore / provider-bridge accept).
1182
+ const stored = this.usersByUid.get(request.uid);
1183
+ if (!stored) {
1184
+ throw makeAuthError('auth/user-not-found', `mintSession: no identity with uid ${request.uid}.`);
1185
+ }
1186
+ if (stored.disabled) {
1187
+ throw makeAuthError('auth/user-disabled', `mintSession: user ${request.uid} is disabled.`);
1188
+ }
1189
+ const providerId = stored.isAnonymous
1190
+ ? 'anonymous'
1191
+ : (stored.providerUserInfo[0]?.providerId ?? 'password');
1192
+ return this.establishDetachedSession(this.buildUserFromStored(stored), providerId);
1193
+ }
1194
+ }
1195
+ }
1196
+ /** The shared sign-in bookkeeping behind {@link mintDetachedSession} —
1197
+ * everything {@link setCurrentUser} does for a real sign-in EXCEPT the
1198
+ * global-session parts. */
1199
+ establishDetachedSession(user, signInProvider) {
1200
+ this.signInProviderByUid.set(user.uid, signInProvider);
1201
+ const stored = this.usersByUid.get(user.uid);
1202
+ const claims = stored?.customClaims ?? {};
1203
+ if (stored) {
1204
+ stored.lastLoginAt = new Date().toISOString();
1205
+ this.notifyUsersChanged();
1206
+ }
1207
+ this.tokenCache.set(user.uid, this.mintToken(user.uid, claims));
1208
+ const state = { uid: user.uid, token: claims };
1209
+ this.emitAuthEvent('sign_in', {
1210
+ path: user.uid,
1211
+ auth: state,
1212
+ after: { uid: user.uid, isAnonymous: user.isAnonymous, email: user.email },
1213
+ // `session: 'connection'` marks a per-connection (detached) session so
1214
+ // observability can tell it from a global sign-in.
1215
+ detail: { providerId: signInProvider, session: 'connection' },
1216
+ });
1217
+ return { user, state };
1218
+ }
1219
+ /** Reconstruct a `User` object from an {@link AuthState}. Returns
1220
+ * `null` for `null` input. Used to mirror sandbox-level state
1221
+ * changes that didn't originate from this backend. */
1222
+ buildUserFromState(state) {
1223
+ if (state === null)
1224
+ return null;
1225
+ // Look up DB-stored attrs (email, displayName) by uid; fall back
1226
+ // to anonymous shape if unknown.
1227
+ const stored = this.usersByUid.get(state.uid);
1228
+ if (stored) {
1229
+ return this.buildUserFromStored({
1230
+ ...stored,
1231
+ customClaims: state.token ?? stored.customClaims,
1232
+ });
1233
+ }
1234
+ return this.makeUser({
1235
+ uid: state.uid,
1236
+ email: null,
1237
+ displayName: null,
1238
+ isAnonymous: state.uid.startsWith('anonymous-'),
1239
+ claims: state.token ?? {},
1240
+ });
1241
+ }
1242
+ // ─── Token cache / refresh ──────────────────────────────────────────
1243
+ /**
1244
+ * Mint a fresh `{ token, result }` for the given uid + claims. Used
1245
+ * both on initial `getIdToken` (cache miss) and on a forced
1246
+ * refresh. Each call advances {@link nextTokenSerial} so two
1247
+ * refreshes inside the same millisecond still produce distinct
1248
+ * token strings.
1249
+ */
1250
+ mintToken(uid, claims) {
1251
+ const issuedAt = new Date();
1252
+ // 100 years out — sandbox tokens never expire.
1253
+ const expires = new Date(issuedAt.getTime() + 100 * 365 * 24 * 60 * 60 * 1000);
1254
+ const serial = this.nextTokenSerial++;
1255
+ const token = sandboxTokenFor(uid, claims, serial);
1256
+ // The provider of the current sign-in session for this uid —
1257
+ // recorded by setCurrentUser at sign-in time. Null for identities
1258
+ // driven via the test driver (no prod analog for that path).
1259
+ const signInProvider = this.signInProviderByUid.get(uid) ?? null;
1260
+ const fullClaims = {
1261
+ sub: uid,
1262
+ aud: 'pyric-sandbox',
1263
+ iss: 'https://sandbox.pyric.dev',
1264
+ auth_time: Math.floor(issuedAt.getTime() / 1000),
1265
+ iat: Math.floor(issuedAt.getTime() / 1000),
1266
+ exp: Math.floor(expires.getTime() / 1000),
1267
+ ...claims,
1268
+ // Synthesized AFTER the custom-claims spread: `firebase` is a
1269
+ // reserved claim namespace in prod (custom claims can't shadow
1270
+ // it), so ours always wins. Mirrors the real JWT's
1271
+ // `firebase.sign_in_provider`.
1272
+ firebase: { sign_in_provider: signInProvider },
1273
+ };
1274
+ const result = {
1275
+ token,
1276
+ claims: fullClaims,
1277
+ expirationTime: expires.toISOString(),
1278
+ issuedAtTime: issuedAt.toISOString(),
1279
+ authTime: issuedAt.toISOString(),
1280
+ signInProvider,
1281
+ };
1282
+ return { token, result };
1283
+ }
1284
+ /**
1285
+ * Get-or-mint the ID token entry for a uid. `forceRefresh: true`
1286
+ * always mints a fresh token, overwrites the cache, and fires the
1287
+ * `onIdTokenChanged` listeners (NOT `onAuthStateChanged` — the
1288
+ * user identity didn't change). Matches prod oracle observations:
1289
+ *
1290
+ * - `auth-getidtoken-force-refresh.json` — forceRefresh returns a
1291
+ * different string than the previous read; a subsequent
1292
+ * getIdToken(false) returns the new (cached) token.
1293
+ * - `auth-onidtokenchanged-force-refresh.json` — onIdTokenChanged
1294
+ * fires after a forced refresh.
1295
+ */
1296
+ getIdTokenFor(uid, claims, forceRefresh) {
1297
+ if (forceRefresh) {
1298
+ const fresh = this.mintToken(uid, claims);
1299
+ this.tokenCache.set(uid, fresh);
1300
+ // Fan out to onIdTokenChanged listeners only — identity is
1301
+ // unchanged, so onAuthStateChanged stays silent.
1302
+ this.fanOut('id-token');
1303
+ return fresh.token;
1304
+ }
1305
+ const cached = this.tokenCache.get(uid);
1306
+ if (cached)
1307
+ return cached.token;
1308
+ const fresh = this.mintToken(uid, claims);
1309
+ this.tokenCache.set(uid, fresh);
1310
+ return fresh.token;
1311
+ }
1312
+ /** {@link getIdTokenFor} variant returning the full IdTokenResult.
1313
+ * Same cache + fan-out semantics. */
1314
+ getIdTokenResultFor(uid, claims, forceRefresh) {
1315
+ if (forceRefresh) {
1316
+ const fresh = this.mintToken(uid, claims);
1317
+ this.tokenCache.set(uid, fresh);
1318
+ this.fanOut('id-token');
1319
+ return fresh.result;
1320
+ }
1321
+ const cached = this.tokenCache.get(uid);
1322
+ if (cached)
1323
+ return cached.result;
1324
+ const fresh = this.mintToken(uid, claims);
1325
+ this.tokenCache.set(uid, fresh);
1326
+ return fresh.result;
1327
+ }
1328
+ /**
1329
+ * Build a `User` shell whose `getIdToken` / `getIdTokenResult`
1330
+ * route through the backend's token cache. The User object closes
1331
+ * over its uid + claims; the cache is owned by the backend so all
1332
+ * User instances for the same uid agree on the current token.
1333
+ */
1334
+ makeUser(args) {
1335
+ const photoURL = args.photoURL ?? null;
1336
+ const phoneNumber = args.phoneNumber ?? null;
1337
+ // Sandbox has no email-verification flow; default to false unless the
1338
+ // caller explicitly supplied it (AUTH-GAP).
1339
+ const emailVerified = args.emailVerified ?? false;
1340
+ const providerId = 'firebase';
1341
+ // Synthesize a single provider entry from the user's own fields for
1342
+ // non-anonymous users (the email/password or popup provider);
1343
+ // anonymous users have no linked provider (AUTH-GAP).
1344
+ const providerData = args.isAnonymous
1345
+ ? []
1346
+ : [{
1347
+ uid: args.uid,
1348
+ displayName: args.displayName,
1349
+ email: args.email,
1350
+ phoneNumber,
1351
+ photoURL,
1352
+ providerId: 'password',
1353
+ }];
1354
+ const user = {
1355
+ uid: args.uid,
1356
+ email: args.email,
1357
+ emailVerified,
1358
+ displayName: args.displayName,
1359
+ photoURL,
1360
+ phoneNumber,
1361
+ isAnonymous: args.isAnonymous,
1362
+ providerId,
1363
+ providerData,
1364
+ // Read LIVE claims by uid on each token call (not the claims frozen
1365
+ // at mint time) so a `seedUsers` re-seed or an `updateUser` claims
1366
+ // change for this uid is reflected in a held user's
1367
+ // `getIdToken(true)` (AUTH-B10) — same propagation story as prod.
1368
+ // Falls back to the closed-over claims for users not in the DB
1369
+ // (anonymous / popup), matching how they were minted.
1370
+ getIdToken: async (forceRefresh) => this.getIdTokenFor(args.uid, this.liveClaims(args.uid, args.claims), forceRefresh === true),
1371
+ getIdTokenResult: async (forceRefresh) => this.getIdTokenResultFor(args.uid, this.liveClaims(args.uid, args.claims), forceRefresh === true),
1372
+ };
1373
+ // Stamp the backend-dispatch hook non-enumerably so the top-level
1374
+ // `updateProfile(user, …)` free function can update THIS user (and the
1375
+ // stored record) without an `auth` handle in scope, matching
1376
+ // `firebase/auth`'s `updateProfile(user, profile)` signature. Hidden
1377
+ // from enumeration/serialization so it never leaks into snapshots.
1378
+ Object.defineProperty(user, USER_INTERNAL, {
1379
+ value: {
1380
+ updateProfile: (p) => {
1381
+ this.updateProfileFor(user, p);
1382
+ return Promise.resolve();
1383
+ },
1384
+ },
1385
+ enumerable: false,
1386
+ });
1387
+ return user;
1388
+ }
1389
+ /**
1390
+ * Update a signed-in user's profile (`displayName` / `photoURL`) — the
1391
+ * backend behind the top-level `updateProfile(user, profile)`.
1392
+ *
1393
+ * Updates the stored record, then mutates the passed `user` AND
1394
+ * `this.cachedUser` IN PLACE when their uid matches (the `User` fields are
1395
+ * TS-`readonly` but runtime-mutable, so held references — including
1396
+ * `auth.currentUser` — reflect the change without a re-read). Only a field
1397
+ * whose `profile.<field> !== undefined` is applied, so `null` clears and
1398
+ * `undefined` leaves the field untouched.
1399
+ *
1400
+ * Fires the coarse user-DB change (`notifyUsersChanged`) + a `user_update`
1401
+ * activity event, but does NOT fan out auth-state / id-token listeners:
1402
+ * real `firebase/auth.updateProfile` does not fire `onAuthStateChanged` /
1403
+ * `onIdTokenChanged`.
1404
+ */
1405
+ updateProfileFor(user, profile) {
1406
+ const stored = this.usersByUid.get(user.uid);
1407
+ const before = stored ? this.toRecord(stored) : undefined;
1408
+ if (stored) {
1409
+ if (profile.displayName !== undefined)
1410
+ stored.displayName = profile.displayName;
1411
+ if (profile.photoURL !== undefined)
1412
+ stored.photoUrl = profile.photoURL;
1413
+ }
1414
+ this.applyProfileToUser(user, profile);
1415
+ if (this.cachedUser && this.cachedUser.uid === user.uid && this.cachedUser !== user) {
1416
+ this.applyProfileToUser(this.cachedUser, profile);
1417
+ }
1418
+ this.notifyUsersChanged();
1419
+ this.emitAuthEvent('user_update', {
1420
+ path: user.uid,
1421
+ auth: { uid: user.uid },
1422
+ before,
1423
+ after: stored ? this.toRecord(stored) : undefined,
1424
+ detail: {
1425
+ displayName: profile.displayName,
1426
+ photoURL: profile.photoURL,
1427
+ },
1428
+ });
1429
+ }
1430
+ /** Mutate a `User`'s `displayName` / `photoURL` (and the first
1431
+ * `providerData` entry's, when present) in place. `readonly` at the type
1432
+ * level, mutable at runtime — cast through a Mutable helper. Only applies a
1433
+ * field when it was explicitly provided (`!== undefined`). */
1434
+ applyProfileToUser(user, profile) {
1435
+ const mutable = user;
1436
+ if (profile.displayName !== undefined)
1437
+ mutable.displayName = profile.displayName;
1438
+ if (profile.photoURL !== undefined)
1439
+ mutable.photoURL = profile.photoURL;
1440
+ const provider0 = user.providerData?.[0];
1441
+ if (provider0) {
1442
+ if (profile.displayName !== undefined)
1443
+ provider0.displayName = profile.displayName;
1444
+ if (profile.photoURL !== undefined)
1445
+ provider0.photoURL = profile.photoURL;
1446
+ }
1447
+ }
1448
+ /**
1449
+ * Worker-path `updateProfile`: update the stored record for `uid`
1450
+ * (+ mutate `this.cachedUser` in place if it matches), then return the
1451
+ * refreshed {@link AuthUserRecord}. The worker holds the real per-port
1452
+ * session `User` and mutates it separately (host-auth.ts). Throws
1453
+ * `auth/user-not-found` for an unknown uid.
1454
+ */
1455
+ updateProfileByUid(uid, profile) {
1456
+ const stored = this.usersByUid.get(uid);
1457
+ if (!stored) {
1458
+ throw makeAuthError('auth/user-not-found', `updateProfile: no identity with uid ${uid}.`);
1459
+ }
1460
+ const before = this.toRecord(stored);
1461
+ if (profile.displayName !== undefined)
1462
+ stored.displayName = profile.displayName;
1463
+ if (profile.photoURL !== undefined)
1464
+ stored.photoUrl = profile.photoURL;
1465
+ if (this.cachedUser && this.cachedUser.uid === uid) {
1466
+ this.applyProfileToUser(this.cachedUser, profile);
1467
+ }
1468
+ this.notifyUsersChanged();
1469
+ const after = this.toRecord(stored);
1470
+ this.emitAuthEvent('user_update', {
1471
+ path: uid,
1472
+ auth: { uid },
1473
+ before,
1474
+ after,
1475
+ detail: { displayName: profile.displayName, photoURL: profile.photoURL },
1476
+ });
1477
+ return after;
1478
+ }
1479
+ /** Live customClaims for a uid — the current value in the user DB,
1480
+ * falling back to `fallback` for uids the DB doesn't track (anonymous
1481
+ * / popup users). Used so a re-seed / `updateUser` is reflected in
1482
+ * held users' forced-refresh tokens (AUTH-B10). */
1483
+ liveClaims(uid, fallback) {
1484
+ return this.usersByUid.get(uid)?.customClaims ?? fallback;
1485
+ }
1486
+ /** Internal validator for email/password — checks the password
1487
+ * matches the stored record. Throws `auth/invalid-email` on a
1488
+ * malformed email, `auth/wrong-password` on mismatch,
1489
+ * `auth/user-not-found` if the email isn't seeded.
1490
+ *
1491
+ * Format validation fires BEFORE the user-DB lookup so callers
1492
+ * shipping malformed inputs see the same `auth/invalid-email`
1493
+ * prod returns — they wouldn't reach the missing-user path in
1494
+ * production either. Password-strength is NOT enforced on sign-in
1495
+ * (a previously-weak seeded password should still let the user
1496
+ * in); only `createUserWithEmailAndPassword` runs the strength
1497
+ * check, mirroring prod. */
1498
+ validatePassword(email, password) {
1499
+ validateEmailFormat(email);
1500
+ // Empty password → `auth/missing-password`, fired BEFORE the user-DB
1501
+ // lookup (same anti-enumeration ordering as the email check). Prod's
1502
+ // signInWithPassword returns the MISSING_PASSWORD server error, mapped
1503
+ // to `auth/missing-password` with message "A non-empty password must
1504
+ // be provided" (`core/errors.ts:92,282,563`) — NOT wrong-password /
1505
+ // user-not-found (AUTH-B11).
1506
+ if (typeof password !== 'string' || password.length === 0) {
1507
+ throw makeAuthError('auth/missing-password', 'A non-empty password must be provided');
1508
+ }
1509
+ const stored = this.findByEmail(email);
1510
+ if (!stored) {
1511
+ throw makeAuthError('auth/user-not-found', `No user found for ${email}.`);
1512
+ }
1513
+ // Disabled check BEFORE the password compare so a disabled
1514
+ // account can't be password-probed. Best-known semantics — the
1515
+ // exact prod ordering (disabled-vs-wrong-password) is flagged for
1516
+ // an oracle capture; the code (`auth/user-disabled`) and message
1517
+ // match prod's documented shape.
1518
+ if (stored.disabled) {
1519
+ throw makeAuthError('auth/user-disabled', 'The user account has been disabled by an administrator.');
1520
+ }
1521
+ if (stored.password !== password) {
1522
+ throw makeAuthError('auth/wrong-password', 'Invalid password.');
1523
+ }
1524
+ return stored;
1525
+ }
1526
+ }
1527
+ // ─── Helpers ──────────────────────────────────────────────────────────
1528
+ /**
1529
+ * Opaque token string. Hash is a tiny deterministic digest over the
1530
+ * serialized claims map + a monotonic serial — enough that two
1531
+ * different claim maps for the same uid get different tokens AND
1532
+ * back-to-back refreshes for the same uid + claims also get
1533
+ * different tokens. NOT a cryptographic primitive. The
1534
+ * `sandbox-id-token-` prefix is grepable in logs.
1535
+ */
1536
+ function sandboxTokenFor(uid, claims, serial) {
1537
+ let hash = 5381;
1538
+ const json = JSON.stringify(claims) + ':' + String(serial);
1539
+ for (let i = 0; i < json.length; i++) {
1540
+ hash = ((hash << 5) + hash + json.charCodeAt(i)) | 0;
1541
+ }
1542
+ const hex = (hash >>> 0).toString(16).padStart(8, '0');
1543
+ return `sandbox-id-token-${uid}-${hex}`;
1544
+ }
1545
+ /**
1546
+ * Empirical match for prod's email-format rejection (matrix row #18).
1547
+ * Prod uses a permissive regex — local-part + `@` + domain-with-dot
1548
+ * is the practical bar. We mirror that shape: reject empty, reject
1549
+ * missing `@`, reject empty local-part or empty domain-part. Anything
1550
+ * else passes; consumer code that ships a more exotic-but-valid
1551
+ * address (quoted local-parts, IDN domains, etc.) should still
1552
+ * round-trip the same as prod.
1553
+ *
1554
+ * Throws `auth/invalid-email` with a message matching prod's shape so
1555
+ * consumer code that switches on `.code` sees the same error in
1556
+ * sandbox + prod. Oracle observation:
1557
+ * `scripts/oracle/observations/auth-row-18-invalid-email-error-code.json`.
1558
+ */
1559
+ function validateEmailFormat(email) {
1560
+ if (typeof email !== 'string' || email.length === 0) {
1561
+ throw makeAuthError('auth/invalid-email', 'Error');
1562
+ }
1563
+ const atIdx = email.indexOf('@');
1564
+ // No `@`, or `@` at start (empty local-part), or `@` at end (empty
1565
+ // domain). Prod also rejects domains without a dot, but we stay
1566
+ // permissive there — the empirical oracle observation only locks
1567
+ // the `not-an-email` rejection.
1568
+ if (atIdx <= 0 || atIdx === email.length - 1) {
1569
+ throw makeAuthError('auth/invalid-email', 'Error');
1570
+ }
1571
+ }
1572
+ /**
1573
+ * Empirical match for prod's password-strength rejection (matrix
1574
+ * row #19). Prod's observed message is "Password should be at least
1575
+ * 6 characters" with code `auth/weak-password`. Oracle observation:
1576
+ * `scripts/oracle/observations/auth-row-19-weak-password-error-code.json`.
1577
+ */
1578
+ function validatePasswordStrength(password) {
1579
+ if (typeof password !== 'string' || password.length < 6) {
1580
+ throw makeAuthError('auth/weak-password', 'Password should be at least 6 characters');
1581
+ }
1582
+ }
1583
+ /**
1584
+ * Build a real `FirebaseError` (the same class `firebase/auth` throws),
1585
+ * with the prod message wrapper `Firebase: <message> (<code>).`
1586
+ * (`clones/.../util/src/errors.ts:121` — `${serviceName}: ${message}
1587
+ * (${fullCode}).`, serviceName `Firebase`). So consumer code that does
1588
+ * `err instanceof FirebaseError` or matches on the wrapped message sees
1589
+ * the same shape sandbox vs prod (AUTH-GAP). `.code` is preserved.
1590
+ *
1591
+ * Oracle-pinned shapes this reproduces:
1592
+ * - `Firebase: Error (auth/invalid-email).`
1593
+ * - `Firebase: Password should be at least 6 characters (auth/weak-password).`
1594
+ */
1595
+ export function makeAuthError(code, message) {
1596
+ return new FirebaseError(code, `Firebase: ${message} (${code}).`);
1597
+ }
1598
+ //# sourceMappingURL=sandbox-backend.js.map