{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-github",
  "title": "GitHub Actions Provider",
  "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.",
  "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/components/provider-github.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 {\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 [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.repository) &&\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 ? { ...policy, label: defaultValue.label } : policy\n    )\n  }\n\n  return (\n    <form\n      data-slot=\"github-policy-form\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      onSubmit={handleSubmit}\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      <Field>\n        <FieldLabel htmlFor=\"gh-repository\" className=\"w-full\">\n          Repository <ClaimChip>aud · repository</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"gh-repository\"\n          value={fields.repository}\n          placeholder=\"acme/web\"\n          onChange={(event) =>\n            apply({ ...fields, repository: event.target.value.trim() })\n          }\n          onPaste={(event) => {\n            const patch = parsePastedRepository(\n              event.clipboardData.getData(\"text\")\n            )\n            if (!patch) {\n              return\n            }\n            event.preventDefault()\n            apply({ ...fields, ...patch })\n          }}\n        />\n        <FieldDescription>\n          owner/repo — or paste any GitHub link to fill the form.\n        </FieldDescription>\n      </Field>\n      <Field>\n        <FieldLabel htmlFor=\"gh-scope\">Scope by</FieldLabel>\n        <Select\n          items={SCOPE_OPTIONS}\n          value={fields.scope}\n          onValueChange={(next) => {\n            const next2 = {\n              ...fields,\n              scope: (next ?? DEFAULTS.scope) as Scope,\n            }\n            next2.scopeValue = \"\"\n            apply(next2)\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      {fields.scope !== \"any\" ? (\n        <Field>\n          <FieldLabel htmlFor=\"gh-scopeValue\" className=\"w-full\">\n            {fields.scope === \"branch\"\n              ? \"Branch name\"\n              : fields.scope === \"tag\"\n                ? \"Tag name\"\n                : \"Environment name\"}{\" \"}\n            <ClaimChip>\n              {fields.scope === \"environment\" ? \"environment\" : \"ref\"}\n            </ClaimChip>\n          </FieldLabel>\n          <Input\n            id=\"gh-scopeValue\"\n            value={fields.scopeValue}\n            placeholder={fields.scope === \"environment\" ? \"production\" : \"main\"}\n            onChange={(event) =>\n              apply({ ...fields, scopeValue: event.target.value })\n            }\n          />\n        </Field>\n      ) : null}\n      {parseRepository(fields.repository)?.owner ? (\n        <FieldDescription>\n          Audience derived automatically:{\" \"}\n          <span className=\"font-mono\">\n            https://github.com/{parseRepository(fields.repository)?.owner}\n          </span>\n        </FieldDescription>\n      ) : null}\n      <ClaimsList\n        value={fields.extraClaims}\n        onChange={(rows) => apply({ ...fields, extraClaims: rows })}\n        reserved={FIRST_CLASS_CLAIMS}\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"
}