{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-custom",
  "title": "Custom OIDC Provider",
  "description": "The escape hatch for any OIDC issuer without a dedicated provider: issuer, audience, optional subject, and free-form optional claims, with a warning when a rule would trust an entire issuer. List it last in the providers array — it matches broadly.",
  "registryDependencies": [
    "https://ui.shadcn.com/oidc/r/policy.json",
    "https://ui.shadcn.com/oidc/r/claims.json",
    "button",
    "field",
    "input",
    "alert"
  ],
  "files": [
    {
      "path": "registry/oidc/components/provider-custom.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldDescription, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Alert, AlertTitle } from \"@/components/ui/alert\"\nimport {\n  ClaimChip,\n  ClaimsList,\n  claimRowsFromPolicy,\n  claimsListValid,\n  mergeClaimRows,\n  type ClaimRow,\n} from \"@/components/oidc/claims\"\nimport type { OidcPolicy } from \"@/lib/oidc/policy\"\n\nfunction CustomOidcIcon({ ...props }: React.ComponentProps<\"svg\">) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" {...props}>\n      <path d=\"M14.54.889l-3.63 1.773v18.17c-4.15-.52-7.27-2.78-7.27-5.5 0-2.58 2.8-4.75 6.63-5.41v-2.31C4.42 8.322 0 11.502 0 15.332c0 3.96 4.74 7.24 10.91 7.78l3.63-1.71V.888m.64 6.724v2.31c1.43.25 2.71.7 3.76 1.31l-1.97 1.11 7.03 1.53-.5-5.21-1.87 1.06c-1.74-1.06-3.96-1.81-6.45-2.11z\" />\n    </svg>\n  )\n}\n\ntype Fields = {\n  issuer: string\n  audience: string\n  subject: string\n  extraClaims: ClaimRow[]\n}\n\nconst DEFAULTS: Fields = {\n  issuer: \"\",\n  audience: \"\",\n  subject: \"\",\n  extraClaims: [],\n}\n\nconst FIRST_CLASS_CLAIMS = [\"aud\", \"sub\"]\n\nfunction parsePastedIssuer(text: string) {\n  const match = text\n    .trim()\n    .match(/^(https:\\/\\/[^?#\\s]+?)\\/\\.well-known\\/openid-configuration\\/?$/)\n  return match ? { issuer: match[1] } : null\n}\n\n// The escape hatch: any OIDC issuer. Matches broadly, so list it LAST in the\n// providers array — edit-routing tries providers in order.\nfunction compile(fields: Fields) {\n  const claims: Record<string, string[]> = {}\n  if (fields.audience) {\n    claims.aud = [fields.audience]\n  }\n  if (fields.subject) {\n    claims.sub = [fields.subject]\n  }\n  mergeClaimRows(claims, fields.extraClaims, FIRST_CLASS_CLAIMS)\n  return { issuer: fields.issuer, claims }\n}\n\nfunction parse(policy: OidcPolicy) {\n  if (!policy.issuer.startsWith(\"https://\")) {\n    return null\n  }\n  if ((policy.claims.aud?.length ?? 0) > 1) {\n    return null\n  }\n  if ((policy.claims.sub?.length ?? 0) > 1) {\n    return null\n  }\n\n  const extraClaims = claimRowsFromPolicy(policy.claims, FIRST_CLASS_CLAIMS)\n  if (extraClaims === null) {\n    return null\n  }\n\n  return {\n    issuer: policy.issuer,\n    audience: policy.claims.aud?.[0] ?? \"\",\n    subject: policy.claims.sub?.[0] ?? \"\",\n    extraClaims,\n  }\n}\n\n/** True when this form can represent the policy — for routing an edit. */\nfunction matchesCustomPolicy(policy: OidcPolicy) {\n  return parse(policy) !== null\n}\n\nfunction CustomPolicyForm({\n  defaultValue,\n  onChange,\n  onSubmit,\n  className,\n}: {\n  defaultValue?: OidcPolicy\n  onChange?: (policy: OidcPolicy) => void\n  onSubmit: (policy: OidcPolicy) => void\n  className?: string\n}) {\n  const [fields, setFields] = React.useState<Fields>(() =>\n    defaultValue ? (parse(defaultValue) ?? DEFAULTS) : DEFAULTS\n  )\n\n  const apply = (next: Fields) => {\n    setFields(next)\n    const policy = compile(next)\n    onChange?.(\n      defaultValue?.label ? { ...policy, label: defaultValue.label } : policy\n    )\n  }\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    if (!(\n      Boolean(fields.issuer && fields.audience) &&\n      claimsListValid(fields.extraClaims, FIRST_CLASS_CLAIMS)\n    )) {\n      return\n    }\n    const policy = compile(fields)\n    onSubmit(\n      defaultValue?.label ? { ...policy, label: defaultValue.label } : policy\n    )\n  }\n\n  return (\n    <form\n      data-slot=\"custom-policy-form\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      onSubmit={handleSubmit}\n    >\n      <Field>\n        <FieldLabel htmlFor=\"cu-issuer\" className=\"w-full\">\n          Issuer <ClaimChip>iss</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"cu-issuer\"\n          value={fields.issuer}\n          placeholder=\"https://issuer.example.com\"\n          className=\"font-mono\"\n          onChange={(event) =>\n            apply({ ...fields, issuer: event.target.value.trim() })\n          }\n          onPaste={(event) => {\n            const patch = parsePastedIssuer(event.clipboardData.getData(\"text\"))\n            if (!patch) {\n              return\n            }\n            event.preventDefault()\n            apply({ ...fields, ...patch })\n          }}\n        />\n        <FieldDescription>\n          The issuer&apos;s OIDC discovery root — pasting a discovery URL works\n          too.\n        </FieldDescription>\n      </Field>\n      <Field>\n        <FieldLabel htmlFor=\"cu-audience\" className=\"w-full\">\n          Audience <ClaimChip>aud</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"cu-audience\"\n          value={fields.audience}\n          placeholder=\"https://your-app.example.com\"\n          onChange={(event) =>\n            apply({ ...fields, audience: event.target.value.trim() })\n          }\n        />\n        <FieldDescription>\n          The audience the caller requests its token for. Pick a value unique to\n          your app.\n        </FieldDescription>\n      </Field>\n      <Field>\n        <FieldLabel htmlFor=\"cu-subject\" className=\"w-full\">\n          Subject <ClaimChip>sub</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"cu-subject\"\n          value={fields.subject}\n          placeholder=\"system:serviceaccount:default:deploy-bot\"\n          className=\"font-mono\"\n          onChange={(event) =>\n            apply({ ...fields, subject: event.target.value.trim() })\n          }\n        />\n        <FieldDescription>\n          Optional: the issuer&apos;s primary identifier for the caller.\n        </FieldDescription>\n      </Field>\n      <ClaimsList\n        value={fields.extraClaims}\n        onChange={(rows) => apply({ ...fields, extraClaims: rows })}\n        reserved={FIRST_CLASS_CLAIMS}\n      />\n      {fields.issuer && fields.audience && !fields.subject ? (\n        <Alert>\n          <AlertTitle>\n            Without a subject, every workload this issuer signs tokens for is\n            trusted.\n          </AlertTitle>\n        </Alert>\n      ) : null}\n      <Button type=\"submit\" className=\"self-start\">\n        Save\n      </Button>\n    </form>\n  )\n}\n\nexport { CustomOidcIcon, CustomPolicyForm, matchesCustomPolicy }\n",
      "type": "registry:component",
      "target": "@components/oidc/provider-custom.tsx"
    }
  ],
  "meta": {
    "tagline": "Any OIDC issuer"
  },
  "categories": [
    "auth",
    "forms"
  ],
  "type": "registry:component"
}