{
  "schema_version": "1.0",
  "baseline": "A-feature-flags-batch-4-readiness",
  "phase": "2b+ → 2c readiness",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "purpose_th": "ตาราง consolidated สำหรับ verification mode · แต่ละแถวคือ input condition และให้ผลชัดว่าควร allow/deny · warnings/auth_source · อ้างอิงได้ในเวลา review + test",
  "purpose_en": "Consolidated decision matrix for FF Service auth handling. Each row describes one input condition and prescribes allow/deny + auth_source + warnings. Source of truth for both the runtime implementation (once wired) and the parity harness.",
  "honest_note": "This matrix is READ-ONLY in 2b+ runtime. app.py does not consult it today. It is the target spec for Phase 2c wiring. Any row marked 'deny' MUST remain deny even under dev_fallback — do not relax on grounds of convenience.",
  "priority_rules": {
    "order": [
      "1. If Bearer present · try verified mode first (when FF_JWKS_URL set and JWKS cached)",
      "2. If verified fails with jwks_unreachable OR kid_not_found → fall through to decode-only (if FF_ALLOW_DEV_HEADERS=true) else deny",
      "3. If verified fails with signature_invalid / algorithm_mismatch / token_expired / token_audience_mismatch / token_issuer_mismatch → DENY (never fall through)",
      "4. If no Bearer → try dev-fallback (headers / body / query) when FF_ALLOW_DEV_HEADERS=true · else anonymous",
      "5. Anonymous always available · but downstream policy may still deny based on role/scope"
    ],
    "never_fall_through_on": [
      "signature_invalid",
      "algorithm_mismatch",
      "token_expired",
      "token_audience_mismatch",
      "token_issuer_mismatch"
    ]
  },
  "matrix": [
    {
      "row": "M-01",
      "condition": "Bearer present · JWKS reachable · kid found · alg=RS256 · signature verifies · iss/aud match · exp future",
      "mode": "verified",
      "result": { "allow": true, "auth_source": "jwt", "warnings": [], "verified": true }
    },
    {
      "row": "M-02",
      "condition": "Bearer present · JWKS unreachable on startup · FF_ALLOW_DEV_HEADERS=true",
      "mode": "decode-only",
      "result": { "allow": true, "auth_source": "jwt_unverified", "warnings": ["auth_not_verified", "jwks_unreachable"], "verified": false }
    },
    {
      "row": "M-03",
      "condition": "Bearer present · JWKS reachable · kid miss · refresh fails · FF_ALLOW_DEV_HEADERS=true",
      "mode": "decode-only",
      "result": { "allow": true, "auth_source": "jwt_unverified", "warnings": ["auth_not_verified", "kid_not_found"], "verified": false }
    },
    {
      "row": "M-04",
      "condition": "Bearer present · signature_invalid · ANY env",
      "mode": "deny",
      "result": { "allow": false, "code": "signature_invalid", "http": 401 }
    },
    {
      "row": "M-05",
      "condition": "Bearer present · alg=HS256 with public JWKS · ANY env",
      "mode": "deny",
      "result": { "allow": false, "code": "algorithm_mismatch", "http": 401 }
    },
    {
      "row": "M-06",
      "condition": "Bearer present · alg=none · ANY env",
      "mode": "deny",
      "result": { "allow": false, "code": "algorithm_mismatch", "http": 401 }
    },
    {
      "row": "M-07",
      "condition": "Bearer present · token_expired · ANY env",
      "mode": "deny",
      "result": { "allow": false, "code": "token_expired", "http": 401 }
    },
    {
      "row": "M-08",
      "condition": "Bearer present · aud wrong · ANY env",
      "mode": "deny",
      "result": { "allow": false, "code": "token_audience_mismatch", "http": 401 }
    },
    {
      "row": "M-09",
      "condition": "Bearer present · iss wrong · ANY env",
      "mode": "deny",
      "result": { "allow": false, "code": "token_issuer_mismatch", "http": 401 }
    },
    {
      "row": "M-10",
      "condition": "Bearer present · JWKS unreachable · FF_ALLOW_DEV_HEADERS=false",
      "mode": "deny",
      "result": { "allow": false, "code": "dev_mode_rejected", "http": 401 }
    },
    {
      "row": "M-11",
      "condition": "No Bearer · X-PTT-User-Id header present · FF_ALLOW_DEV_HEADERS=true",
      "mode": "dev-fallback",
      "result": { "allow": true, "auth_source": "dev_headers", "warnings": ["dev_mode"], "verified": false }
    },
    {
      "row": "M-12",
      "condition": "No Bearer · body has {user,tenant,roles} on POST · FF_ALLOW_DEV_HEADERS=true",
      "mode": "dev-fallback",
      "result": { "allow": true, "auth_source": "body", "warnings": ["dev_mode"], "verified": false }
    },
    {
      "row": "M-13",
      "condition": "No Bearer · query ?user=&tenant= on GET · FF_ALLOW_DEV_HEADERS=true",
      "mode": "dev-fallback",
      "result": { "allow": true, "auth_source": "query", "warnings": ["dev_mode"], "verified": false }
    },
    {
      "row": "M-14",
      "condition": "No Bearer · mixed dev headers + body · FF_ALLOW_DEV_HEADERS=true",
      "mode": "dev-fallback",
      "result": { "allow": true, "auth_source": "mixed", "warnings": ["dev_mode", "auth_source_mixed"], "verified": false }
    },
    {
      "row": "M-15",
      "condition": "No Bearer · dev headers present · FF_ALLOW_DEV_HEADERS=false",
      "mode": "deny",
      "result": { "allow": false, "code": "dev_mode_rejected", "http": 401 }
    },
    {
      "row": "M-16",
      "condition": "No Bearer · no dev inputs · defaults",
      "mode": "anonymous",
      "result": { "allow": true, "auth_source": "none", "warnings": [], "verified": false }
    },
    {
      "row": "M-17",
      "condition": "Bearer present · decode succeeds but sub missing",
      "mode": "anonymous",
      "result": { "allow": true, "auth_source": "none", "warnings": ["missing_claim:sub", "auth_not_verified"], "verified": false }
    }
  ],
  "sensitive_overlay": {
    "when_applied": "when flag.registry entry has sensitive_flag=true OR requires_approval=true",
    "additional_requirement": "auth_source MUST be 'jwt' (verified) · any unverified path forces deny with reason='auth_not_verified' regardless of base matrix row",
    "verified_mode_required_for_sensitive": true,
    "current_state": "rule declared · enforcement deferred to 2c (admin cross-service call not wired)"
  },
  "implementation_notes": {
    "current_app_py_behaviour": "app.py in 2b+ reads X-PTT-* headers and query only · no Bearer parsing · no JWKS · this matrix describes the TARGET after 2c wiring",
    "where_this_matrix_lives_in_code_target": [
      "app.py · new _resolve_auth_mode(request) helper",
      "app.py · _merge_ctx() extended to consult matrix",
      "new auth.py module to host JWKS fetch + verify (target · not shipped)"
    ],
    "testing": "parity_test_matrix.json references these M-* rows 1:1 · each row is a mandatory harness input once verified mode is wired"
  },
  "pair_with": {
    "jwks_notes": "docs/runtime/feature-flags-service/jwks_integration_notes.json",
    "jwt_examples": "docs/runtime/feature-flags-service/jwt_claim_examples.json",
    "jwt_notes_2b_plus": "docs/runtime/feature-flags-service/jwt_verification_notes.json",
    "parity_matrix": "docs/runtime/feature-flags-service/parity_test_matrix.json",
    "cutover_checklist": "docs/runtime/feature-flags-service/cutover_readiness_checklist.json"
  }
}
