{
  "contract": {
    "name": "bmai-identity",
    "version": "2.0.0"
  },
  "package": {
    "name": "@busymate/identity-contract",
    "version": "2.0.0-beta.2"
  },
  "statuses": [
    "live",
    "planned",
    "legacy"
  ],
  "urls": [
    {
      "id": "manifest",
      "status": "live",
      "url": "https://busymate.ai/.well-known/bmai-integration.json"
    },
    {
      "id": "guide",
      "status": "live",
      "url": "https://busymate.ai/docs/identity"
    },
    {
      "id": "backend-samples",
      "status": "live",
      "url": "https://busymate.ai/sdk/v1/kit/backend/"
    },
    {
      "id": "embed-script-path",
      "status": "live",
      "url": "/embed/v1.js"
    },
    {
      "id": "bridge-v2",
      "status": "live",
      "url": "https://busymate.ai/sdk/v2/2.0.0/"
    },
    {
      "id": "actor-jwks",
      "status": "live",
      "url": "https://busymate.ai/.well-known/bmai-actor-jwks.json"
    },
    {
      "id": "identity-check",
      "status": "live",
      "url": "https://busymate.ai/developers/identity-check"
    }
  ],
  "releases": [
    {
      "id": "web-sdk",
      "status": "live",
      "version": "0.1.1",
      "summary": "@busymate/ai-web (npm)",
      "repo": "serebano/busymate-ai-sdk-web",
      "url": "https://www.npmjs.com/package/@busymate/ai-web",
      "next": "becomes the one web path; the script tag becomes its build"
    },
    {
      "id": "embed-script",
      "status": "live",
      "version": "v1",
      "summary": "Website script /embed/v1.js",
      "url": "https://busymate.ai/embed/v1.js",
      "next": "becomes a build of the Web SDK"
    },
    {
      "id": "sdk-ios",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI ios SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-ios",
      "url": "https://github.com/serebano/busymate-ai-sdk-ios/releases/tag/1.0.2"
    },
    {
      "id": "sdk-android",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI android SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-android",
      "url": "https://github.com/serebano/busymate-ai-sdk-android/releases/tag/1.0.2"
    },
    {
      "id": "sdk-macos",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI macos SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-macos",
      "url": "https://github.com/serebano/busymate-ai-sdk-macos/releases/tag/1.0.2"
    },
    {
      "id": "sdk-windows",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI windows SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-windows",
      "url": "https://github.com/serebano/busymate-ai-sdk-windows/releases/tag/1.0.2"
    },
    {
      "id": "sdk-linux",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI linux SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-linux",
      "url": "https://github.com/serebano/busymate-ai-sdk-linux/releases/tag/1.0.2"
    },
    {
      "id": "sdk-electron",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI electron SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-electron",
      "url": "https://github.com/serebano/busymate-ai-sdk-electron/releases/tag/1.0.2"
    },
    {
      "id": "sdk-tauri",
      "status": "live",
      "version": "1.0.2",
      "summary": "Busymate AI tauri SDK, bridge busymate.bridge/2",
      "repo": "serebano/busymate-ai-sdk-tauri",
      "url": "https://github.com/serebano/busymate-ai-sdk-tauri/releases/tag/1.0.2"
    },
    {
      "id": "bridge-v2",
      "status": "live",
      "version": "2.0.0",
      "summary": "Frozen bridge files busymate.bridge/2 (React Native, Flutter)",
      "url": "https://busymate.ai/sdk/v2/2.0.0/"
    },
    {
      "id": "kit-v1",
      "status": "legacy",
      "version": "1",
      "summary": "v1 kit drop-ins (web, desktop preload, RN, Flutter, backend samples)",
      "url": "https://busymate.ai/sdk/v1/kit/"
    },
    {
      "id": "verify",
      "status": "live",
      "version": "0.1.2",
      "summary": "@busymate/ai-verify (Node/Deno actor-token verifier; Python, PHP, Go files in the repository)",
      "repo": "serebano/busymate-ai-sdk-verify",
      "url": "https://www.npmjs.com/package/@busymate/ai-verify"
    },
    {
      "id": "identity-check",
      "status": "live",
      "version": "0.1.2",
      "summary": "npx @busymate/identity-check (conformance CLI)",
      "repo": "serebano/busymate-ai-sdk-verify",
      "url": "https://www.npmjs.com/package/@busymate/identity-check"
    }
  ],
  "launch": {
    "algorithms": [
      "ES256",
      "RS256"
    ],
    "defaultAudience": "busymate-ai",
    "maxTokenAgeSeconds": {
      "min": 30,
      "max": 900,
      "schemaDefault": 300,
      "consoleDefault": 120,
      "guideSeconds": 120
    },
    "clockSkewSeconds": {
      "min": 0,
      "max": 120,
      "default": 30
    },
    "requiredClaims": [
      "exp",
      "iat",
      "sub",
      "jti",
      "nonce"
    ],
    "sessionTtlMinutes": {
      "min": 5,
      "max": 1440,
      "default": 120
    },
    "claims": [
      {
        "name": "iss",
        "required": true,
        "status": "live",
        "rule": "Your issuer. Must equal the registered issuer."
      },
      {
        "name": "aud",
        "required": true,
        "status": "live",
        "rule": "`busymate-ai`. Must equal a registered audience."
      },
      {
        "name": "sub",
        "required": true,
        "status": "live",
        "rule": "Your immutable internal user id. Never an e-mail address or phone number, and one person per `sub`. A shared organization id gives every member the same account."
      },
      {
        "name": "{tenantClaim}",
        "required": true,
        "status": "live",
        "rule": "Your tenant value (default claim name `tenant_id`). Must equal the registered tenant claim value."
      },
      {
        "name": "nonce",
        "required": true,
        "status": "live",
        "rule": "The `nonce` from the request, unchanged. Busymate generates it; you never make your own."
      },
      {
        "name": "jti",
        "required": true,
        "status": "live",
        "rule": "Random, unique per token. Single use: a replay gets `409`."
      },
      {
        "name": "iat",
        "required": true,
        "status": "live",
        "rule": "Now."
      },
      {
        "name": "exp",
        "required": true,
        "status": "live",
        "rule": "Now + 120 s. Keep it at your provider's maximum token age or less."
      },
      {
        "name": "name, email, ...",
        "required": false,
        "status": "live",
        "rule": "Optional display claims. Only what the chat may show; never secrets."
      }
    ]
  },
  "provider": {
    "fields": [
      {
        "name": "issuer",
        "required": true,
        "status": "live",
        "description": "HTTPS issuer; `iss` is an exact string match."
      },
      {
        "name": "jwksUri",
        "required": true,
        "status": "live",
        "description": "HTTPS JWKS URL publishing your ES256/RS256 public keys."
      },
      {
        "name": "audiences",
        "required": true,
        "status": "live",
        "description": "One or more accepted `aud` values (default `busymate-ai`)."
      },
      {
        "name": "tenantClaim",
        "required": true,
        "status": "live",
        "description": "Claim naming your tenant (default `tenant_id`)."
      },
      {
        "name": "tenantClaimValue",
        "required": true,
        "status": "live",
        "description": "The value that claim must carry."
      },
      {
        "name": "subjectClaim",
        "required": true,
        "status": "live",
        "description": "Claim holding the immutable user id (default `sub`)."
      },
      {
        "name": "displayClaims",
        "required": false,
        "status": "live",
        "description": "Claims the chat may show (at most 20)."
      },
      {
        "name": "allowedAlgs",
        "required": true,
        "status": "live",
        "description": "Subset of ES256, RS256."
      },
      {
        "name": "maxTokenAgeSeconds",
        "required": true,
        "status": "live",
        "description": "Maximum `exp - iat`, 30 to 900."
      },
      {
        "name": "clockSkewSeconds",
        "required": true,
        "status": "live",
        "description": "Allowed clock skew, 0 to 120."
      },
      {
        "name": "identityEndpointUrl",
        "required": true,
        "status": "live",
        "description": "Your mint endpoint: POST {nonce} returns 201 {token, nonce, expiresIn}."
      },
      {
        "name": "loginUrl",
        "required": false,
        "status": "live",
        "description": "Your login page, for hosted pages and custom domains."
      },
      {
        "name": "accountUrl",
        "required": false,
        "status": "live",
        "description": "Where signed-in customers manage their account."
      },
      {
        "name": "delegatedAccess",
        "required": false,
        "status": "legacy",
        "description": "Advertises delegated tools under signed actor tokens. Removed when `customerIdentity` ships (P3)."
      }
    ],
    "limits": {
      "maxProviders": 20,
      "maxAudiences": 20,
      "maxDisplayClaims": 20
    }
  },
  "handoff": {
    "loginUrlParams": {
      "returnTo": "return_to",
      "nonce": "bmai_nonce",
      "client": "bmai_client",
      "prompt": "bmai_prompt"
    },
    "promptNone": "none",
    "responses": {
      "success": "#bmai_token=<token>&bmai_nonce=<nonce>",
      "loginRequired": "#error=login_required",
      "loginRequiredAlt": "#bmai_error=login_required"
    },
    "pendingWindowSeconds": 600,
    "silentBudgetMs": 3000,
    "requirements": [
      {
        "id": "framing",
        "status": "live",
        "text": "Load inside a hidden frame on the chat host: send `Content-Security-Policy: frame-ancestors <chat origins>` and do not send `X-Frame-Options: DENY`."
      },
      {
        "id": "cookie",
        "status": "live",
        "text": "Send the session cookie with `Secure; SameSite=None`, or the frame cannot see the signed-in session."
      },
      {
        "id": "no-ui",
        "status": "live",
        "text": "On `bmai_prompt=none` never render UI: redirect with the token, or with `#error=login_required`."
      },
      {
        "id": "exact-return",
        "status": "live",
        "text": "Redirect to exactly `return_to` plus the fragment; the nonce is echoed unchanged."
      }
    ]
  },
  "lifecycle": {
    "web": [
      {
        "name": "getIdentity({ nonce, reason })",
        "status": "live",
        "description": "Calls your mint endpoint with THAT nonce; returns { token, nonce }, or null when signed out."
      },
      {
        "name": "identityChanged()",
        "status": "live",
        "description": "After sign-in, token rotation or an account switch."
      },
      {
        "name": "signedOut(reason)",
        "status": "live",
        "description": "After logout. Without it the next person at that browser sees the previous person's chat."
      },
      {
        "name": "account()",
        "status": "live",
        "description": "Optional: returns the current user id (or null) so a missed account switch is caught."
      }
    ],
    "native": [
      {
        "name": "account",
        "status": "live",
        "description": "Returns the signed-in user id, or null. Only a hash leaves the app."
      },
      {
        "name": "mint",
        "status": "live",
        "description": "Calls your mint endpoint with the request's nonce; never cached."
      },
      {
        "name": "accountChanged()",
        "status": "live",
        "description": "On login, logout and account switch."
      }
    ],
    "aliases": [
      {
        "alias": "logout()",
        "of": "signedOut",
        "status": "legacy",
        "documented": false
      },
      {
        "alias": "setUser(null)",
        "of": "signedOut",
        "status": "legacy",
        "documented": false
      },
      {
        "alias": "switchAccount()",
        "of": "identityChanged",
        "status": "legacy",
        "documented": false
      },
      {
        "alias": "identify()",
        "of": "identityChanged",
        "status": "legacy",
        "documented": false
      },
      {
        "alias": "refreshIdentity()",
        "of": "identityChanged",
        "status": "legacy",
        "documented": false
      },
      {
        "alias": "assistant*",
        "of": "native bridge v1 assistant calls",
        "status": "legacy",
        "documented": false
      }
    ],
    "desktopChannel": "desktop_sdk",
    "forbiddenDesktopChannel": "desktop"
  },
  "actor": {
    "v2": {
      "version": 2,
      "status": "live",
      "alg": "HS256",
      "typ": "JWT",
      "kid": "bmai-support-v2",
      "issuer": "https://busymate.ai",
      "lifetimeSeconds": 300,
      "clockSkewSeconds": 10,
      "transport": "Authorization: Bearer <token>",
      "verifierMinBytes": 32,
      "verifierGraceHours": 24,
      "actorKinds": [
        "identified",
        "anonymous"
      ],
      "claims": [
        {
          "name": "iss",
          "status": "live",
          "rule": "`https://busymate.ai`."
        },
        {
          "name": "aud",
          "status": "live",
          "rule": "The EXACT origin (scheme and host) of the connector URL."
        },
        {
          "name": "sub",
          "status": "live",
          "rule": "The verified user id. For `actor_kind: \"anonymous\"` it is `anonymous:<session>`: never map it to an account."
        },
        {
          "name": "tenant_id",
          "status": "live",
          "rule": "Your Busymate tenant id."
        },
        {
          "name": "connector_id",
          "status": "live",
          "rule": "Your connector id."
        },
        {
          "name": "support_session_id",
          "status": "live",
          "rule": "The chat session."
        },
        {
          "name": "policy_revision",
          "status": "live",
          "rule": "The published revision that allowed the call."
        },
        {
          "name": "actor_kind",
          "status": "live",
          "rule": "`identified` or `anonymous`. Check it: anonymous means public data only."
        },
        {
          "name": "confirmed",
          "status": "live",
          "rule": "True only for a call the customer confirmed. Require it for money and writes."
        },
        {
          "name": "iat",
          "status": "live",
          "rule": "Issued at."
        },
        {
          "name": "nbf",
          "status": "live",
          "rule": "Not before (iat - 5)."
        },
        {
          "name": "exp",
          "status": "live",
          "rule": "iat + 300."
        },
        {
          "name": "jti",
          "status": "live",
          "rule": "Unique id; optionally reject a repeat until it expires."
        }
      ]
    },
    "v3": {
      "version": 3,
      "status": "live",
      "alg": "ES256",
      "typ": "bmai-actor+jwt",
      "issuer": "https://busymate.ai",
      "jwksUrl": "https://busymate.ai/.well-known/bmai-actor-jwks.json",
      "keyRotationDays": 90,
      "keyOverlapDays": 14,
      "lifetimeSeconds": 120,
      "clockSkewSeconds": 10,
      "identifiedOnly": true,
      "transport": "X-Bmai-Actor: <token>",
      "dualSignDays": 90,
      "flag": "actor_token_v3",
      "statusDeclaration": {
        "actorVerifier": true,
        "actor": 3
      },
      "claims": [
        {
          "name": "iss",
          "status": "live",
          "rule": "`https://busymate.ai`."
        },
        {
          "name": "aud",
          "status": "live",
          "rule": "The EXACT origin of the connector URL."
        },
        {
          "name": "sub",
          "status": "live",
          "rule": "The verified launch subject."
        },
        {
          "name": "idp",
          "status": "live",
          "rule": "The identity provider id. Map users by (idp, sub), never by sub alone."
        },
        {
          "name": "tenant_id",
          "status": "live",
          "rule": "Your Busymate tenant id."
        },
        {
          "name": "connector_id",
          "status": "live",
          "rule": "Your connector id."
        },
        {
          "name": "support_session_id",
          "status": "live",
          "rule": "The chat session."
        },
        {
          "name": "actor_kind",
          "status": "live",
          "rule": "Always `identified`."
        },
        {
          "name": "confirmed",
          "status": "live",
          "rule": "True only for a call the customer confirmed."
        },
        {
          "name": "tool",
          "status": "live",
          "rule": "The tool this token was minted for."
        },
        {
          "name": "iat",
          "status": "live",
          "rule": "Issued at."
        },
        {
          "name": "nbf",
          "status": "live",
          "rule": "Not before."
        },
        {
          "name": "exp",
          "status": "live",
          "rule": "iat + at most 120."
        },
        {
          "name": "jti",
          "status": "live",
          "rule": "Unique id; a verifier rejects a repeat."
        }
      ]
    },
    "rules": [
      "Check `actor_kind`: an anonymous token is for public data only.",
      "Take the user only from `sub` (v3: from `(idp, sub)`), never from a tool argument such as `accountId`.",
      "Require `confirmed === true` for anything that moves money or writes data.",
      "Optionally reject a repeated `jti` until it expires.",
      "Optionally serve `GET /api/bmai/status` returning `{\"actorVerifier\": true}` so the Console can show \"connected\"."
    ]
  },
  "customerIdentity": [
    {
      "value": "busymate",
      "status": "planned",
      "audience": "Connectors the workspace declares as its own product. Each identified call carries the actor token.",
      "card": "Never an OAuth card. With \"Ask customers\" ON: a one-time consent confirm. If the MCP rejects the token: the customer sees \"temporarily unavailable\" and the admin gets an alert."
    },
    {
      "value": "oauth",
      "status": "planned",
      "audience": "A real third party whose own OAuth is the identity.",
      "card": "\"Authorize {partner}\" once per customer (and after revoke)."
    },
    {
      "value": "none",
      "status": "planned",
      "audience": "Public tools only.",
      "card": "None. Account tools answer \"you can do this at {product}\"."
    }
  ],
  "reasons": [
    {
      "code": "clock_skew",
      "domain": "launch",
      "status": "live",
      "customer": "Your server clock is out of step with ours.",
      "fix": "Sync the server clock (NTP) and mint a fresh token."
    },
    {
      "code": "token_expired",
      "domain": "launch",
      "status": "live",
      "customer": "The token expired before it reached us.",
      "fix": "Mint a new token on every call; never cache one; keep `exp - iat` at 120 s or less."
    },
    {
      "code": "signature_invalid",
      "domain": "launch",
      "status": "live",
      "customer": "The token signature does not match your published keys.",
      "fix": "Sign with the key whose public half is in your JWKS; for ES256 use raw r||s (64 bytes), not DER."
    },
    {
      "code": "unknown_kid",
      "domain": "launch",
      "status": "live",
      "customer": "The token's `kid` is not in your JWKS.",
      "fix": "Publish the signing key in your JWKS before using it; keep the old key until old tokens expire."
    },
    {
      "code": "jwks_unavailable",
      "domain": "launch",
      "status": "live",
      "customer": "We could not fetch your JWKS.",
      "fix": "Make `<issuer>/.well-known/jwks.json` reachable over HTTPS without authentication."
    },
    {
      "code": "claim_invalid",
      "domain": "launch",
      "status": "live",
      "customer": "A required claim is missing or differs from the registration.",
      "fix": "Match `iss`, `aud`, `sub`, `jti`, `iat`, `exp` and the maximum token age to the registered provider."
    },
    {
      "code": "tenant_claim_mismatch",
      "domain": "launch",
      "status": "live",
      "customer": "The token names another workspace.",
      "fix": "Set the tenant claim to the value registered for this workspace."
    },
    {
      "code": "nonce_mismatch",
      "domain": "launch",
      "status": "live",
      "customer": "The token's nonce is not the one the chat asked with.",
      "fix": "Use the nonce the chat passes you, unchanged, and mint a new token every call."
    },
    {
      "code": "malformed_token",
      "domain": "launch",
      "status": "live",
      "customer": "The token is not a parseable JWT.",
      "fix": "Return a compact JWS (three dot-separated parts) with an `iss` claim."
    },
    {
      "code": "provider_not_registered",
      "domain": "launch",
      "status": "live",
      "customer": "No enabled sign-in provider has this issuer.",
      "fix": "Register the provider under Console › Identity with the exact issuer."
    },
    {
      "code": "provider_not_published",
      "domain": "launch",
      "status": "live",
      "customer": "The provider is saved but not in the published settings.",
      "fix": "Publish (Console › Publish, or `publish_tenant_runtime`)."
    },
    {
      "code": "issuer_owned_elsewhere",
      "domain": "launch",
      "status": "live",
      "customer": "This issuer and tenant claim belong to another workspace.",
      "fix": "Use an issuer and tenant claim unique to your workspace, or contact support."
    },
    {
      "code": "verifier_misconfigured",
      "domain": "launch",
      "status": "live",
      "customer": "The provider has no usable signing algorithm.",
      "fix": "Allow ES256 and/or RS256 on the provider."
    },
    {
      "code": "launch_replayed",
      "domain": "launch",
      "status": "live",
      "customer": "This token was already used; tokens are single use.",
      "fix": "Generate a unique `jti` per token and never cache or resend a token."
    },
    {
      "code": "launch_failed",
      "domain": "launch",
      "status": "live",
      "customer": "The verified identity could not be turned into a session.",
      "fix": "Retry; if it persists, check the identity link events in the Console."
    },
    {
      "code": "origin_not_allowed",
      "domain": "launch",
      "status": "live",
      "customer": "This page's origin is not an allowed website of the workspace.",
      "fix": "Add the page origin under Console › Integration › Allowed origins."
    },
    {
      "code": "verifier_error",
      "domain": "launch",
      "status": "live",
      "customer": "Another verification failure.",
      "fix": "Check the identity link events in the Console for the precise internal reason."
    },
    {
      "code": "not_identified",
      "domain": "card",
      "status": "live",
      "customer": "Sign in before this account tool can run.",
      "fix": "Fix launch identity first (steps 1 and 2): the visitor is not identified."
    },
    {
      "code": "third_party",
      "domain": "card",
      "status": "live",
      "customer": "\"Authorize {product} account tools\" is shown while the customer is signed in.",
      "fix": "The connector is Per-customer OAuth and not treated as yours: switch it to Signed actor token and accept the actor token on your MCP."
    },
    {
      "code": "revoked",
      "domain": "card",
      "status": "live",
      "customer": "The customer revoked access and is asked to authorize again.",
      "fix": "Nothing to fix: consent is theirs to renew."
    },
    {
      "code": "ask_users",
      "domain": "card",
      "status": "live",
      "customer": "The workspace asks every customer before using their account.",
      "fix": "Turn \"Ask customers before using their account\" off if you do not want the prompt."
    },
    {
      "code": "grant_reauth",
      "domain": "card",
      "status": "live",
      "customer": "A grant the customer gave has expired and must be renewed.",
      "fix": "Nothing to fix: only the customer can renew it."
    },
    {
      "code": "partner_rejected",
      "domain": "card",
      "status": "live",
      "customer": "The card keeps returning because your server answered 401 to the actor token.",
      "fix": "Check the secret, the exact `aud` origin and the `kid`, and that the verifier is installed."
    },
    {
      "code": "silent",
      "domain": "readiness",
      "status": "live",
      "customer": "Recognised customers reach their account with no second prompt.",
      "fix": "Nothing to fix."
    },
    {
      "code": "asks_policy_on",
      "domain": "readiness",
      "status": "live",
      "customer": "The workspace chose to ask customers before using their account.",
      "fix": "Turn the \"Ask customers\" switch off for a prompt-free experience."
    },
    {
      "code": "asks_third_party",
      "domain": "readiness",
      "status": "live",
      "customer": "The connector is treated as somebody else's product, so the consent card is shown.",
      "fix": "Make the connector yours: accept the actor token on your MCP and use Signed actor token."
    },
    {
      "code": "asks_identity_not_conveyed",
      "domain": "readiness",
      "status": "live",
      "customer": "Account tools are hidden: the connector identity mode is `none`.",
      "fix": "Set the connector to Signed actor token."
    },
    {
      "code": "never_succeeded",
      "domain": "readiness",
      "status": "live",
      "customer": "Customers arrive identified but no call was ever accepted.",
      "fix": "Install the actor-token verifier on your MCP and check the secret and `aud`."
    },
    {
      "code": "stale_evidence",
      "domain": "readiness",
      "status": "live",
      "customer": "A call was accepted once, but not under the current published revision.",
      "fix": "Run a signed-in test call after publishing."
    },
    {
      "code": "unknown",
      "domain": "readiness",
      "status": "live",
      "customer": "Nothing has exercised this path yet.",
      "fix": "Run the Step 5 checks."
    },
    {
      "code": "login_page_unreachable",
      "domain": "login_page",
      "status": "live",
      "customer": "We could not fetch your login page.",
      "fix": "Make `login_url` a public https page that answers within 5 seconds."
    },
    {
      "code": "login_page_shows_ui",
      "domain": "login_page",
      "status": "live",
      "customer": "With `bmai_prompt=none` and no session your login page returned a page.",
      "fix": "It must never show UI: redirect to `return_to#error=login_required`."
    },
    {
      "code": "login_page_wrong_redirect",
      "domain": "login_page",
      "status": "live",
      "customer": "With `bmai_prompt=none` and no session your login page redirected somewhere other than `return_to#error=login_required`.",
      "fix": "Redirect to exactly the `return_to` you were given, with `#error=login_required`."
    },
    {
      "code": "login_page_frame_blocked_xfo",
      "domain": "login_page",
      "status": "live",
      "customer": "Your login page sends `X-Frame-Options`, so the silent check cannot load it in the hidden frame.",
      "fix": "Remove `X-Frame-Options` and use `Content-Security-Policy: frame-ancestors` with the chat origins."
    },
    {
      "code": "login_page_frame_ancestors_missing",
      "domain": "login_page",
      "status": "live",
      "customer": "Your login page's `frame-ancestors` does not list every chat origin.",
      "fix": "Add the chat origins shown by the probe to `frame-ancestors`."
    },
    {
      "code": "login_page_cookie_not_samesite_none",
      "domain": "login_page",
      "status": "live",
      "customer": "Your login page sets a cookie without `SameSite=None; Secure`, so the browser will not send the session in the hidden frame.",
      "fix": "Set the session cookie with `SameSite=None; Secure`."
    },
    {
      "code": "delegation_unconfigured",
      "domain": "connector",
      "status": "live",
      "customer": "Signed connector access is temporarily unavailable.",
      "fix": "Configure the connector's customer identity mode and its actor verifier."
    },
    {
      "code": "credential_missing",
      "domain": "connector",
      "status": "live",
      "customer": "A sign-in-required connector has no stored credential on this channel.",
      "fix": "Channels such as WhatsApp or Telegram cannot carry your customer's identity yet."
    }
  ]
}
