{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "policy",
  "title": "OIDC Policy Contract",
  "description": "The OidcPolicy rule type, its matching semantics, validation, and the match/explain functions. No dependencies — the foundation every other oidc-ui item builds on.",
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/oidc/lib/policy.ts",
      "content": "/**\n * The rule contract for trusted OIDC callers. Matching semantics:\n *\n * - `issuer` equals the token's `iss` exactly.\n * - Every listed claim must be present with a value in the accepted list;\n *   exact string equality, arrays match on intersection.\n * - Unlisted claims are not checked. A policy with no claim rules never\n *   matches, and validation requires an `aud` rule.\n * - Accepted values must be arrays of plain strings; any other shape fails\n *   closed.\n */\ntype OidcPolicy = {\n  /** OIDC discovery root. https only, no query or fragment. */\n  issuer: string\n  claims: Record<string, string[]>\n  label?: string\n}\n\n/** The gate for anything headed to storage; the verifier refuses failures. */\nfunction validateOidcPolicy(policy: OidcPolicy) {\n  const errors: { path: string; message: string }[] = []\n\n  if (!policy.issuer) {\n    errors.push({ path: \"issuer\", message: \"Issuer is required.\" })\n  } else {\n    let url: URL | null = null\n    try {\n      url = new URL(policy.issuer)\n    } catch {\n      url = null\n    }\n    if (!url) {\n      errors.push({ path: \"issuer\", message: \"Issuer must be a valid URL.\" })\n    } else {\n      if (url.protocol !== \"https:\") {\n        errors.push({ path: \"issuer\", message: \"Issuer must use https.\" })\n      }\n      if (url.search || url.hash) {\n        errors.push({\n          path: \"issuer\",\n          message: \"Issuer must not contain a query or fragment.\",\n        })\n      }\n    }\n  }\n\n  const claims = policy.claims ?? {}\n  if (!Object.keys(claims).includes(\"aud\")) {\n    errors.push({\n      path: \"claims.aud\",\n      message: \"An audience (aud) claim is required.\",\n    })\n  }\n  for (const [name, accepted] of Object.entries(claims)) {\n    if (!name.trim()) {\n      errors.push({ path: \"claims\", message: \"Claim names cannot be empty.\" })\n      continue\n    }\n    // Prototype-chain names silently vanish from plain-object claim maps.\n    if ([\"__proto__\", \"constructor\", \"prototype\"].includes(name)) {\n      errors.push({\n        path: `claims.${name}`,\n        message: `Claim name \"${name}\" is not allowed.`,\n      })\n      continue\n    }\n    if (!Array.isArray(accepted) || accepted.length === 0) {\n      errors.push({\n        path: `claims.${name}`,\n        message: `Claim \"${name}\" must accept at least one value.`,\n      })\n      continue\n    }\n    if (accepted.some((value) => typeof value !== \"string\" || value === \"\")) {\n      errors.push({\n        path: `claims.${name}`,\n        message: `Claim \"${name}\" has an empty or non-string accepted value.`,\n      })\n    }\n  }\n\n  return errors\n}\n\n/** The matching source of truth. `claims` must be a verified payload. */\nfunction explainOidcPolicy(\n  policy: OidcPolicy,\n  claims: Record<string, unknown>\n) {\n  const checks: {\n    claim: string\n    accepted: string[]\n    actual: unknown\n    ok: boolean\n    reason?: \"missing\" | \"mismatch\" | \"invalid-rule\"\n  }[] = []\n\n  checks.push({\n    claim: \"iss\",\n    accepted: [policy.issuer],\n    actual: claims.iss,\n    ok: claims.iss === policy.issuer,\n    reason:\n      claims.iss === policy.issuer\n        ? undefined\n        : claims.iss === undefined\n          ? \"missing\"\n          : \"mismatch\",\n  })\n\n  const rules = Object.entries(policy.claims ?? {})\n  if (rules.length === 0) {\n    checks.push({\n      claim: \"aud\",\n      accepted: [],\n      actual: claims.aud,\n      ok: false,\n      reason: \"invalid-rule\",\n    })\n  }\n\n  for (const [name, accepted] of rules) {\n    if (\n      !Array.isArray(accepted) ||\n      accepted.some((value) => typeof value !== \"string\")\n    ) {\n      checks.push({\n        claim: name,\n        accepted: [],\n        actual: claims[name],\n        ok: false,\n        reason: \"invalid-rule\",\n      })\n      continue\n    }\n    const actual = claims[name]\n    if (actual === undefined || actual === null) {\n      checks.push({\n        claim: name,\n        accepted,\n        actual,\n        ok: false,\n        reason: \"missing\",\n      })\n      continue\n    }\n    const values = Array.isArray(actual) ? actual : [actual]\n    const ok = values.some(\n      (value) => typeof value === \"string\" && accepted.includes(value)\n    )\n    checks.push({\n      claim: name,\n      accepted,\n      actual,\n      ok,\n      reason: ok ? undefined : \"mismatch\",\n    })\n  }\n\n  return { ok: checks.every((check) => check.ok), checks }\n}\n\nfunction matchOidcPolicy(policy: OidcPolicy, claims: Record<string, unknown>) {\n  return explainOidcPolicy(policy, claims).ok\n}\n\nfunction matchOidcPolicies(\n  policies: OidcPolicy[],\n  claims: Record<string, unknown>\n) {\n  return policies.find((policy) => matchOidcPolicy(policy, claims)) ?? null\n}\n\nexport {\n  validateOidcPolicy,\n  explainOidcPolicy,\n  matchOidcPolicy,\n  matchOidcPolicies,\n}\nexport type { OidcPolicy }\n",
      "type": "registry:lib",
      "target": "@lib/oidc/policy.ts"
    }
  ],
  "categories": [
    "auth"
  ],
  "type": "registry:lib"
}