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.
- package/README.md +138 -2
- package/README.md.orig +54 -0
- package/dist/app/index.d.ts +81 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +78 -0
- package/dist/app/index.js.map +1 -0
- package/dist/auth/index.d.ts +346 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +701 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/prod-backend.d.ts +39 -0
- package/dist/auth/prod-backend.d.ts.map +1 -0
- package/dist/auth/prod-backend.js +179 -0
- package/dist/auth/prod-backend.js.map +1 -0
- package/dist/auth/providers.d.ts +107 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +138 -0
- package/dist/auth/providers.js.map +1 -0
- package/dist/auth/sandbox-backend.d.ts +769 -0
- package/dist/auth/sandbox-backend.d.ts.map +1 -0
- package/dist/auth/sandbox-backend.js +1598 -0
- package/dist/auth/sandbox-backend.js.map +1 -0
- package/dist/auth/target.d.ts +40 -0
- package/dist/auth/target.d.ts.map +1 -0
- package/dist/auth/target.js +28 -0
- package/dist/auth/target.js.map +1 -0
- package/dist/auth/types.d.ts +252 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +26 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/database/constraints/atoms.d.ts +29 -0
- package/dist/database/constraints/atoms.d.ts.map +1 -0
- package/dist/database/constraints/atoms.js +43 -0
- package/dist/database/constraints/atoms.js.map +1 -0
- package/dist/database/constraints/compose.d.ts +16 -0
- package/dist/database/constraints/compose.d.ts.map +1 -0
- package/dist/database/constraints/compose.js +15 -0
- package/dist/database/constraints/compose.js.map +1 -0
- package/dist/database/constraints/data.d.ts +30 -0
- package/dist/database/constraints/data.d.ts.map +1 -0
- package/dist/database/constraints/data.js +46 -0
- package/dist/database/constraints/data.js.map +1 -0
- package/dist/database/constraints/document.d.ts +42 -0
- package/dist/database/constraints/document.d.ts.map +1 -0
- package/dist/database/constraints/document.js +86 -0
- package/dist/database/constraints/document.js.map +1 -0
- package/dist/database/constraints/game.d.ts +30 -0
- package/dist/database/constraints/game.d.ts.map +1 -0
- package/dist/database/constraints/game.js +49 -0
- package/dist/database/constraints/game.js.map +1 -0
- package/dist/database/constraints/index.d.ts +12 -0
- package/dist/database/constraints/index.d.ts.map +1 -0
- package/dist/database/constraints/index.js +9 -0
- package/dist/database/constraints/index.js.map +1 -0
- package/dist/database/constraints/policies.d.ts +16 -0
- package/dist/database/constraints/policies.d.ts.map +1 -0
- package/dist/database/constraints/policies.js +17 -0
- package/dist/database/constraints/policies.js.map +1 -0
- package/dist/database/constraints/ruleset.d.ts +5 -0
- package/dist/database/constraints/ruleset.d.ts.map +1 -0
- package/dist/database/constraints/ruleset.js +132 -0
- package/dist/database/constraints/ruleset.js.map +1 -0
- package/dist/database/constraints/schema.d.ts +17 -0
- package/dist/database/constraints/schema.d.ts.map +1 -0
- package/dist/database/constraints/schema.js +74 -0
- package/dist/database/constraints/schema.js.map +1 -0
- package/dist/database/constraints/types.d.ts +22 -0
- package/dist/database/constraints/types.d.ts.map +1 -0
- package/dist/database/constraints/types.js +2 -0
- package/dist/database/constraints/types.js.map +1 -0
- package/dist/database/crawl/handler.d.ts +7 -0
- package/dist/database/crawl/handler.d.ts.map +1 -0
- package/dist/database/crawl/handler.js +91 -0
- package/dist/database/crawl/handler.js.map +1 -0
- package/dist/database/crawl/semaphore.d.ts +9 -0
- package/dist/database/crawl/semaphore.d.ts.map +1 -0
- package/dist/database/crawl/semaphore.js +27 -0
- package/dist/database/crawl/semaphore.js.map +1 -0
- package/dist/database/crawl/spec.d.ts +33 -0
- package/dist/database/crawl/spec.d.ts.map +1 -0
- package/dist/database/crawl/spec.js +12 -0
- package/dist/database/crawl/spec.js.map +1 -0
- package/dist/database/data/handler.d.ts +9 -0
- package/dist/database/data/handler.d.ts.map +1 -0
- package/dist/database/data/handler.js +96 -0
- package/dist/database/data/handler.js.map +1 -0
- package/dist/database/data/spec.d.ts +77 -0
- package/dist/database/data/spec.d.ts.map +1 -0
- package/dist/database/data/spec.js +17 -0
- package/dist/database/data/spec.js.map +1 -0
- package/dist/database/data/validated.d.ts +8 -0
- package/dist/database/data/validated.d.ts.map +1 -0
- package/dist/database/data/validated.js +99 -0
- package/dist/database/data/validated.js.map +1 -0
- package/dist/database/grammar/RtdbExpr.ohm +98 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.d.ts +2 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.js +103 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.js.map +1 -0
- package/dist/database/grammar/RtdbExprParser.d.ts +5 -0
- package/dist/database/grammar/RtdbExprParser.d.ts.map +1 -0
- package/dist/database/grammar/RtdbExprParser.js +53 -0
- package/dist/database/grammar/RtdbExprParser.js.map +1 -0
- package/dist/database/grammar/linter.d.ts +3 -0
- package/dist/database/grammar/linter.d.ts.map +1 -0
- package/dist/database/grammar/linter.js +66 -0
- package/dist/database/grammar/linter.js.map +1 -0
- package/dist/database/grammar/simulator.d.ts +31 -0
- package/dist/database/grammar/simulator.d.ts.map +1 -0
- package/dist/database/grammar/simulator.js +255 -0
- package/dist/database/grammar/simulator.js.map +1 -0
- package/dist/database/grammar/validator.d.ts +3 -0
- package/dist/database/grammar/validator.d.ts.map +1 -0
- package/dist/database/grammar/validator.js +84 -0
- package/dist/database/grammar/validator.js.map +1 -0
- package/dist/database/host.d.ts +50 -0
- package/dist/database/host.d.ts.map +1 -0
- package/dist/database/host.js +41 -0
- package/dist/database/host.js.map +1 -0
- package/dist/database/index.d.ts +40 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +47 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/initialize-from-app.d.ts +28 -0
- package/dist/database/initialize-from-app.d.ts.map +1 -0
- package/dist/database/initialize-from-app.js +23 -0
- package/dist/database/initialize-from-app.js.map +1 -0
- package/dist/database/ir/handler.d.ts +6 -0
- package/dist/database/ir/handler.d.ts.map +1 -0
- package/dist/database/ir/handler.js +52 -0
- package/dist/database/ir/handler.js.map +1 -0
- package/dist/database/ir/spec.d.ts +25 -0
- package/dist/database/ir/spec.d.ts.map +1 -0
- package/dist/database/ir/spec.js +11 -0
- package/dist/database/ir/spec.js.map +1 -0
- package/dist/database/mapper.d.ts +9 -0
- package/dist/database/mapper.d.ts.map +1 -0
- package/dist/database/mapper.js +115 -0
- package/dist/database/mapper.js.map +1 -0
- package/dist/database/modular.d.ts +604 -0
- package/dist/database/modular.d.ts.map +1 -0
- package/dist/database/modular.js +1226 -0
- package/dist/database/modular.js.map +1 -0
- package/dist/database/replay.d.ts +33 -0
- package/dist/database/replay.d.ts.map +1 -0
- package/dist/database/replay.js +124 -0
- package/dist/database/replay.js.map +1 -0
- package/dist/database/resolver.d.ts +4 -0
- package/dist/database/resolver.d.ts.map +1 -0
- package/dist/database/resolver.js +66 -0
- package/dist/database/resolver.js.map +1 -0
- package/dist/database/sandbox/backend.d.ts +403 -0
- package/dist/database/sandbox/backend.d.ts.map +1 -0
- package/dist/database/sandbox/backend.js +1467 -0
- package/dist/database/sandbox/backend.js.map +1 -0
- package/dist/database/sandbox/data-tree.d.ts +123 -0
- package/dist/database/sandbox/data-tree.d.ts.map +1 -0
- package/dist/database/sandbox/data-tree.js +310 -0
- package/dist/database/sandbox/data-tree.js.map +1 -0
- package/dist/database/sandbox/normalize.d.ts +77 -0
- package/dist/database/sandbox/normalize.d.ts.map +1 -0
- package/dist/database/sandbox/normalize.js +165 -0
- package/dist/database/sandbox/normalize.js.map +1 -0
- package/dist/database/sandbox/push-id.d.ts +32 -0
- package/dist/database/sandbox/push-id.d.ts.map +1 -0
- package/dist/database/sandbox/push-id.js +82 -0
- package/dist/database/sandbox/push-id.js.map +1 -0
- package/dist/database/sandbox/query.d.ts +175 -0
- package/dist/database/sandbox/query.d.ts.map +1 -0
- package/dist/database/sandbox/query.js +269 -0
- package/dist/database/sandbox/query.js.map +1 -0
- package/dist/database/sandbox/rules-eval.d.ts +69 -0
- package/dist/database/sandbox/rules-eval.d.ts.map +1 -0
- package/dist/database/sandbox/rules-eval.js +124 -0
- package/dist/database/sandbox/rules-eval.js.map +1 -0
- package/dist/database/sandbox/sentinels.d.ts +71 -0
- package/dist/database/sandbox/sentinels.d.ts.map +1 -0
- package/dist/database/sandbox/sentinels.js +92 -0
- package/dist/database/sandbox/sentinels.js.map +1 -0
- package/dist/database/simulation/handler.d.ts +6 -0
- package/dist/database/simulation/handler.d.ts.map +1 -0
- package/dist/database/simulation/handler.js +236 -0
- package/dist/database/simulation/handler.js.map +1 -0
- package/dist/database/simulation/spec.d.ts +69 -0
- package/dist/database/simulation/spec.d.ts.map +1 -0
- package/dist/database/simulation/spec.js +25 -0
- package/dist/database/simulation/spec.js.map +1 -0
- package/dist/database/tools.d.ts +22 -0
- package/dist/database/tools.d.ts.map +1 -0
- package/dist/database/tools.js +272 -0
- package/dist/database/tools.js.map +1 -0
- package/dist/database/types.d.ts +210 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +26 -0
- package/dist/database/types.js.map +1 -0
- package/dist/database/write/handler.d.ts +7 -0
- package/dist/database/write/handler.d.ts.map +1 -0
- package/dist/database/write/handler.js +42 -0
- package/dist/database/write/handler.js.map +1 -0
- package/dist/database/write/spec.d.ts +18 -0
- package/dist/database/write/spec.d.ts.map +1 -0
- package/dist/database/write/spec.js +7 -0
- package/dist/database/write/spec.js.map +1 -0
- package/dist/firestore/index.d.ts +533 -0
- package/dist/firestore/index.d.ts.map +1 -0
- package/dist/firestore/index.js +1536 -0
- package/dist/firestore/index.js.map +1 -0
- package/dist/firestore/tools.d.ts +67 -0
- package/dist/firestore/tools.d.ts.map +1 -0
- package/dist/firestore/tools.js +318 -0
- package/dist/firestore/tools.js.map +1 -0
- package/dist/firestore-values/index.d.ts +63 -0
- package/dist/firestore-values/index.d.ts.map +1 -0
- package/dist/firestore-values/index.js +148 -0
- package/dist/firestore-values/index.js.map +1 -0
- package/dist/project-scope.d.ts +5 -0
- package/dist/project-scope.d.ts.map +1 -0
- package/dist/project-scope.js +2 -0
- package/dist/project-scope.js.map +1 -0
- package/dist/rules/extract.d.ts +28 -0
- package/dist/rules/extract.d.ts.map +1 -0
- package/dist/rules/extract.js +25 -0
- package/dist/rules/extract.js.map +1 -0
- package/dist/rules/generators/assembler.d.ts +19 -0
- package/dist/rules/generators/assembler.d.ts.map +1 -0
- package/dist/rules/generators/assembler.js +292 -0
- package/dist/rules/generators/assembler.js.map +1 -0
- package/dist/rules/generators/expressions.d.ts +52 -0
- package/dist/rules/generators/expressions.d.ts.map +1 -0
- package/dist/rules/generators/expressions.js +117 -0
- package/dist/rules/generators/expressions.js.map +1 -0
- package/dist/rules/generators/grid.d.ts +22 -0
- package/dist/rules/generators/grid.d.ts.map +1 -0
- package/dist/rules/generators/grid.js +52 -0
- package/dist/rules/generators/grid.js.map +1 -0
- package/dist/rules/grammar/FirestoreAST.d.ts +129 -0
- package/dist/rules/grammar/FirestoreAST.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreAST.js +3 -0
- package/dist/rules/grammar/FirestoreAST.js.map +1 -0
- package/dist/rules/grammar/FirestoreAssembler.d.ts +5 -0
- package/dist/rules/grammar/FirestoreAssembler.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreAssembler.js +158 -0
- package/dist/rules/grammar/FirestoreAssembler.js.map +1 -0
- package/dist/rules/grammar/FirestoreParser.d.ts +46 -0
- package/dist/rules/grammar/FirestoreParser.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreParser.js +409 -0
- package/dist/rules/grammar/FirestoreParser.js.map +1 -0
- package/dist/rules/grammar/FirestoreRules.ohm +229 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.d.ts +2 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.js +234 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.js.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts +10 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.js +482 -0
- package/dist/rules/grammar/FirestoreValidator.js.map +1 -0
- package/dist/rules/index.d.ts +67 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +80 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/indexes/extract/annotation-collect.d.ts +42 -0
- package/dist/rules/indexes/extract/annotation-collect.d.ts.map +1 -0
- package/dist/rules/indexes/extract/annotation-collect.js +69 -0
- package/dist/rules/indexes/extract/annotation-collect.js.map +1 -0
- package/dist/rules/indexes/extract/annotations.d.ts +50 -0
- package/dist/rules/indexes/extract/annotations.d.ts.map +1 -0
- package/dist/rules/indexes/extract/annotations.js +144 -0
- package/dist/rules/indexes/extract/annotations.js.map +1 -0
- package/dist/rules/indexes/extract/ast.d.ts +54 -0
- package/dist/rules/indexes/extract/ast.d.ts.map +1 -0
- package/dist/rules/indexes/extract/ast.js +119 -0
- package/dist/rules/indexes/extract/ast.js.map +1 -0
- package/dist/rules/indexes/extract/classify.d.ts +41 -0
- package/dist/rules/indexes/extract/classify.d.ts.map +1 -0
- package/dist/rules/indexes/extract/classify.js +138 -0
- package/dist/rules/indexes/extract/classify.js.map +1 -0
- package/dist/rules/indexes/extract/composite.d.ts +38 -0
- package/dist/rules/indexes/extract/composite.d.ts.map +1 -0
- package/dist/rules/indexes/extract/composite.js +85 -0
- package/dist/rules/indexes/extract/composite.js.map +1 -0
- package/dist/rules/indexes/extract/dataflow.d.ts +45 -0
- package/dist/rules/indexes/extract/dataflow.d.ts.map +1 -0
- package/dist/rules/indexes/extract/dataflow.js +200 -0
- package/dist/rules/indexes/extract/dataflow.js.map +1 -0
- package/dist/rules/indexes/extract/enumerate.d.ts +41 -0
- package/dist/rules/indexes/extract/enumerate.d.ts.map +1 -0
- package/dist/rules/indexes/extract/enumerate.js +163 -0
- package/dist/rules/indexes/extract/enumerate.js.map +1 -0
- package/dist/rules/indexes/extract/extractor.d.ts +3 -0
- package/dist/rules/indexes/extract/extractor.d.ts.map +1 -0
- package/dist/rules/indexes/extract/extractor.js +274 -0
- package/dist/rules/indexes/extract/extractor.js.map +1 -0
- package/dist/rules/indexes/extract/types.d.ts +211 -0
- package/dist/rules/indexes/extract/types.d.ts.map +1 -0
- package/dist/rules/indexes/extract/types.js +14 -0
- package/dist/rules/indexes/extract/types.js.map +1 -0
- package/dist/rules/indexes/extractHandler.d.ts +16 -0
- package/dist/rules/indexes/extractHandler.d.ts.map +1 -0
- package/dist/rules/indexes/extractHandler.js +100 -0
- package/dist/rules/indexes/extractHandler.js.map +1 -0
- package/dist/rules/indexes/extractTool.d.ts +15 -0
- package/dist/rules/indexes/extractTool.d.ts.map +1 -0
- package/dist/rules/indexes/extractTool.js +55 -0
- package/dist/rules/indexes/extractTool.js.map +1 -0
- package/dist/rules/indexes/types.d.ts +71 -0
- package/dist/rules/indexes/types.d.ts.map +1 -0
- package/dist/rules/indexes/types.js +15 -0
- package/dist/rules/indexes/types.js.map +1 -0
- package/dist/rules/inspect/handler.d.ts +6 -0
- package/dist/rules/inspect/handler.d.ts.map +1 -0
- package/dist/rules/inspect/handler.js +144 -0
- package/dist/rules/inspect/handler.js.map +1 -0
- package/dist/rules/inspect/spec.d.ts +37 -0
- package/dist/rules/inspect/spec.d.ts.map +1 -0
- package/dist/rules/inspect/spec.js +2 -0
- package/dist/rules/inspect/spec.js.map +1 -0
- package/dist/rules/inspect/tools.d.ts +21 -0
- package/dist/rules/inspect/tools.d.ts.map +1 -0
- package/dist/rules/inspect/tools.js +21 -0
- package/dist/rules/inspect/tools.js.map +1 -0
- package/dist/rules/linter/ast-utils.d.ts +97 -0
- package/dist/rules/linter/ast-utils.d.ts.map +1 -0
- package/dist/rules/linter/ast-utils.js +491 -0
- package/dist/rules/linter/ast-utils.js.map +1 -0
- package/dist/rules/linter/hallucinations.d.ts +34 -0
- package/dist/rules/linter/hallucinations.d.ts.map +1 -0
- package/dist/rules/linter/hallucinations.js +334 -0
- package/dist/rules/linter/hallucinations.js.map +1 -0
- package/dist/rules/linter/linter.d.ts +84 -0
- package/dist/rules/linter/linter.d.ts.map +1 -0
- package/dist/rules/linter/linter.js +784 -0
- package/dist/rules/linter/linter.js.map +1 -0
- package/dist/rules/modules/resolver-browser.d.ts +41 -0
- package/dist/rules/modules/resolver-browser.d.ts.map +1 -0
- package/dist/rules/modules/resolver-browser.js +63 -0
- package/dist/rules/modules/resolver-browser.js.map +1 -0
- package/dist/rules/modules/resolver-core.d.ts +51 -0
- package/dist/rules/modules/resolver-core.d.ts.map +1 -0
- package/dist/rules/modules/resolver-core.js +338 -0
- package/dist/rules/modules/resolver-core.js.map +1 -0
- package/dist/rules/modules/resolver.d.ts +17 -0
- package/dist/rules/modules/resolver.d.ts.map +1 -0
- package/dist/rules/modules/resolver.js +49 -0
- package/dist/rules/modules/resolver.js.map +1 -0
- package/dist/rules/modules/stdlib/atomic.rules +52 -0
- package/dist/rules/modules/stdlib/atomic.test.json +152 -0
- package/dist/rules/modules/stdlib/auth.rules +7 -0
- package/dist/rules/modules/stdlib/auth.test.json +53 -0
- package/dist/rules/modules/stdlib/content.rules +50 -0
- package/dist/rules/modules/stdlib/content.test.json +238 -0
- package/dist/rules/modules/stdlib/counters.rules +41 -0
- package/dist/rules/modules/stdlib/counters.test.json +254 -0
- package/dist/rules/modules/stdlib/geometry.rules +43 -0
- package/dist/rules/modules/stdlib/geometry.test.json +200 -0
- package/dist/rules/modules/stdlib/joining.rules +49 -0
- package/dist/rules/modules/stdlib/joining.test.json +287 -0
- package/dist/rules/modules/stdlib/lifecycle.rules +51 -0
- package/dist/rules/modules/stdlib/lifecycle.test.json +288 -0
- package/dist/rules/modules/stdlib/lobby.rules +44 -0
- package/dist/rules/modules/stdlib/lobby.test.json +32 -0
- package/dist/rules/modules/stdlib/membership.rules +40 -0
- package/dist/rules/modules/stdlib/membership.test.json +107 -0
- package/dist/rules/modules/stdlib/spaces.rules +46 -0
- package/dist/rules/modules/stdlib/spaces.test.json +392 -0
- package/dist/rules/modules/stdlib/state.rules +28 -0
- package/dist/rules/modules/stdlib/state.test.json +55 -0
- package/dist/rules/modules/stdlib/timing.rules +20 -0
- package/dist/rules/modules/stdlib/timing.test.json +106 -0
- package/dist/rules/modules/stdlib/transitions.rules +34 -0
- package/dist/rules/modules/stdlib/transitions.test.json +86 -0
- package/dist/rules/modules/stdlib/turns.rules +27 -0
- package/dist/rules/modules/stdlib/turns.test.json +64 -0
- package/dist/rules/modules/stdlib/validation.rules +31 -0
- package/dist/rules/modules/stdlib/validation.test.json +184 -0
- package/dist/rules/modules/stdlib-content.d.ts +11 -0
- package/dist/rules/modules/stdlib-content.d.ts.map +1 -0
- package/dist/rules/modules/stdlib-content.js +590 -0
- package/dist/rules/modules/stdlib-content.js.map +1 -0
- package/dist/rules/node.d.ts +17 -0
- package/dist/rules/node.d.ts.map +1 -0
- package/dist/rules/node.js +21 -0
- package/dist/rules/node.js.map +1 -0
- package/dist/rules/rtdb.d.ts +28 -0
- package/dist/rules/rtdb.d.ts.map +1 -0
- package/dist/rules/rtdb.js +15 -0
- package/dist/rules/rtdb.js.map +1 -0
- package/dist/rules/simulator/evaluator.d.ts +179 -0
- package/dist/rules/simulator/evaluator.d.ts.map +1 -0
- package/dist/rules/simulator/evaluator.js +1221 -0
- package/dist/rules/simulator/evaluator.js.map +1 -0
- package/dist/rules/simulator/expression/eval-errors.d.ts +86 -0
- package/dist/rules/simulator/expression/eval-errors.d.ts.map +1 -0
- package/dist/rules/simulator/expression/eval-errors.js +68 -0
- package/dist/rules/simulator/expression/eval-errors.js.map +1 -0
- package/dist/rules/simulator/expression/evaluator.d.ts +43 -0
- package/dist/rules/simulator/expression/evaluator.d.ts.map +1 -0
- package/dist/rules/simulator/expression/evaluator.js +298 -0
- package/dist/rules/simulator/expression/evaluator.js.map +1 -0
- package/dist/rules/simulator/expression/lexer.d.ts +36 -0
- package/dist/rules/simulator/expression/lexer.d.ts.map +1 -0
- package/dist/rules/simulator/expression/lexer.js +318 -0
- package/dist/rules/simulator/expression/lexer.js.map +1 -0
- package/dist/rules/simulator/expression/parser.d.ts +39 -0
- package/dist/rules/simulator/expression/parser.d.ts.map +1 -0
- package/dist/rules/simulator/expression/parser.js +329 -0
- package/dist/rules/simulator/expression/parser.js.map +1 -0
- package/dist/rules/simulator/expression/types.d.ts +137 -0
- package/dist/rules/simulator/expression/types.d.ts.map +1 -0
- package/dist/rules/simulator/expression/types.js +90 -0
- package/dist/rules/simulator/expression/types.js.map +1 -0
- package/dist/rules/simulator/expression/walk-data.d.ts +52 -0
- package/dist/rules/simulator/expression/walk-data.d.ts.map +1 -0
- package/dist/rules/simulator/expression/walk-data.js +167 -0
- package/dist/rules/simulator/expression/walk-data.js.map +1 -0
- package/dist/rules/simulator/firestore-set.d.ts +25 -0
- package/dist/rules/simulator/firestore-set.d.ts.map +1 -0
- package/dist/rules/simulator/firestore-set.js +89 -0
- package/dist/rules/simulator/firestore-set.js.map +1 -0
- package/dist/rules/simulator/handler.d.ts +36 -0
- package/dist/rules/simulator/handler.d.ts.map +1 -0
- package/dist/rules/simulator/handler.js +583 -0
- package/dist/rules/simulator/handler.js.map +1 -0
- package/dist/rules/simulator/mapdiff.d.ts +29 -0
- package/dist/rules/simulator/mapdiff.d.ts.map +1 -0
- package/dist/rules/simulator/mapdiff.js +72 -0
- package/dist/rules/simulator/mapdiff.js.map +1 -0
- package/dist/rules/simulator/project-after-state.d.ts +33 -0
- package/dist/rules/simulator/project-after-state.d.ts.map +1 -0
- package/dist/rules/simulator/project-after-state.js +88 -0
- package/dist/rules/simulator/project-after-state.js.map +1 -0
- package/dist/rules/simulator/query-proof.d.ts +80 -0
- package/dist/rules/simulator/query-proof.d.ts.map +1 -0
- package/dist/rules/simulator/query-proof.js +142 -0
- package/dist/rules/simulator/query-proof.js.map +1 -0
- package/dist/rules/simulator/value-equality.d.ts +8 -0
- package/dist/rules/simulator/value-equality.d.ts.map +1 -0
- package/dist/rules/simulator/value-equality.js +40 -0
- package/dist/rules/simulator/value-equality.js.map +1 -0
- package/dist/rules/simulator/wrappers/base.d.ts +129 -0
- package/dist/rules/simulator/wrappers/base.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/base.js +90 -0
- package/dist/rules/simulator/wrappers/base.js.map +1 -0
- package/dist/rules/simulator/wrappers/bytes.d.ts +45 -0
- package/dist/rules/simulator/wrappers/bytes.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/bytes.js +130 -0
- package/dist/rules/simulator/wrappers/bytes.js.map +1 -0
- package/dist/rules/simulator/wrappers/duration.d.ts +76 -0
- package/dist/rules/simulator/wrappers/duration.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/duration.js +176 -0
- package/dist/rules/simulator/wrappers/duration.js.map +1 -0
- package/dist/rules/simulator/wrappers/float.d.ts +52 -0
- package/dist/rules/simulator/wrappers/float.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/float.js +88 -0
- package/dist/rules/simulator/wrappers/float.js.map +1 -0
- package/dist/rules/simulator/wrappers/latlng.d.ts +40 -0
- package/dist/rules/simulator/wrappers/latlng.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/latlng.js +100 -0
- package/dist/rules/simulator/wrappers/latlng.js.map +1 -0
- package/dist/rules/simulator/wrappers/path.d.ts +75 -0
- package/dist/rules/simulator/wrappers/path.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/path.js +158 -0
- package/dist/rules/simulator/wrappers/path.js.map +1 -0
- package/dist/rules/simulator/wrappers/reference.d.ts +82 -0
- package/dist/rules/simulator/wrappers/reference.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/reference.js +145 -0
- package/dist/rules/simulator/wrappers/reference.js.map +1 -0
- package/dist/rules/simulator/wrappers/timestamp.d.ts +86 -0
- package/dist/rules/simulator/wrappers/timestamp.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/timestamp.js +207 -0
- package/dist/rules/simulator/wrappers/timestamp.js.map +1 -0
- package/dist/rules/simulator/wrappers/vector.d.ts +43 -0
- package/dist/rules/simulator/wrappers/vector.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/vector.js +80 -0
- package/dist/rules/simulator/wrappers/vector.js.map +1 -0
- package/dist/rules/simulator-tools-impl.d.ts +64 -0
- package/dist/rules/simulator-tools-impl.d.ts.map +1 -0
- package/dist/rules/simulator-tools-impl.js +462 -0
- package/dist/rules/simulator-tools-impl.js.map +1 -0
- package/dist/rules/simulator.d.ts +20 -0
- package/dist/rules/simulator.d.ts.map +1 -0
- package/dist/rules/simulator.js +24 -0
- package/dist/rules/simulator.js.map +1 -0
- package/dist/rules/stdlib-modules.d.ts +73 -0
- package/dist/rules/stdlib-modules.d.ts.map +1 -0
- package/dist/rules/stdlib-modules.js +849 -0
- package/dist/rules/stdlib-modules.js.map +1 -0
- package/dist/rules/stdlib-tools.d.ts +19 -0
- package/dist/rules/stdlib-tools.d.ts.map +1 -0
- package/dist/rules/stdlib-tools.js +164 -0
- package/dist/rules/stdlib-tools.js.map +1 -0
- package/dist/rules/test/handler.d.ts +9 -0
- package/dist/rules/test/handler.d.ts.map +1 -0
- package/dist/rules/test/handler.js +124 -0
- package/dist/rules/test/handler.js.map +1 -0
- package/dist/rules/test/spec.d.ts +517 -0
- package/dist/rules/test/spec.d.ts.map +1 -0
- package/dist/rules/test/spec.js +227 -0
- package/dist/rules/test/spec.js.map +1 -0
- package/dist/rules/tools.d.ts +41 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +124 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/write/handler.d.ts +6 -0
- package/dist/rules/write/handler.d.ts.map +1 -0
- package/dist/rules/write/handler.js +126 -0
- package/dist/rules/write/handler.js.map +1 -0
- package/dist/rules/write/spec.d.ts +28 -0
- package/dist/rules/write/spec.d.ts.map +1 -0
- package/dist/rules/write/spec.js +2 -0
- package/dist/rules/write/spec.js.map +1 -0
- package/dist/sandbox/admin-compat.d.ts +20 -0
- package/dist/sandbox/admin-compat.d.ts.map +1 -0
- package/dist/sandbox/admin-compat.js +19 -0
- package/dist/sandbox/admin-compat.js.map +1 -0
- package/dist/sandbox/admin-firestore/error-translation.d.ts +86 -0
- package/dist/sandbox/admin-firestore/error-translation.d.ts.map +1 -0
- package/dist/sandbox/admin-firestore/error-translation.js +204 -0
- package/dist/sandbox/admin-firestore/error-translation.js.map +1 -0
- package/dist/sandbox/admin-firestore/index.d.ts +169 -0
- package/dist/sandbox/admin-firestore/index.d.ts.map +1 -0
- package/dist/sandbox/admin-firestore/index.js +456 -0
- package/dist/sandbox/admin-firestore/index.js.map +1 -0
- package/dist/sandbox/admin-firestore/remote.d.ts +97 -0
- package/dist/sandbox/admin-firestore/remote.d.ts.map +1 -0
- package/dist/sandbox/admin-firestore/remote.js +735 -0
- package/dist/sandbox/admin-firestore/remote.js.map +1 -0
- package/dist/sandbox/branches/index.d.ts +141 -0
- package/dist/sandbox/branches/index.d.ts.map +1 -0
- package/dist/sandbox/branches/index.js +327 -0
- package/dist/sandbox/branches/index.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/batch.d.ts +34 -0
- package/dist/sandbox/firestore/admin-compat/batch.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/batch.js +73 -0
- package/dist/sandbox/firestore/admin-compat/batch.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.d.ts +38 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.js +154 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/firestore.d.ts +45 -0
- package/dist/sandbox/firestore/admin-compat/firestore.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/firestore.js +114 -0
- package/dist/sandbox/firestore/admin-compat/firestore.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/index.d.ts +45 -0
- package/dist/sandbox/firestore/admin-compat/index.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/index.js +27 -0
- package/dist/sandbox/firestore/admin-compat/index.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/paths.d.ts +37 -0
- package/dist/sandbox/firestore/admin-compat/paths.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/paths.js +49 -0
- package/dist/sandbox/firestore/admin-compat/paths.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/query.d.ts +261 -0
- package/dist/sandbox/firestore/admin-compat/query.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/query.js +704 -0
- package/dist/sandbox/firestore/admin-compat/query.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.d.ts +8 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.js +59 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.d.ts +39 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.js +50 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/transaction.d.ts +35 -0
- package/dist/sandbox/firestore/admin-compat/transaction.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/transaction.js +88 -0
- package/dist/sandbox/firestore/admin-compat/transaction.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/types.d.ts +297 -0
- package/dist/sandbox/firestore/admin-compat/types.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/types.js +127 -0
- package/dist/sandbox/firestore/admin-compat/types.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/value-order.d.ts +49 -0
- package/dist/sandbox/firestore/admin-compat/value-order.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/value-order.js +172 -0
- package/dist/sandbox/firestore/admin-compat/value-order.js.map +1 -0
- package/dist/sandbox/firestore/auto-id.d.ts +26 -0
- package/dist/sandbox/firestore/auto-id.d.ts.map +1 -0
- package/dist/sandbox/firestore/auto-id.js +36 -0
- package/dist/sandbox/firestore/auto-id.js.map +1 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.d.ts +48 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.js +85 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.js.map +1 -0
- package/dist/sandbox/firestore/converters/fieldvalue.d.ts +69 -0
- package/dist/sandbox/firestore/converters/fieldvalue.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/fieldvalue.js +166 -0
- package/dist/sandbox/firestore/converters/fieldvalue.js.map +1 -0
- package/dist/sandbox/firestore/converters/reference.d.ts +31 -0
- package/dist/sandbox/firestore/converters/reference.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/reference.js +51 -0
- package/dist/sandbox/firestore/converters/reference.js.map +1 -0
- package/dist/sandbox/firestore/converters/timestamp.d.ts +32 -0
- package/dist/sandbox/firestore/converters/timestamp.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/timestamp.js +63 -0
- package/dist/sandbox/firestore/converters/timestamp.js.map +1 -0
- package/dist/sandbox/firestore/converters/user-timestamp.d.ts +46 -0
- package/dist/sandbox/firestore/converters/user-timestamp.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/user-timestamp.js +65 -0
- package/dist/sandbox/firestore/converters/user-timestamp.js.map +1 -0
- package/dist/sandbox/firestore/converters/vector.d.ts +24 -0
- package/dist/sandbox/firestore/converters/vector.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/vector.js +53 -0
- package/dist/sandbox/firestore/converters/vector.js.map +1 -0
- package/dist/sandbox/firestore/errors.d.ts +82 -0
- package/dist/sandbox/firestore/errors.d.ts.map +1 -0
- package/dist/sandbox/firestore/errors.js +54 -0
- package/dist/sandbox/firestore/errors.js.map +1 -0
- package/dist/sandbox/firestore/event-log.d.ts +99 -0
- package/dist/sandbox/firestore/event-log.d.ts.map +1 -0
- package/dist/sandbox/firestore/event-log.js +67 -0
- package/dist/sandbox/firestore/event-log.js.map +1 -0
- package/dist/sandbox/firestore/field-merge.d.ts +64 -0
- package/dist/sandbox/firestore/field-merge.d.ts.map +1 -0
- package/dist/sandbox/firestore/field-merge.js +221 -0
- package/dist/sandbox/firestore/field-merge.js.map +1 -0
- package/dist/sandbox/firestore/list-query-proof.d.ts +77 -0
- package/dist/sandbox/firestore/list-query-proof.d.ts.map +1 -0
- package/dist/sandbox/firestore/list-query-proof.js +170 -0
- package/dist/sandbox/firestore/list-query-proof.js.map +1 -0
- package/dist/sandbox/firestore/local-environment.d.ts +806 -0
- package/dist/sandbox/firestore/local-environment.d.ts.map +1 -0
- package/dist/sandbox/firestore/local-environment.js +3028 -0
- package/dist/sandbox/firestore/local-environment.js.map +1 -0
- package/dist/sandbox/firestore/local-state.d.ts +196 -0
- package/dist/sandbox/firestore/local-state.d.ts.map +1 -0
- package/dist/sandbox/firestore/local-state.js +413 -0
- package/dist/sandbox/firestore/local-state.js.map +1 -0
- package/dist/sandbox/firestore/overlay-backing.d.ts +44 -0
- package/dist/sandbox/firestore/overlay-backing.d.ts.map +1 -0
- package/dist/sandbox/firestore/overlay-backing.js +82 -0
- package/dist/sandbox/firestore/overlay-backing.js.map +1 -0
- package/dist/sandbox/firestore/sentinel-capture.d.ts +30 -0
- package/dist/sandbox/firestore/sentinel-capture.d.ts.map +1 -0
- package/dist/sandbox/firestore/sentinel-capture.js +61 -0
- package/dist/sandbox/firestore/sentinel-capture.js.map +1 -0
- package/dist/sandbox/firestore/snapshot-listeners.d.ts +284 -0
- package/dist/sandbox/firestore/snapshot-listeners.d.ts.map +1 -0
- package/dist/sandbox/firestore/snapshot-listeners.js +194 -0
- package/dist/sandbox/firestore/snapshot-listeners.js.map +1 -0
- package/dist/sandbox/firestore/topk.d.ts +12 -0
- package/dist/sandbox/firestore/topk.d.ts.map +1 -0
- package/dist/sandbox/firestore/topk.js +42 -0
- package/dist/sandbox/firestore/topk.js.map +1 -0
- package/dist/sandbox/firestore/transaction-merge.d.ts +55 -0
- package/dist/sandbox/firestore/transaction-merge.d.ts.map +1 -0
- package/dist/sandbox/firestore/transaction-merge.js +107 -0
- package/dist/sandbox/firestore/transaction-merge.js.map +1 -0
- package/dist/sandbox/firestore/transaction-types.d.ts +152 -0
- package/dist/sandbox/firestore/transaction-types.d.ts.map +1 -0
- package/dist/sandbox/firestore/transaction-types.js +10 -0
- package/dist/sandbox/firestore/transaction-types.js.map +1 -0
- package/dist/sandbox/firestore/transaction.d.ts +79 -0
- package/dist/sandbox/firestore/transaction.d.ts.map +1 -0
- package/dist/sandbox/firestore/transaction.js +95 -0
- package/dist/sandbox/firestore/transaction.js.map +1 -0
- package/dist/sandbox/firestore/value-equality.d.ts +2 -0
- package/dist/sandbox/firestore/value-equality.d.ts.map +1 -0
- package/dist/sandbox/firestore/value-equality.js +35 -0
- package/dist/sandbox/firestore/value-equality.js.map +1 -0
- package/dist/sandbox/firestore/value-resolver.d.ts +172 -0
- package/dist/sandbox/firestore/value-resolver.d.ts.map +1 -0
- package/dist/sandbox/firestore/value-resolver.js +221 -0
- package/dist/sandbox/firestore/value-resolver.js.map +1 -0
- package/dist/sandbox/firestore/wire-encoder.d.ts +14 -0
- package/dist/sandbox/firestore/wire-encoder.d.ts.map +1 -0
- package/dist/sandbox/firestore/wire-encoder.js +130 -0
- package/dist/sandbox/firestore/wire-encoder.js.map +1 -0
- package/dist/sandbox/index.d.ts +50 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +63 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/internal/index.d.ts +36 -0
- package/dist/sandbox/internal/index.d.ts.map +1 -0
- package/dist/sandbox/internal/index.js +72 -0
- package/dist/sandbox/internal/index.js.map +1 -0
- package/dist/sandbox/internal/sandbox-impl.d.ts +343 -0
- package/dist/sandbox/internal/sandbox-impl.d.ts.map +1 -0
- package/dist/sandbox/internal/sandbox-impl.js +718 -0
- package/dist/sandbox/internal/sandbox-impl.js.map +1 -0
- package/dist/sandbox/persistence/backends.d.ts +37 -0
- package/dist/sandbox/persistence/backends.d.ts.map +1 -0
- package/dist/sandbox/persistence/backends.js +202 -0
- package/dist/sandbox/persistence/backends.js.map +1 -0
- package/dist/sandbox/persistence/chunk-format.d.ts +58 -0
- package/dist/sandbox/persistence/chunk-format.d.ts.map +1 -0
- package/dist/sandbox/persistence/chunk-format.js +163 -0
- package/dist/sandbox/persistence/chunk-format.js.map +1 -0
- package/dist/sandbox/persistence/controller.d.ts +39 -0
- package/dist/sandbox/persistence/controller.d.ts.map +1 -0
- package/dist/sandbox/persistence/controller.js +605 -0
- package/dist/sandbox/persistence/controller.js.map +1 -0
- package/dist/sandbox/persistence/index.d.ts +13 -0
- package/dist/sandbox/persistence/index.d.ts.map +1 -0
- package/dist/sandbox/persistence/index.js +5 -0
- package/dist/sandbox/persistence/index.js.map +1 -0
- package/dist/sandbox/persistence/serialize.d.ts +53 -0
- package/dist/sandbox/persistence/serialize.d.ts.map +1 -0
- package/dist/sandbox/persistence/serialize.js +98 -0
- package/dist/sandbox/persistence/serialize.js.map +1 -0
- package/dist/sandbox/persistence/types.d.ts +118 -0
- package/dist/sandbox/persistence/types.d.ts.map +1 -0
- package/dist/sandbox/persistence/types.js +15 -0
- package/dist/sandbox/persistence/types.js.map +1 -0
- package/dist/sandbox/remote.d.ts +118 -0
- package/dist/sandbox/remote.d.ts.map +1 -0
- package/dist/sandbox/remote.js +60 -0
- package/dist/sandbox/remote.js.map +1 -0
- package/dist/sandbox/replay/index.d.ts +106 -0
- package/dist/sandbox/replay/index.d.ts.map +1 -0
- package/dist/sandbox/replay/index.js +246 -0
- package/dist/sandbox/replay/index.js.map +1 -0
- package/dist/sandbox/sandbox-context.d.ts +40 -0
- package/dist/sandbox/sandbox-context.d.ts.map +1 -0
- package/dist/sandbox/sandbox-context.js +72 -0
- package/dist/sandbox/sandbox-context.js.map +1 -0
- package/dist/sandbox/tab-sync/index.d.ts +120 -0
- package/dist/sandbox/tab-sync/index.d.ts.map +1 -0
- package/dist/sandbox/tab-sync/index.js +272 -0
- package/dist/sandbox/tab-sync/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +1235 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +46 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/storage/admin/api.d.ts +206 -0
- package/dist/storage/admin/api.d.ts.map +1 -0
- package/dist/storage/admin/api.js +349 -0
- package/dist/storage/admin/api.js.map +1 -0
- package/dist/storage/admin/handler.d.ts +29 -0
- package/dist/storage/admin/handler.d.ts.map +1 -0
- package/dist/storage/admin/handler.js +82 -0
- package/dist/storage/admin/handler.js.map +1 -0
- package/dist/storage/admin/spec.d.ts +64 -0
- package/dist/storage/admin/spec.d.ts.map +1 -0
- package/dist/storage/admin/spec.js +8 -0
- package/dist/storage/admin/spec.js.map +1 -0
- package/dist/storage/admin/tools.d.ts +23 -0
- package/dist/storage/admin/tools.d.ts.map +1 -0
- package/dist/storage/admin/tools.js +90 -0
- package/dist/storage/admin/tools.js.map +1 -0
- package/dist/storage/download.d.ts +27 -0
- package/dist/storage/download.d.ts.map +1 -0
- package/dist/storage/download.js +136 -0
- package/dist/storage/download.js.map +1 -0
- package/dist/storage/enforce.d.ts +26 -0
- package/dist/storage/enforce.d.ts.map +1 -0
- package/dist/storage/enforce.js +16 -0
- package/dist/storage/enforce.js.map +1 -0
- package/dist/storage/errors.d.ts +46 -0
- package/dist/storage/errors.d.ts.map +1 -0
- package/dist/storage/errors.js +62 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/index.d.ts +49 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +60 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/internal.d.ts +18 -0
- package/dist/storage/internal.d.ts.map +1 -0
- package/dist/storage/internal.js +18 -0
- package/dist/storage/internal.js.map +1 -0
- package/dist/storage/list.d.ts +17 -0
- package/dist/storage/list.d.ts.map +1 -0
- package/dist/storage/list.js +91 -0
- package/dist/storage/list.js.map +1 -0
- package/dist/storage/metadata.d.ts +86 -0
- package/dist/storage/metadata.d.ts.map +1 -0
- package/dist/storage/metadata.js +167 -0
- package/dist/storage/metadata.js.map +1 -0
- package/dist/storage/persistence.d.ts +134 -0
- package/dist/storage/persistence.d.ts.map +1 -0
- package/dist/storage/persistence.js +132 -0
- package/dist/storage/persistence.js.map +1 -0
- package/dist/storage/reference.d.ts +63 -0
- package/dist/storage/reference.d.ts.map +1 -0
- package/dist/storage/reference.js +156 -0
- package/dist/storage/reference.js.map +1 -0
- package/dist/storage/rules.d.ts +176 -0
- package/dist/storage/rules.d.ts.map +1 -0
- package/dist/storage/rules.js +621 -0
- package/dist/storage/rules.js.map +1 -0
- package/dist/storage/service.d.ts +139 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +241 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/upload.d.ts +43 -0
- package/dist/storage/upload.d.ts.map +1 -0
- package/dist/storage/upload.js +235 -0
- package/dist/storage/upload.js.map +1 -0
- 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
|