{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/t-k/fireemu/main/spec/config/fireemu.schema.json",
  "title": "fireemu canonical configuration",
  "description": "Single source of truth for the daemon configuration (spec section 17). Unknown keys are errors everywhere. Cross-field constraints that JSON Schema cannot express are enforced by tools/config-schema-check and by the Rust config loader.",
  "type": "object",
  "additionalProperties": false,
  "required": ["schemaVersion"],
  "properties": {
    "$schema": {
      "type": "string",
      "enum": ["https://raw.githubusercontent.com/t-k/fireemu/main/spec/config/fireemu.schema.json", "https://fireemu.dev/spec/config/fireemu.schema.json"],
      "examples": ["https://raw.githubusercontent.com/t-k/fireemu/main/spec/config/fireemu.schema.json"]
    },
    "eventarc": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "authentication": {
          "type": "string",
          "enum": ["permissive", "production"],
          "default": "permissive",
          "description": "Whether the strict Eventarc listener checks the request's credential. permissive (the development default, in both profiles) admits any credential or none, as the official emulator does, so the Admin SDK publishes with a fake credential in CI; every other strict check keeps production's order and answers. production refuses a missing, malformed or JWT-shaped bearer with production's 401 and applies oauthCredentials. Defaults to production under productionExact, and under the strict profile when oauthCredentials is configured; an explicit value wins, except that permissive with oauthCredentials under strict is an error. production requires the strict profile."
        },
        "oauthCredentials": {
          "description": "Optional local OAuth credential authority for strict Eventarc, applied when eventarc.authentication is production (which it selects when authentication is absent). Keys are SHA-256 digests of synthetic bearer bytes; absence preserves legacy shape-only admission. An empty catalog admits no OAuth-shaped credential. This does not verify Google credentials or IAM permissions.",
          "type": "object",
          "propertyNames": { "minLength": 64, "maxLength": 64, "pattern": "^[0-9a-f]{64}$" },
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "required": ["scopes"],
            "properties": {
              "scopes": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }
            }
          }
        }
      }
    },
    "schemaVersion": { "const": 1 },
    "firebaseJson": {
      "type": "string",
      "minLength": 1,
      "description": "Firebase project configuration loaded relative to this canonical file."
    },
    "profile": {
      "description": "Compatibility profile (spec/compatibility/contract.json). strict is recommended, including when migrating from the official emulator; emulator is only for checking alignment with the pinned official emulator and is not needed for a migration. strict behaves like production Firebase where the official emulators do not (composite index validation with production's rules, Standard query limits, ID-token verification); emulator reproduces the pinned official emulators, including their documented limitations. The profile decides the index validation and the ID-token semantics of the Security Rules surfaces and sets the default of firestore.enforceLimits; an explicit enforceLimits wins over it.",
      "enum": ["strict", "emulator"],
      "default": "strict"
    },
    "productionExact": {
      "type": "boolean",
      "default": false,
      "description": "fireemu is run mostly by tests in CI and on a developer's machine, so where production needs setup a test should not have to do (signed tokens, signing keys, Admin API calls), the default is the development behaviour, in both profiles. true restores production's behaviour for every such default; an explicit key still decides over it. Requires the strict profile. docs/compatibility-contract.md (\"Development defaults\") lists the keys it moves."
    },
    "bind": { "type": "string", "default": "127.0.0.1", "description": "Currently has no effect. The daemon binds the loopback address; emulators.<name>.host in firebase.json sets a host per service." },
    "network": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "allowNonLoopbackHosts": {
          "type": "boolean",
          "default": false,
          "description": "Let emulators.<name>.host in firebase.json name a non-loopback IP address such as 0.0.0.0 or :: for the product emulators (firestore, auth, storage, functions, eventarc, tasks, pubsub), for example to publish them from a container. They serve without a credential, so every such listener is warned about at start. The Hub, the UI and the Logging emulator stay on loopback, and Auth, whose port serves the control API, binds loopback or every interface only. The control API (/v1/, /health/), App Check debug-token management and the routes that load Security Rules or clear Firestore data answer loopback peers only, and every control API request but /health/ then needs Authorization: Bearer <control token> (FIREEMU_CONTROL_TOKEN in a fireemu exec command). The rule-loading and data-clear routes keep their unauthenticated access for a loopback peer, so a forwarder inside the network namespace (kubectl port-forward, a sidecar proxy) still reaches them."
        }
      }
    },
    "projects": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "requireDemoPrefix": { "type": "boolean", "default": true },
        "unknownProjects": {
          "description": "Under the strict profile, refuse a request naming a project other than the daemon's with production's 403 PERMISSION_DENIED (CONSUMER_INVALID) instead of serving it in its own session (scope decision C11 of FS-CONFIG-LIFECYCLE).",
          "enum": ["serve", "refuse"],
          "default": "serve"
        }
      }
    },
    "limits": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "catalog": { "type": "string", "pattern": "^firestore-(standard|enterprise-native)-[0-9]{4}-[0-9]{2}-[0-9]{2}$", "description": "Currently has no effect." },
        "queryCatalog": {
          "oneOf": [
            { "type": "null" },
            { "type": "string", "pattern": "^firestore-standard-query-[0-9]{4}-[0-9]{2}-[0-9]{2}$" }
          ],
          "description": "Currently has no effect."
        },
        "rulesCatalog": { "type": "string", "pattern": "^firebase-rules-[0-9]{4}-[0-9]{2}-[0-9]{2}$", "description": "Currently has no effect." },
        "enforcement": { "enum": ["strict", "observe"], "description": "Currently has no effect." },
        "warningThresholds": { "$ref": "#/$defs/thresholds" },
        "perLimitWarningThresholds": {
          "type": "object",
          "propertyNames": { "pattern": "^[A-Z0-9-]+$" },
          "additionalProperties": { "$ref": "#/$defs/thresholds" }
        },
        "warningsAsErrors": {
          "type": "array",
          "items": { "type": "string", "pattern": "^([A-Z0-9-]+|notice|warning|critical)$" },
          "uniqueItems": true
        },
        "quotaAccounting": { "enum": ["off", "observe", "enforce"] },
        "firestorePlan": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "billingEnabled": { "type": "boolean" },
            "compositeIndexLimitOverride": { "$ref": "#/$defs/optionalCount" },
            "singleFieldConfigLimitOverride": { "$ref": "#/$defs/optionalCount" },
            "enterpriseIndexLimitOverride": { "$ref": "#/$defs/optionalCount" }
          }
        }
      }
    },
    "firestore": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "edition": { "enum": ["standard", "enterprise"] },
        "apiMode": { "enum": ["native", "mongodb-compatible"] },
        "rules": {
          "type": "string",
          "description": "Deprecated, and never read: the Firestore rules file is rules.source in this file or firestore.rules in firebase.json. Kept so that configuration files written with this key keep loading; the daemon says at start-up that it has no effect."
        },
        "location": {
          "type": "string",
          "minLength": 1,
          "pattern": "^[^\\u0000-\\u0020\\u007f-\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]+(?![\\s\\S])",
          "default": "nam5",
          "description": "Database location used in strict second-generation Firestore event attributes. Defaults to nam5; the emulator profile always emits nam5 regardless of this setting. Locations are opaque identifiers: unknown non-empty strings are accepted verbatim, but whitespace and control characters are rejected anywhere. This setting does not control routing or perform a location lookup."
        },
        "indexFile": { "type": "string" },
        "textIndexDefinitionFile": { "type": "string" },
        "enforceLimits": { "type": "boolean" },
        "missingIndexes": {
          "type": "string",
          "enum": ["report", "refuse"],
          "default": "refuse",
          "description": "What the strict profile does with a query whose composite index is not defined. refuse (the default under the strict profile, as in production): production's FAILED_PRECONDITION with its console link to the index. report (an explicit opt-out for a project that wants the query to run): run the query as if the index existed, but only where production would refuse it (a query production serves by merging indexes is not reported), and print once per database and index production's refusal text, with the console link, and the firestore.indexes.json entry (an index an Admin create is still building, or one that was deleted, also runs, and is reported in production's wording for that state); the query's answer carries the FS_MISSING_INDEX_REPORTED warning. productionExact does not move this key. The emulator profile always runs the query, as the official emulator checks no composite index, and refuse requires the strict profile."
        },
        "listenStreamLifetime": {
          "type": "string",
          "enum": ["oneHour", "unlimited"],
          "default": "unlimited",
          "description": "Whether an end user's native gRPC Listen stream ends with INTERNAL an hour after it opened, as production ends it. unlimited (the default, and \"oneHour\" under productionExact) keeps a long local session open; oneHour, which requires the strict profile, closes it as production does. POST /v1/firestore/listen:expire on the control API ends every held end-user stream at once in either case."
        },
        "databaseCreateTime": {
          "type": "string",
          "format": "date-time",
          "description": "When the daemon's databases were created: the createTime they report and the instant before which a read_time is refused with INVALID_ARGUMENT. Defaults to the daemon's start; a run compared with a production database names that database's createTime. It must not be after the clock start."
        },
        "deletedDatabaseIdCooldownSeconds": {
          "type": "integer",
          "minimum": 0,
          "maximum": 300,
          "default": 300,
          "description": "How long the id of a deleted database stays unavailable to databases.create. Production's is 300 seconds; a comparison run may shorten it, because how long a transitional state lasts is not compared (FS-CONFIG-LIFECYCLE scope decision C10)."
        },
        "ttlSweepIntervalSeconds": {
          "type": "integer",
          "minimum": 1,
          "maximum": 259200,
          "default": 86400,
          "description": "How long a document whose time-to-live field has expired stays readable before the expiry sweep deletes it. Production deletes an expired document typically within 24 hours and within 72 hours at worst, so the default is 86400 and the maximum is the documented outer bound of 259200."
        },
        "history": {
          "type": "object",
          "additionalProperties": false,
          "description": "Local bounds on retained MVCC history, which production does not have. Unset, a database retains at most 1 GiB of logical history bytes and 1,000,000 versions; a document keeps every version of the last hour on the wall clock, and on a pinned clock (daemon.clockStart) at most 1,024, a cap that works like maxVersionsPerPath. A commit that would cross a limit is refused whole with RESOURCE_EXHAUSTED. Unset keys keep the defaults.",
          "properties": {
            "maxVersionsPerPath": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000,
              "description": "How many versions of one document a database retains, on either clock, in place of the defaults. The effect reaches the whole database: once any document has more versions than the cap, no snapshot older than that document's oldest kept version is retained for any document. A read_time read or a new read-only transaction older than that is refused with FAILED_PRECONDITION, a transaction already open on an older snapshot is ABORTED (The referenced transaction has expired or is no longer valid.), and a Listen resume from there is reset. SDKs retry ABORTED, so one busy document under a very small cap can keep aborting unrelated transactions."
            },
            "maxBytes": {
              "type": "integer",
              "minimum": 1048576,
              "maximum": 1073741824,
              "default": 1073741824,
              "description": "The logical history bytes one database retains (Firestore's storage size model plus fixed metadata charges, not process memory)."
            }
          }
        },
        "explain": { "type": "boolean", "description": "Currently has no effect." },
        "pipeline": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enabled": { "type": "boolean", "description": "Currently has no effect." },
            "executionMode": { "enum": ["strict-native", "proxy", "record-only"], "description": "Currently has no effect." },
            "unknownStagePolicy": { "enum": ["reject"] },
            "writeStagePolicy": { "enum": ["reject-unimplemented"] }
          }
        },
        "textSearch": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "fidelity": { "enum": ["strict-validation-only", "deterministic-basic", "external-analyzer", "upstream-proxy"], "description": "Currently has no effect." },
            "analyzer": { "type": "string", "description": "Currently has no effect." },
            "scorePrecision": { "enum": ["exact", "approximate", "oracle-only", "unsupported"], "description": "Currently has no effect." },
            "multipleIndexPolicy": { "enum": ["warn-and-deterministic", "reject", "oracle"], "description": "Currently has no effect." },
            "indexBuildPolicy": { "enum": ["synchronous", "virtual-async", "manual"], "description": "Currently has no effect." },
            "visibilityPolicy": { "enum": ["atomic-with-commit", "virtual-lag", "oracle-defined"], "description": "Currently has no effect." },
            "visibilityLag": {
              "oneOf": [
                { "type": "null" },
                { "type": "object", "additionalProperties": false, "required": ["maxLogicalDelayMillis"], "properties": { "maxLogicalDelayMillis": { "type": "integer", "minimum": 0 } } }
              ],
              "description": "Currently has no effect."
            },
            "queryLanguagePolicy": { "enum": ["index-default-or-autodetect", "detect-query-text", "analyze-per-indexed-language", "oracle-defined"], "description": "Currently has no effect." },
            "backendDefaultLanguageOverride": { "enum": ["implicit-language-field", "disabled", "unresolved"], "description": "Currently has no effect." },
            "snapshotPolicy": { "enum": ["materialized", "rebuild"], "description": "Currently has no effect." },
            "warnings": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "broadCandidateCount": { "type": "integer", "minimum": 0, "description": "Currently has no effect." },
                "scoreCandidateCount": { "type": "integer", "minimum": 0, "description": "Currently has no effect." },
                "warningsAsErrors": { "type": "array", "items": { "type": "string", "pattern": "^FS_[A-Z0-9_]+$" }, "uniqueItems": true, "description": "Currently has no effect." }
              }
            }
          }
        }
      }
    },
    "rules": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "executionMode": { "enum": ["admin-bypass", "native", "oracle", "disabled"] },
        "source": { "type": "string" },
        "denialLog": {
          "enum": ["summary", "off"],
          "default": "summary",
          "description": "Whether each Security Rules denial of an end-user Firestore or Storage request is printed on standard error with every allow condition it evaluated and what it evaluated to (summary), or not (off). The client is answered with production's denial either way."
        },
        "staticLimitChecks": { "type": "boolean", "description": "Currently has no effect." },
        "runtimeBudgets": { "type": "boolean", "description": "Currently has no effect." },
        "compiledSize": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "mode": { "enum": ["estimate", "off"], "description": "Currently has no effect." },
            "oracle": { "enum": ["off", "projects-test", "disposable-ruleset"], "description": "Currently has no effect." }
          }
        },
        "diagnostics": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "format": { "enum": ["human", "json", "sarif"], "description": "Currently has no effect." },
            "largestContributors": { "type": "integer", "minimum": 0, "description": "Currently has no effect." }
          }
        }
      }
    },
    "auth": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "description": "Currently has no effect." },
        "projectIssuer": { "type": "string", "pattern": "^https://securetoken\\.google\\.com/[a-z0-9-]+$", "description": "Currently has no effect." },
        "idTokenSigning": { "enum": ["session-rsa", "unsigned-emulator"] },
        "apiKeys": {
          "type": "array",
          "minItems": 1,
          "description": "The default project's Web API keys. When present, a client request with any other key is refused with production's 400 API_KEY_INVALID before the service reads it; when absent, any key is accepted, as by the official emulator.",
          "items": { "type": "string", "pattern": "^[A-Za-z0-9._-]+$" }
        },
        "allowUnsignedCustomTokens": {
          "type": "boolean",
          "default": true,
          "description": "Accept well-formed unsigned custom JWTs from the Admin SDK in emulator mode, in both strict and emulator profiles, even when customTokenSigners is configured. Defaults to true, and to false under productionExact. Set false to require signed custom tokens verified by customTokenSigners. Strict claim validation still applies. In the emulator profile, false also disables JSON fake custom tokens. This setting does not affect ID tokens or other credentials; see allowUnsignedIdTokens."
        },
        "allowUnsignedIdTokens": {
          "type": "boolean",
          "default": true,
          "description": "Allow unsigned mock ID token fallback after normal verification fails on Firestore, Storage and Realtime Database Security Rules surfaces. Defaults to true in both profiles, and to false under productionExact; an explicit value wins. Under strict the fallback accepts only the createMockUserToken shape: exactly three JWT segments, header bytes {\"alg\":\"none\",\"type\":\"JWT\"}, an empty signature, a subject and the routed project as audience; exp is not checked by the fallback. Under emulator the fallback accepts any unsigned token with the required claims. false disables only this fallback: fireemu-issued unsigned ID tokens (header typ, not type) that pass normal verification remain accepted. Changing only an issued token's header to the SDK shape can still pass normal verification. For a refusal test, use the SDK helper's default iat 0 / exp 3600, or that exact shape and those times, with a clock after expiry including Firestore's 30-second allowance. Set auth.idTokenSigning to session-rsa to refuse all unsigned ID tokens. Callable Functions, Identity Toolkit and App Check are not affected."
        },
        "customTokenSigners": {
          "type": "object",
          "description": "Service accounts whose signed custom tokens are accepted, each mapped to its public JWK set (the document Google publishes at https://www.googleapis.com/service_accounts/v1/jwk/<service account>). Signed custom tokens are accepted only when these keys verify an RS256 signature; a verifying token of another project's service account is refused with CREDENTIAL_MISMATCH, as production does. Unsigned emulator custom tokens are accepted by default in both profiles independently of these keys; set allowUnsignedCustomTokens to false to refuse them. Without configured keys, signed custom tokens are refused.",
          "propertyNames": { "pattern": "^[^@\\s]+@([a-z0-9-]+\\.iam|appspot)\\.gserviceaccount\\.com$" },
          "additionalProperties": {
            "type": "object",
            "required": ["keys"],
            "properties": {
              "keys": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "object",
                  "required": ["kty", "n", "e"],
                  "properties": {
                    "kty": { "const": "RSA" },
                    "alg": { "const": "RS256" },
                    "kid": { "type": "string" },
                    "n": { "type": "string" },
                    "e": { "type": "string" }
                  }
                }
              }
            }
          }
        },
        "providers": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": false,
          "description": "Initial project custom provider resources, validated through the selected profile's existing Admin create operation. Each name is a provider ID (oidc.<id> or saml.<id>), never a full resource name. Missing/null collections are undeclared; [] declares an empty replacement. Admin changes affect live resources until reset restores only declared kinds. Later projects inherit the declaration, while tenant and built-in providers remain separate. Account clearing preserves live resources; snapshots and Auth export/import do not transfer declarations or provider credentials. Strict OIDC still requires matching auth.idpSigners and all current signature/trust checks; emulator-profile fixture sign-in continues without provider resources, while the strict profile refuses an oidc.* or saml.* provider that has none (or is disabled) even with fixture assertions.",
          "properties": {
            "oidc": {
              "type": [
                "array",
                "null"
              ],
              "items": {
                "type": "object",
                "required": [
                  "name",
                  "clientId",
                  "issuer"
                ],
                "properties": {
                  "displayName": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 6,
                    "maxLength": 128,
                    "pattern": "^oidc\\.[A-Za-z0-9._-]+$"
                  },
                  "clientId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "issuer": {
                    "type": "string",
                    "pattern": "^https?://[^\\s\\u0000-\\u001f\\u007f-\\u009f]+$"
                  },
                  "clientSecret": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "responseType": {
                    "type": "object",
                    "properties": {
                      "idToken": {
                        "type": "boolean"
                      },
                      "code": {
                        "type": "boolean"
                      },
                      "token": {
                        "const": false
                      }
                    },
                    "oneOf": [
                      {
                        "required": [
                          "idToken"
                        ],
                        "properties": {
                          "idToken": {
                            "const": true
                          },
                          "code": {
                            "const": false
                          }
                        }
                      },
                      {
                        "required": [
                          "code"
                        ],
                        "properties": {
                          "code": {
                            "const": true
                          },
                          "idToken": {
                            "const": false
                          }
                        }
                      }
                    ]
                  }
                },
                "allOf": [
                  {
                    "if": {
                      "required": [
                        "responseType"
                      ],
                      "properties": {
                        "responseType": {
                          "required": [
                            "code"
                          ],
                          "properties": {
                            "code": {
                              "const": true
                            }
                          }
                        }
                      }
                    },
                    "then": {
                      "required": [
                        "clientSecret"
                      ],
                      "properties": {
                        "clientSecret": {
                          "type": "string",
                          "minLength": 1
                        }
                      }
                    }
                  }
                ]
              }
            },
            "saml": {
              "type": [
                "array",
                "null"
              ],
              "items": {
                "type": "object",
                "required": [
                  "name",
                  "idpConfig",
                  "spConfig"
                ],
                "properties": {
                  "displayName": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 6,
                    "maxLength": 128,
                    "pattern": "^saml\\.[A-Za-z0-9._-]+$"
                  },
                  "idpConfig": {
                    "type": "object",
                    "required": [
                      "idpEntityId",
                      "ssoUrl",
                      "idpCertificates"
                    ],
                    "properties": {
                      "idpEntityId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "ssoUrl": {
                        "type": "string",
                        "pattern": "^https?://[^\\s\\u0000-\\u001f\\u007f-\\u009f]+$"
                      },
                      "idpCertificates": {
                        "type": "array",
                        "minItems": 1,
                        "items": {
                          "type": "object",
                          "required": [
                            "x509Certificate"
                          ],
                          "properties": {
                            "x509Certificate": {
                              "type": "string",
                              "minLength": 1
                            }
                          }
                        }
                      },
                      "signRequest": {
                        "type": "boolean"
                      }
                    }
                  },
                  "spConfig": {
                    "type": "object",
                    "required": [
                      "spEntityId",
                      "callbackUri"
                    ],
                    "properties": {
                      "spEntityId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "callbackUri": {
                        "type": "string",
                        "pattern": "^https?://[^\\s\\u0000-\\u001f\\u007f-\\u009f]+$"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "idpAssertions": {
          "type": "string",
          "enum": ["fixture", "signed"],
          "default": "fixture",
          "description": "How signInWithIdp assertions are checked. fixture (the default in both profiles) accepts the official emulator's fixture assertions without verifying them: a JSON or unsigned ID token, a JSON SAML response and what the sign-in widget (/emulator/auth/handler) posts, for built-in, oidc.* and saml.* providers; the strict profile's answer shapes and other checks are unchanged. signed (strict profile only) verifies them as production does: an oidc.* ID token only with an idpSigners key, a saml.* response only with the provider's certificates, and the fixture and the built-in providers are refused. Under the strict profile the default is signed with productionExact or when idpSigners is set, and fixture beside idpSigners is refused."
        },
        "idpSigners": {
          "type": "object",
          "description": "OIDC issuers whose signed ID tokens the strict profile verifies with idpAssertions signed, each mapped to its public JWK set; under the strict profile, setting it makes signed the default of idpAssertions. No key is fetched from an issuer. With signed assertions, signInWithIdp accepts only an ID token of an enabled oidc.* provider whose issuer is listed here and whose key (selected by the token's kid) verifies it; the unsigned fixture IdP and other providers are refused. The emulator profile ignores this setting.",
          "propertyNames": { "pattern": "^https://[^/?#\\s]+(/[^?#\\s]*)?$" },
          "additionalProperties": {
            "type": "object",
            "required": ["keys"],
            "properties": {
              "authorization_endpoint": {
                "type": "string",
                "pattern": "^https://[^/?#\\s]+(/[^?#\\s]*)?$",
                "description": "The issuer's authorization endpoint, as its discovery document names it. With it, the strict profile answers createAuthUri for the issuer's oidc.* providers (the ID-token flow); no discovery document is fetched."
              },
              "scopes_supported": {
                "type": "array",
                "minItems": 1,
                "maxItems": 32,
                "items": { "type": "string", "pattern": "^[A-Za-z0-9._:/-]{1,128}$" },
                "description": "The issuer's scopes_supported, as its discovery document lists them. The strict profile's createAuthUri asks for these scopes, in this order, and for openid without them (inferred from production's answers for two issuers); no discovery document is fetched."
              },
              "keys": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "object",
                  "required": ["kty", "n", "e", "kid", "alg", "use"],
                  "not": { "anyOf": [{ "required": ["d"] }, { "required": ["p"] }, { "required": ["q"] }, { "required": ["dp"] }, { "required": ["dq"] }, { "required": ["qi"] }, { "required": ["oth"] }, { "required": ["k"] }] },
                  "properties": {
                    "kty": { "const": "RSA" },
                    "alg": { "const": "RS256" },
                    "use": { "const": "sig" },
                    "kid": { "type": "string", "minLength": 1 },
                    "n": { "type": "string", "maxLength": 1366 },
                    "e": { "type": "string", "maxLength": 8 }
                  }
                }
              }
            }
          }
        },
        "authorizedDomains": {
          "type": ["array", "null"],
          "description": "The project's initial authorized domains, replacing the whole default list (include localhost if needed). Empty means no authorized host under strict; absent or null declares no seed and keeps localhost plus the project's Firebase Hosting domains. Validated with the selected profile's Admin PATCH rules: empty strings are refused in both profiles, strict also checks hostname syntax, numeric and boolean entries become strings, and null entries are omitted. Prefer string entries. Admin updates change the live list without changing this declaration; session reset restores only declared domains. Later projects inherit the declaration rather than the default project's live list. Account clearing does not restore it; Auth export/import carries neither the live list nor the declaration. The emulator profile still does not enforce the continue-URL allowlist. Tenant-scoped strict action links currently check the tenant's default domains rather than this project declaration; support for the parent project's live list is pending.",
          "items": { "type": ["string", "number", "boolean", "null"], "minLength": 1 },
          "examples": [["localhost", "app.test"], [], null]
        },
        "totp": {
          "type": "object",
          "description": "Configures fireemu's TOTP extension. Under strict, auth.totp alone does not enable enrollment: the project MFA configuration must have state ENABLED or MANDATORY and a providerConfigs entry with state ENABLED or MANDATORY and totpProviderConfig. Under the strict profile that is the development default (see auth.mfa), so auth.totp only tunes TOTP there; under productionExact, or with a declared auth.mfa that leaves TOTP off, the configuration must be seeded through auth.mfa or set through the Admin API. auth.totp is not required when that MFA configuration enables TOTP. Under emulator, declaring auth.totp enables the extension; an enabling project MFA configuration also enables TOTP without it. Tenants under strict require their own enabling mfaConfig. See README.md#enable-multi-factor-authentication-at-start for a minimal strict configuration.",
          "additionalProperties": false,
          "properties": {
            "periodSeconds": { "type": "integer", "minimum": 1 },
            "digits": { "type": "integer", "minimum": 6, "maximum": 8 },
            "windowSteps": { "type": "integer", "minimum": 0, "maximum": 10 },
            "enrollmentSessionTtlSeconds": { "type": "integer", "minimum": 1 }
          }
        },
        "mfa": {
          "type": ["object", "null"],
          "description": "The project's initial multi-factor configuration, in the Identity Platform Config.mfa shape: it is the project's configuration when the daemon starts and for every project it creates later, and a control-plane reset (POST /v1/sessions/{s}/reset) returns to it. It is validated as the Admin API validates an update of the mfa member. Under the strict profile TOTP is enabled only by the project's multi-factor configuration (auth.totp alone enables nothing, as in production); an Admin API update replaces the live configuration (the seed is kept, and a reset returns to it), and disabling refuses TOTP as production does. Absent or null, a development default applies (owner ledger 1413): under the strict profile a project starts with multi-factor on, SMS and TOTP enabled (state ENABLED, PHONE_SMS and an enabled totpProviderConfig; the accepted TOTP window stays auth.totp's), so a test needs no console toggle or updateConfig call, and a reset returns to it. productionExact restores production's state, a new project with multi-factor off until it is enabled (a reset then leaves an Admin-set configuration alone). A declared block always decides, so {\"state\": \"DISABLED\"} turns multi-factor off whatever productionExact says. The emulator profile is unchanged. Tenants keep their own mfaConfig.",
          "additionalProperties": false,
          "properties": {
            "state": {
              "description": "The project's multi-factor state. Under the strict profile it is ENABLED when auth.mfa is absent (a development default), and unset under productionExact; a declared auth.mfa decides.",
              "enum": ["STATE_UNSPECIFIED", "DISABLED", "ENABLED", "MANDATORY"]
            },
            "enabledProviders": {
              "type": ["array", "null"],
              "items": { "enum": ["PROVIDER_UNSPECIFIED", "PHONE_SMS"] }
            },
            "providerConfigs": {
              "type": ["array", "null"],
              "items": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "state": { "enum": ["STATE_UNSPECIFIED", "DISABLED", "ENABLED", "MANDATORY"] },
                  "totpProviderConfig": {
                    "type": ["object", "null"],
                    "additionalProperties": false,
                    "properties": {
                      "adjacentIntervals": { "type": ["integer", "null"], "minimum": 0, "maximum": 10 }
                    }
                  }
                }
              }
            }
          }
        },
        "multiTenant": {
          "type": ["object", "null"],
          "description": "The default project's multi-tenancy switch, as the Admin API's multiTenant.allowTenants: it is set when the daemon starts, for every session project created later, and after a control-plane reset (POST /v1/sessions/{s}/reset). Under the strict profile tenants are created only in a project that allows them, as in production. Absent or null, a development default applies (owner ledger 1413): under the strict profile the switch is on in a project nobody has written it in, so Admin tenant creation and declaring auth.tenants need no updateConfig call or allowTenants: true. productionExact restores production's new project, the switch unset until it is enabled, and declaring auth.tenants then requires allowTenants: true. A declared value, or one written through the Admin API (false included), always decides. A reset leaves an Admin-written value alone, in every session; without a declared value the default only sets the initial state of a project. The emulator profile is unchanged.",
          "additionalProperties": false,
          "properties": {
            "allowTenants": {
              "type": "boolean",
              "description": "Whether the project allows tenants. Under the strict profile it is true when the key is absent (a development default), and unset under productionExact; a declared value decides."
            }
          }
        },
        "tenants": {
          "type": ["array", "null"],
          "description": "The default project's tenants, each an Admin v2 Tenant document with its tenantId: under auth.tenantIdFormat \"readable\" (the default) any id that starts with a letter and holds letters, digits and hyphens (at most 63 characters, such as tenant-a), under \"generated\" (productionExact) a display name, a hyphen and five characters of [a-z0-9], as production generates it. A document is read by the code the Admin tenant create route reads one with, so a value production refuses stops the daemon at startup. The tenants are created empty in the default project when the daemon starts, in every session project created later, and again after a control-plane reset of a session (the default session's included); an --import is authoritative for a tenant of the same id. A namespace the emulator profile creates for an unknown project id is seeded when it is created (by a request that writes a tenant there, or by an Admin user, provider or project-setting write). Absent or null, no tenant is created.",
          "items": {
            "type": "object",
            "required": ["tenantId", "displayName"],
            "properties": {
              "tenantId": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,62}$" },
              "displayName": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{3,19}$" }
            }
          }
        },
        "tenantIdFormat": {
          "enum": ["readable", "generated"],
          "default": "readable",
          "description": "Which tenantId a tenant of auth.tenants may have. readable (the default, a development default, and \"generated\" under productionExact): any id that starts with a letter and holds letters, digits and hyphens, at most 63 characters, such as tenant-a. generated: the id production would generate, the display name, a hyphen and five characters of [a-z0-9]."
        },
        "secretMaterialization": { "enum": ["redact", "allow"], "description": "Currently has no effect." },
        "forwardInboundCredentials": {
          "type": "boolean",
          "description": "Allow blocking Auth Functions to receive caller-supplied IdP access, ID and refresh tokens. Defaults to false."
        },
        "improvedEmailPrivacy": {
          "type": "boolean",
          "description": "Email enumeration protection, as production enables it for every new project: wrong-password and unknown-address sign-ins answer INVALID_LOGIN_CREDENTIALS and a password reset for an unknown address is acknowledged. Defaults to true under the strict profile and to false, the official Auth emulator's default, under the emulator profile; false gives the official Auth emulator's EMAIL_NOT_FOUND and INVALID_PASSWORD answers."
        },
        "signIn": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "allowDuplicateEmails": { "type": "boolean", "default": false },
            "email": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "enabled": { "type": "boolean", "default": true },
                "passwordRequired": { "type": "boolean", "default": false }
              }
            },
            "anonymous": {
              "type": "object",
              "additionalProperties": false,
              "properties": { "enabled": { "type": "boolean", "default": true } }
            },
            "phoneNumber": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "enabled": { "type": "boolean", "default": true },
                "testPhoneNumbers": {
                  "type": "object",
                  "maxProperties": 10,
                  "propertyNames": { "pattern": "^\\+[1-9][0-9]{1,14}$" },
                  "additionalProperties": { "type": "string" }
                }
              }
            }
          },
          "description": "The project's sign-in providers. Every provider starts enabled in both profiles (a new production project enables none), so a production project's providers and test phone numbers can be carried over here."
        },
        "client": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "permissions": { "$ref": "#/$defs/authClientPermissions" }
          }
        },
        "configOverrides": {
          "type": "array",
          "items": { "$ref": "#/$defs/authConfigOverride" },
          "description": "Explicit project/tenant overrides for client.permissions and improvedEmailPrivacy. Password policies use passwordPolicyOverrides."
        },
        "blockingFunctions": { "$ref": "#/$defs/blockingFunctions" },
        "quota": { "$ref": "#/$defs/authQuota" },
        "quotaSimulation": { "$ref": "#/$defs/authQuotaSimulation" },
        "logActionCodes": {
          "type": "boolean",
          "description": "Print every email action link and SMS verification code to the daemon's standard output (and the Logging emulator) as the official Auth emulator does. Defaults to true. These lines carry credentials: set false where the console is shared or captured, and read the codes from /emulator/v1/projects/{project}/oobCodes and verificationCodes instead. --log-verbosity quiet also silences them."
        },
        "passwordPolicy": { "$ref": "#/$defs/passwordPolicy" },
        "passwordPolicyOverrides": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["projectId", "passwordPolicy"],
            "properties": {
              "projectId": { "type": "string", "minLength": 1 },
              "tenantId": { "type": "string", "minLength": 1 },
              "passwordPolicy": { "$ref": "#/$defs/passwordPolicy" }
            }
          }
        }
      }
    },
    "appCheck": {
      "type": "object",
      "additionalProperties": false,
      "description": "Firebase App Check (docs/specifications/firebase-app-check.md section 8). appCheck.enabled defaults to false: no product baseline applies, so an existing configuration keeps its behaviour; the exchange and the callable trust boundary are served anyway under the strict profile unless an appCheck block, productionExact or debugTokens \"registered\" says otherwise. Cross-field rules (a non-off service mode needs enabled, one project ID per project number and back, one app ID per project, an embedded project number that matches) are enforced by tools/config-schema-check and by the Rust loader.",
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "debugTokens": { "enum": ["any", "registered"], "default": "any", "description": "Which debug tokens the exchange accepts. The strict profile's development default is \"any\" when there is no appCheck block: any well-formed UUIDv4 debug token exchanges for any app with a standard app ID, which is registered on first use, so an enforceAppCheck callable works without registering apps or computing digests (a missing or malformed token is still refused). \"registered\" is production's rule, where only apps from apps[] with a registered digest exchange; it is the default under productionExact, under the emulator profile, and in any appCheck block that does not set this key." },
        "storageServerRequests": { "enum": ["loopback", "capability"], "default": "loopback", "description": "How server-side Cloud Storage JSON API traffic (the Admin SDK, gcloud) passes appCheck.services.storage \"enforced\". \"loopback\" admits a /storage/v1, /upload/storage/v1 or /download/storage/v1 request from a loopback peer that addresses a loopback Host (a DNS-rebound name is refused) and carries no browser evidence (no Origin, Sec-Fetch-Site, Sec-Fetch-Dest, Referer or Cookie header) and no \"Authorization: Firebase <token>\" client credential, a resumable upload continuation included; fireemu exec then exports the plain STORAGE_EMULATOR_HOST=http://host:port. Production does not apply App Check to the Admin SDK, which authenticates with a service account over OAuth, so this is production's behaviour, and productionExact does not change it. \"capability\" admits only the per-run capability fireemu exec carries as userinfo in STORAGE_EMULATOR_HOST (http://fireemu:<capability>@host:port), which @google-cloud/storage 8.1 and later (firebase-admin 14.4 and later) refuse, and which a resumable continuation URL does not carry. The Firebase Storage API (/v0, the client SDKs) always needs an App Check token under enforcement." },
        "tokenSigning": { "enum": ["instance-rsa"], "default": "instance-rsa" },
        "tokenTtlSeconds": { "type": "integer", "minimum": 1800, "maximum": 604800, "default": 3600 },
        "apps": {
          "type": "array",
          "maxItems": 1024,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["projectId", "projectNumber", "appId"],
            "properties": {
              "projectId": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$" },
              "projectNumber": { "type": "string", "pattern": "^[1-9][0-9]{0,31}$" },
              "appId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "enabled": { "type": "boolean", "default": true },
              "debugTokenSha256": {
                "type": "array",
                "maxItems": 128,
                "uniqueItems": true,
                "items": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
              }
            }
          }
        },
        "services": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "auth": { "$ref": "#/$defs/appCheckMode" },
            "firestore": { "$ref": "#/$defs/appCheckMode" },
            "storage": { "$ref": "#/$defs/appCheckMode" }
          }
        }
      }
    },
    "daemon": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "firestorePort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "httpPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "storagePort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "functionsPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "eventarcPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "tasksPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "pubsubPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "databasePort": {
          "type": "integer",
          "minimum": 0,
          "maximum": 65535,
          "description": "Port of the Realtime Database emulator, the same setting as emulators.database.port in firebase.json and --database-port. Setting it starts the emulator; the official default port is 9000."
        },
        "loggingPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
        "hubPort": {
          "type": "integer",
          "minimum": 0,
          "maximum": 65535,
          "description": "Port of the Emulator Hub, the same setting as emulators.hub.port in firebase.json and --hub-port. The default is the official 4400, best effort: a busy default disables discovery, a busy explicit port is an error. 0 turns the hub off."
        },
        "uiPort": {
          "type": "integer",
          "minimum": 0,
          "maximum": 65535,
          "description": "Port of the web UI, the same setting as emulators.ui.port in firebase.json and --ui-port. The default is the official 4000, best effort: a busy default disables the UI, a busy explicit port is an error. 0 turns the UI off."
        },
        "clockStart": { "type": "string", "format": "date-time" },
        "seed": { "type": "integer", "minimum": 0 },
        "authProject": { "type": "string", "pattern": "^[a-z0-9-]+$" },
        "authProjectNumbers": { "type": "object", "propertyNames": { "pattern": "^[a-z0-9-]+$" }, "additionalProperties": { "$ref": "#/$defs/authProjectNumber" } }
      }
    },
    "storage": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "rules": { "type": "string", "description": "Path of the Storage Security Rules file (service firebase.storage)." },
        "mode": { "enum": ["native"] },
        "objectNameNormalization": { "enum": ["preserve"] },
        "maxStoredBytes": {
          "type": "integer",
          "minimum": 1,
          "description": "The object data the Storage emulator retains, in bytes, a local bound production does not have. Unset, nothing is bounded. A write that would grow the total past it (upload, resumable finalize, copy) is refused with 402 and the message storage.maxStoredBytes limit exceeded, and changes nothing; a resumable session stays open. Each object counts its bytes once (a copy counts again). Import and snapshot restore are not checked against it; an import above it starts with a warning."
        },
        "buckets": {
          "type": "array",
          "items": { "type": "string", "minLength": 3, "maxLength": 222 },
          "description": "Cloud Storage buckets the project owns besides its two default ones (<project>.appspot.com and <project>.firebasestorage.app). Under strict the first write to a bucket that is not declared is served, as the official emulator serves it, but reported once on standard error (production answers 404 for a bucket that does not exist), so a typo in a bucket name is visible; declare the bucket here to silence that note. A bucket named by firebase.json storage targets, by a deployed Storage trigger, or holding imported data exists without being listed. A name listed twice counts once. Each name must be one Cloud Storage accepts (3 to 222 lowercase letters, digits, '-', '_' and '.', starting and ending with a letter or digit). The emulator profile creates a bucket on first use without a note and ignores this key."
        },
        "defaultBucket": {
          "enum": ["both", "firebasestorage.app", "appspot.com"],
          "default": "both",
          "description": "Which of the project's two default buckets exist under strict. A production project has one: <project>.firebasestorage.app when Storage was set up after 2024-10-30, <project>.appspot.com before. A local run cannot know which, so both exist by default. Name the one your project has: the other name then counts as an undeclared bucket, so its first write prints the undeclared-bucket note and the bucket resource answers 404 (production answers 404 for the name your project does not have). The emulator profile ignores this key."
        },
        "publicBuckets": {
          "type": "array",
          "items": { "type": "string", "minLength": 3, "maxLength": 222 },
          "description": "Buckets whose objects anyone can read through the public URL (GET /{bucket}/{object}, what file.publicUrl() builds) without a credential. It stands for a bucket whose IAM grants allUsers the object viewer role, which production sets in the console. Under strict that URL otherwise answers 403 AccessDenied for an object that was not made public in application code (file.makePublic(), predefinedAcl=publicRead or an allUsers READER ACL entry). Firebase download-token URLs and requests that carry an Authorization header are not affected. A bucket listed here also counts as declared. A name listed twice counts once. Each name must be one Cloud Storage accepts. The emulator profile serves the public URL to anyone and ignores this key."
        },
        "persistence": { "enum": ["memory", "snapshot-files", "redb"], "description": "Currently has no effect." }
      }
    },
    "events": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "delivery": { "enum": ["exactly-once-test", "at-least-once", "chaos"] },
        "maxAttempts": { "type": "integer", "minimum": 1 }
      }
    },
    "database": {
      "type": "object",
      "additionalProperties": false,
      "description": "The Realtime Database emulator (served with --only database, emulators.database or daemon.databasePort).",
      "properties": {
        "defaultAccess": {
          "enum": ["open", "locked"],
          "default": "open",
          "description": "What a database instance without a rules file allows; an instance with one follows its rules. \"open\" (development default, as the official emulator): every request. \"locked\" (productionExact; strict profile only): only the emulator's owner credential (Authorization: Bearer owner), as a new production database in locked mode admits only administrators."
        }
      }
    },
    "pubsub": {
      "type": "object",
      "description": "Pub/Sub emulator behaviour that the service configuration has no counterpart for.",
      "additionalProperties": false,
      "properties": {
        "pushMinimumRedeliveryIntervalMillis": {
          "description": "Minimum interval kept between two push deliveries of the same message while the subscription has no retry policy. The default 0 follows production, which redelivers as soon as possible without a retry policy; a non-zero interval is an opt-in emulator protection against re-requesting a failing push endpoint with no interval at all. A retry policy always decides its own backoff.",
          "type": "integer",
          "minimum": 0,
          "maximum": 600000,
          "default": 0
        },
        "projectNumbers": {
          "type": "object",
          "additionalProperties": { "type": "string", "pattern": "^[1-9][0-9]{0,19}$" },
          "description": "The real Pub/Sub project number of each source project, as a positive decimal string, used to name the service agent a dead-letter grant is for. Independent of the Auth project numbers. Under the strict profile, naming a number selects deadLetterAuthorization \"iam\" unless that key is set."
        },
        "authentication": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "localBearerToken": {
              "type": "string",
              "minLength": 1,
              "description": "A local fixture credential: under the strict profile the Pub/Sub endpoint then accepts only one Authorization header exactly equal to \"Bearer <token>\". The emulator profile ignores it. Never reuse a production credential."
            }
          },
          "description": "Strict-profile admission of Pub/Sub requests."
        },
        "deadLetterAuthorization": {
          "enum": ["implicit", "iam"],
          "default": "implicit",
          "description": "Whether strict dead-letter forwarding needs production's grants: the Pub/Sub service agent (service-<projectNumber>@gcp-sa-pubsub.iam.gserviceaccount.com) as roles/pubsub.subscriber on the source subscription and roles/pubsub.publisher on the dead-letter topic. The development default \"implicit\" forwards as if both were granted, so a test of dead lettering needs no project number and no SetIamPolicy calls. \"iam\" is production's rule (the default under productionExact, strict profile only): nothing is forwarded until both grants exist."
        }
      }
    },
    "scheduler": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "clock": { "enum": ["virtual", "wall"] },
        "defaultTimeZone": { "type": "string" },
        "catchUp": { "enum": ["all", "latest", "none"] },
        "maxCatchUpRuns": { "type": "integer", "minimum": 1, "maximum": 100000 },
        "overlap": { "enum": ["allow", "skip", "queue", "reject", "skip-in-flight"] }
      }
    },
    "functions": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "clock": { "type": "object", "additionalProperties": false, "properties": {
          "date": { "enum": ["real", "virtual"], "default": "real" },
          "timers": { "enum": ["real", "virtual"], "default": "real" },
          "tasks": { "enum": ["real", "virtual"], "default": "real" }
        } },
        "manifest": { "type": "string" },
        "source": { "type": "string", "description": "Functions codebase directory loaded by the runner (discovery)." },
        "runner": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "Runner command; default: node tools/runner-node/index.mjs" },
        "maxGlobalConcurrency": { "type": "integer", "minimum": 1 },
        "unservedTriggers": { "enum": ["refuse", "report"], "description": "An exported trigger whose product fireemu does not serve: refuse discovery (default) or report each one and carry on." }
      }
    },
    "trace": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "description": "Currently has no effect." },
        "canonical": { "type": "boolean", "description": "Currently has no effect." }
      }
    }
  },
  "$defs": {
    "passwordPolicy": {
      "type": "object",
      "additionalProperties": false,
      "required": ["enforcementState"],
      "properties": {
        "enforcementState": { "enum": ["OFF", "ENFORCE"] },
        "forceUpgradeOnSignin": { "type": "boolean", "default": false },
        "constraints": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "minLength": { "type": "integer", "minimum": 6, "maximum": 30, "default": 6 },
            "maxLength": { "oneOf": [{ "type": "null" }, { "type": "integer", "minimum": 6, "maximum": 4096 }] },
            "requireUppercase": { "type": "boolean", "default": false },
            "requireLowercase": { "type": "boolean", "default": false },
            "requireNumeric": { "type": "boolean", "default": false },
            "requireNonAlphanumeric": { "type": "boolean", "default": false }
          }
        }
      }
    },
    "authClientPermissions": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "disabledUserSignup": { "type": "boolean", "default": false },
        "disabledUserDeletion": { "type": "boolean", "default": false }
      }
    },
    "authConfigOverride": {
      "type": "object",
      "additionalProperties": false,
      "required": ["projectId", "config"],
      "properties": {
        "projectId": { "type": "string", "minLength": 1 },
        "tenantId": { "type": "string", "minLength": 1 },
        "config": {
          "type": "object",
          "additionalProperties": false,
          "minProperties": 1,
          "properties": {
            "client": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "permissions": { "$ref": "#/$defs/authClientPermissions" }
              }
            },
            "improvedEmailPrivacy": { "type": "boolean" }
          }
        }
      }
    },
    "blockingFunctionTrigger": {
      "oneOf": [
        { "type": "null" },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["function"],
          "properties": {
            "function": { "type": "string", "minLength": 1 },
            "region": { "type": "string", "minLength": 1 }
          }
        }
      ]
    },
    "blockingFunctions": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "triggers": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "beforeCreate": { "$ref": "#/$defs/blockingFunctionTrigger" },
            "beforeSignIn": { "$ref": "#/$defs/blockingFunctionTrigger" }
          }
        },
        "forwardInboundCredentials": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "idToken": { "type": "boolean", "default": false },
            "accessToken": { "type": "boolean", "default": false },
            "refreshToken": { "type": "boolean", "default": false }
          }
        }
      }
    },
    "authQuota": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "signUpQuotaConfig": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": false,
              "required": ["quota", "startTime", "quotaDuration"],
              "properties": {
                "quota": { "type": "string", "pattern": "^[0-9]+$" },
                "startTime": { "type": "string", "format": "date-time" },
                "quotaDuration": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]{1,9})?s$" }
              }
            }
          ]
        }
      }
    },
    "authQuotaSimulation": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": { "enum": ["off", "observe", "enforce"], "default": "off" },
        "algorithm": { "enum": ["fixed-window-v1"], "default": "fixed-window-v1" },
        "defaultQuotaPerHour": { "type": "integer", "minimum": 0, "maximum": 1000000, "default": 100 },
        "maxTrackedBuckets": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 4096 }
      }
    },
    "appCheckMode": { "enum": ["off", "unenforced", "enforced"], "default": "off" },
    "authProjectNumber": {
      "type": "string",
      "description": "A positive decimal project number that fits the u64 runtime representation.",
      "oneOf": [
        { "pattern": "^[1-9][0-9]{0,18}$" },
        { "pattern": "^1[0-7][0-9]{18}$" },
        { "pattern": "^18[0-3][0-9]{17}$" },
        { "pattern": "^184[0-3][0-9]{16}$" },
        { "pattern": "^1844[0-5][0-9]{15}$" },
        { "pattern": "^18446[0-6][0-9]{14}$" },
        { "pattern": "^184467[0-3][0-9]{13}$" },
        { "pattern": "^1844674[0-3][0-9]{12}$" },
        { "pattern": "^184467440[0-6][0-9]{10}$" },
        { "pattern": "^1844674407[0-2][0-9]{9}$" },
        { "pattern": "^18446744073[0-6][0-9]{8}$" },
        { "pattern": "^1844674407370[0-8][0-9]{6}$" },
        { "pattern": "^18446744073709[0-4][0-9]{5}$" },
        { "pattern": "^184467440737095[0-4][0-9]{4}$" },
        { "pattern": "^18446744073709550[0-9]{3}$" },
        { "pattern": "^18446744073709551[0-5][0-9]{2}$" },
        { "pattern": "^1844674407370955160[0-9]$" },
        { "pattern": "^1844674407370955161[0-5]$" }
      ]
    },
    "optionalCount": { "oneOf": [ { "type": "null" }, { "type": "integer", "minimum": 0 } ] },
    "thresholds": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["basisPoints", "severity"],
        "properties": {
          "basisPoints": { "type": "integer", "minimum": 1, "maximum": 10000 },
          "severity": { "enum": ["notice", "warning", "critical"] }
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "profile": {
            "not": {
              "const": "emulator"
            }
          }
        }
      },
      "then": {
        "properties": {
          "auth": {
            "properties": {
              "providers": {
                "properties": {
                  "oidc": {
                    "items": {
                      "properties": {
                        "issuer": {
                          "pattern": "^https://[^\\s\\u0000-\\u001f\\u007f-\\u009f]+$"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "not": {
          "required": [
            "profile"
          ],
          "properties": {
            "profile": {
              "const": "emulator"
            }
          }
        }
      },
      "then": {
        "properties": {
          "auth": {
            "properties": {
              "authorizedDomains": {
                "items": {
                  "if": {
                    "type": "string"
                  },
                  "then": {
                    "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$"
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "anyOf": [
          {
            "required": ["auth"],
            "properties": { "auth": { "required": ["tenantIdFormat"], "properties": { "tenantIdFormat": { "const": "generated" } } } }
          },
          {
            "required": ["productionExact"],
            "properties": {
              "productionExact": { "const": true },
              "auth": { "not": { "required": ["tenantIdFormat"] } }
            }
          }
        ]
      },
      "then": {
        "properties": {
          "auth": {
            "properties": {
              "tenants": {
                "items": {
                  "properties": {
                    "tenantId": { "pattern": "^[A-Za-z][A-Za-z0-9-]{3,19}-[a-z0-9]{5}$" }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": {
        "required": ["profile"],
        "properties": { "profile": { "const": "emulator" } }
      },
      "then": {
        "properties": {
          "firestore": {
            "properties": {
              "listenStreamLifetime": { "not": { "const": "oneHour" } }
            }
          }
        }
      }
    }
  ]
}
