{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-github-rhf",
  "title": "GitHub Actions Provider (React Hook Form)",
  "description": "A self-contained form component that emits an OidcPolicy for a GitHub Actions caller: a repository (owner/repo or a pasted URL) and branch/tag/environment scoping with derived audience. Built with React Hook Form.",
  "dependencies": [
    "react-hook-form"
  ],
  "registryDependencies": [
    "https://ui.shadcn.com/oidc/r/policy.json",
    "https://ui.shadcn.com/oidc/r/claims.json",
    "button",
    "field",
    "input",
    "select"
  ],
  "files": [
    {
      "path": "registry/oidc/flavors/rhf-none/provider-github.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Controller, useForm, useWatch } from \"react-hook-form\"\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 {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\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 GithubIcon({ ...props }: React.ComponentProps<\"svg\">) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" {...props}>\n      <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n    </svg>\n  )\n}\n\ntype Scope = \"any\" | \"branch\" | \"tag\" | \"environment\"\n\ntype Fields = {\n  repository: string\n  scope: Scope\n  scopeValue: string\n  extraClaims: ClaimRow[]\n}\n\nconst SCOPE_OPTIONS: { label: string; value: Scope }[] = [\n  { label: \"Any branch or tag\", value: \"any\" },\n  { label: \"Branch\", value: \"branch\" },\n  { label: \"Tag\", value: \"tag\" },\n  { label: \"Environment\", value: \"environment\" },\n]\n\nconst DEFAULTS: Fields = {\n  repository: \"\",\n  scope: \"any\",\n  scopeValue: \"\",\n  extraClaims: [],\n}\n\nconst FIRST_CLASS_CLAIMS = [\"aud\", \"repository\", \"ref\", \"environment\"]\n\nconst ISSUER = \"https://token.actions.githubusercontent.com\"\n\n// Paths under these are GitHub product pages, never repositories.\nconst RESERVED_OWNERS = [\n  \"orgs\",\n  \"features\",\n  \"topics\",\n  \"collections\",\n  \"marketplace\",\n  \"sponsors\",\n  \"apps\",\n  \"settings\",\n  \"explore\",\n  \"enterprise\",\n]\n\n// Accepts owner/repo, a pasted repository URL, or an SSH remote.\nfunction parseRepository(input: string) {\n  const match = input\n    .trim()\n    .match(\n      /^(?:(?:https?:\\/\\/)?github\\.com[/:]|git@github\\.com:)?([A-Za-z0-9-]+)\\/([\\w.-]+?)(?:\\.git)?\\/?$/\n    )\n  if (!match || RESERVED_OWNERS.includes(match[1])) {\n    return null\n  }\n  return { owner: match[1], repo: match[2] }\n}\n\nfunction decodeSegment(segment: string) {\n  try {\n    return decodeURIComponent(segment)\n  } catch {\n    return null\n  }\n}\n\n// Turns any pasted GitHub link into a field patch: repo pages, tree and\n// release URLs (which also set the scope), clone URLs, or plain owner/repo.\nfunction parsePastedRepository(text: string) {\n  const url = text\n    .trim()\n    .match(\n      /^(?:https?:\\/\\/)?(?:www\\.)?github\\.com\\/([A-Za-z0-9-]+)\\/([\\w.-]+?)(?:\\.git)?(?:\\/([^?#]*))?(?:[?#].*)?$/\n    )\n  if (!url) {\n    const repository = parseRepository(text)\n    return repository\n      ? { repository: `${repository.owner}/${repository.repo}` }\n      : null\n  }\n  const [, owner, repo, rawPath = \"\"] = url\n  if (RESERVED_OWNERS.includes(owner)) {\n    return null\n  }\n  const repository = `${owner}/${repo}`\n  const path = rawPath.replace(/\\/$/, \"\")\n  const tag = path.match(/^releases\\/tag\\/(.+)$/)\n  if (tag) {\n    const scopeValue = decodeSegment(tag[1])\n    if (scopeValue !== null) {\n      return { repository, scope: \"tag\" as const, scopeValue }\n    }\n  }\n  const branch = path.match(/^tree\\/(.+)$/)\n  if (branch) {\n    const scopeValue = decodeSegment(branch[1])\n    if (scopeValue !== null) {\n      return { repository, scope: \"branch\" as const, scopeValue }\n    }\n  }\n  return { repository }\n}\n\nfunction compile(fields: Fields) {\n  const claims: Record<string, string[]> = {}\n  const repository = parseRepository(fields.repository)\n  if (repository) {\n    claims.aud = [`https://github.com/${repository.owner}`]\n    claims.repository = [`${repository.owner}/${repository.repo}`]\n  }\n  if (fields.scopeValue) {\n    if (fields.scope === \"branch\") {\n      claims.ref = [`refs/heads/${fields.scopeValue}`]\n    } else if (fields.scope === \"tag\") {\n      claims.ref = [`refs/tags/${fields.scopeValue}`]\n    } else if (fields.scope === \"environment\") {\n      claims.environment = [fields.scopeValue]\n    }\n  }\n  mergeClaimRows(claims, fields.extraClaims, FIRST_CLASS_CLAIMS)\n  return { issuer: ISSUER, claims }\n}\n\nfunction parse(policy: OidcPolicy) {\n  if (policy.issuer !== ISSUER) {\n    return null\n  }\n  if (\n    FIRST_CLASS_CLAIMS.some((name) => (policy.claims[name]?.length ?? 0) > 1)\n  ) {\n    return null\n  }\n  const extraClaims = claimRowsFromPolicy(policy.claims, FIRST_CLASS_CLAIMS)\n  if (extraClaims === null) {\n    return null\n  }\n\n  const [repository = \"\"] = policy.claims.repository ?? []\n  const parsed = parseRepository(repository)\n  if (!parsed || `${parsed.owner}/${parsed.repo}` !== repository) {\n    return null\n  }\n\n  const [aud] = policy.claims.aud ?? []\n  if (aud !== `https://github.com/${parsed.owner}`) {\n    return null\n  }\n\n  const [ref] = policy.claims.ref ?? []\n  const [environment] = policy.claims.environment ?? []\n  if (ref && environment) {\n    return null\n  }\n\n  let scope: Scope = \"any\"\n  let scopeValue = \"\"\n  if (ref?.startsWith(\"refs/heads/\")) {\n    scope = \"branch\"\n    scopeValue = ref.slice(\"refs/heads/\".length)\n  } else if (ref?.startsWith(\"refs/tags/\")) {\n    scope = \"tag\"\n    scopeValue = ref.slice(\"refs/tags/\".length)\n  } else if (ref) {\n    return null\n  } else if (environment) {\n    scope = \"environment\"\n    scopeValue = environment\n  }\n  if (ref && !scopeValue) {\n    return null\n  }\n\n  return { repository, scope, scopeValue, extraClaims }\n}\n\n/** True when this form can represent the policy — for routing an edit. */\nfunction matchesGithubPolicy(policy: OidcPolicy) {\n  return parse(policy) !== null\n}\n\nfunction GithubPolicyForm({\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 form = useForm<Fields>({\n    mode: \"onChange\",\n    defaultValues: defaultValue ? (parse(defaultValue) ?? DEFAULTS) : DEFAULTS,\n  })\n  const watched = {\n    repository: useWatch({ control: form.control, name: \"repository\" }),\n    scope: useWatch({ control: form.control, name: \"scope\" }),\n  }\n\n  const onChangeRef = React.useRef(onChange)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n  }, [onChange])\n\n  React.useEffect(() => {\n    return form.subscribe({\n      formState: { values: true },\n      callback: ({ values }) => {\n        const policy = compile(values)\n        onChangeRef.current?.(\n          defaultValue?.label\n            ? { ...policy, label: defaultValue.label }\n            : policy\n        )\n      },\n    })\n  }, [form, defaultValue?.label])\n\n  return (\n    <form\n      data-slot=\"github-policy-form\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      onSubmit={form.handleSubmit((fields) => {\n        if (!(\n          claimsListValid(fields.extraClaims, FIRST_CLASS_CLAIMS) &&\n          parseRepository(fields.repository) !== null\n        )) {\n          return\n        }\n        const policy = compile(fields)\n        onSubmit(\n          defaultValue?.label\n            ? { ...policy, label: defaultValue.label }\n            : policy\n        )\n      })}\n    >\n      <Field>\n        <FieldLabel htmlFor=\"gh-issuer\" className=\"w-full\">\n          Issuer <ClaimChip>iss</ClaimChip>\n        </FieldLabel>\n        <Input id=\"gh-issuer\" value={ISSUER} readOnly disabled />\n        <FieldDescription>Fixed for GitHub Actions.</FieldDescription>\n      </Field>\n      <Controller\n        control={form.control}\n        name=\"repository\"\n        rules={{ required: true }}\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"gh-repository\" className=\"w-full\">\n              Repository <ClaimChip>aud · repository</ClaimChip>\n            </FieldLabel>\n            <Input\n              {...field}\n              id=\"gh-repository\"\n              placeholder=\"acme/web\"\n              onChange={(event) => field.onChange(event.target.value.trim())}\n              onPaste={(event) => {\n                const patch = parsePastedRepository(\n                  event.clipboardData.getData(\"text\")\n                )\n                if (!patch) {\n                  return\n                }\n                event.preventDefault()\n                form.reset(\n                  { ...form.getValues(), ...patch },\n                  { keepDefaultValues: true }\n                )\n              }}\n            />\n            <FieldDescription>\n              owner/repo — or paste any GitHub link to fill the form.\n            </FieldDescription>\n          </Field>\n        )}\n      />\n      <Controller\n        control={form.control}\n        name=\"scope\"\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"gh-scope\">Scope by</FieldLabel>\n            <Select\n              items={SCOPE_OPTIONS}\n              value={field.value}\n              onValueChange={(next) => {\n                field.onChange((next ?? DEFAULTS.scope) as Scope)\n                form.setValue(\"scopeValue\", \"\")\n              }}\n            >\n              <SelectTrigger id=\"gh-scope\" className=\"w-full\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectGroup>\n                  {SCOPE_OPTIONS.map((option) => (\n                    <SelectItem key={option.value} value={option.value}>\n                      {option.label}\n                    </SelectItem>\n                  ))}\n                </SelectGroup>\n              </SelectContent>\n            </Select>\n          </Field>\n        )}\n      />\n      {watched.scope !== \"any\" ? (\n        <Controller\n          control={form.control}\n          name=\"scopeValue\"\n          render={({ field }) => (\n            <Field>\n              <FieldLabel htmlFor=\"gh-scopeValue\" className=\"w-full\">\n                {watched.scope === \"branch\"\n                  ? \"Branch name\"\n                  : watched.scope === \"tag\"\n                    ? \"Tag name\"\n                    : \"Environment name\"}{\" \"}\n                <ClaimChip>\n                  {watched.scope === \"environment\" ? \"environment\" : \"ref\"}\n                </ClaimChip>\n              </FieldLabel>\n              <Input\n                {...field}\n                id=\"gh-scopeValue\"\n                placeholder={\n                  watched.scope === \"environment\" ? \"production\" : \"main\"\n                }\n              />\n            </Field>\n          )}\n        />\n      ) : null}\n      {parseRepository(watched.repository)?.owner ? (\n        <FieldDescription>\n          Audience derived automatically:{\" \"}\n          <span className=\"font-mono\">\n            https://github.com/{parseRepository(watched.repository)?.owner}\n          </span>\n        </FieldDescription>\n      ) : null}\n      <Controller\n        control={form.control}\n        name=\"extraClaims\"\n        render={({ field: controller }) => (\n          <ClaimsList\n            value={controller.value}\n            onChange={controller.onChange}\n            reserved={FIRST_CLASS_CLAIMS}\n          />\n        )}\n      />\n      <Button type=\"submit\" className=\"self-start\">\n        Save\n      </Button>\n    </form>\n  )\n}\n\nexport { GithubIcon, GithubPolicyForm, matchesGithubPolicy }\n",
      "type": "registry:component",
      "target": "@components/oidc/provider-github.tsx"
    }
  ],
  "meta": {
    "tagline": "Trust workflows from a repository"
  },
  "categories": [
    "auth",
    "forms"
  ],
  "type": "registry:component"
}