{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-github-tanstack-zod",
  "title": "GitHub Actions Provider (TanStack Form + Zod)",
  "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 TanStack Form and validated with Zod.",
  "dependencies": [
    "@tanstack/react-form",
    "zod"
  ],
  "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/tanstack-zod/provider-github.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useForm, useStore } from \"@tanstack/react-form\"\nimport { z } from \"zod\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n} 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\nconst repositorySchema = z\n  .string()\n  .min(1, \"Enter the repository.\")\n  .regex(\n    /^(?:(?:https?:\\/\\/)?github\\.com[\\/:]|git@github\\.com:)?[A-Za-z0-9-]+\\/[\\w.-]+?(?:\\.git)?\\/?$/,\n    \"Use owner/repo or paste the repository URL.\"\n  )\nconst scopeSchema = z.enum([\"any\", \"branch\", \"tag\", \"environment\"])\nconst scopeValueSchema = z.string()\n\ntype Fields = {\n  repository: z.infer<typeof repositorySchema>\n  scope: z.infer<typeof scopeSchema>\n  scopeValue: z.infer<typeof scopeValueSchema>\n  extraClaims: ClaimRow[]\n}\ntype Scope = Fields[\"scope\"]\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({\n    defaultValues: (defaultValue\n      ? (parse(defaultValue) ?? DEFAULTS)\n      : DEFAULTS) as Fields,\n    onSubmit: ({ value: fields }) => {\n      if (!(\n        claimsListValid(fields.extraClaims) &&\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  const values = {\n    repository: useStore(form.store, (state) => state.values.repository),\n    scope: useStore(form.store, (state) => state.values.scope),\n  }\n\n  const onChangeRef = React.useRef(onChange)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n  }, [onChange])\n\n  React.useEffect(() => {\n    let previousValues = form.store.state.values\n    const subscription = form.store.subscribe(() => {\n      const { values } = form.store.state\n      if (values === previousValues) {\n        return\n      }\n      previousValues = values\n      const policy = compile(values)\n      onChangeRef.current?.(\n        defaultValue?.label ? { ...policy, label: defaultValue.label } : policy\n      )\n    })\n    return () => subscription.unsubscribe()\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={(event) => {\n        event.preventDefault()\n        event.stopPropagation()\n        void form.handleSubmit()\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      <form.Field name=\"repository\" validators={{ onChange: repositorySchema }}>\n        {(field) => {\n          const isInvalid =\n            field.state.meta.isTouched && !field.state.meta.isValid\n          return (\n            <Field data-invalid={isInvalid}>\n              <FieldLabel htmlFor=\"gh-repository\" className=\"w-full\">\n                Repository <ClaimChip>aud · repository</ClaimChip>\n              </FieldLabel>\n              <Input\n                id=\"gh-repository\"\n                value={field.state.value}\n                placeholder=\"acme/web\"\n                aria-invalid={isInvalid}\n                onBlur={field.handleBlur}\n                onChange={(event) =>\n                  field.handleChange(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                  form.reset(\n                    { ...form.store.state.values, ...patch },\n                    { keepDefaultValues: true }\n                  )\n                }}\n              />\n              {isInvalid && <FieldError errors={field.state.meta.errors} />}\n              <FieldDescription>\n                owner/repo — or paste any GitHub link to fill the form.\n              </FieldDescription>\n            </Field>\n          )\n        }}\n      </form.Field>\n      <form.Field name=\"scope\" validators={{ onChange: scopeSchema }}>\n        {(field) => {\n          const isInvalid =\n            field.state.meta.isTouched && !field.state.meta.isValid\n          return (\n            <Field data-invalid={isInvalid}>\n              <FieldLabel htmlFor=\"gh-scope\">Scope by</FieldLabel>\n              <Select\n                items={SCOPE_OPTIONS}\n                value={field.state.value}\n                onValueChange={(next) => {\n                  field.handleChange((next ?? DEFAULTS.scope) as Scope)\n                  form.setFieldValue(\"scopeValue\", \"\")\n                }}\n              >\n                <SelectTrigger\n                  id=\"gh-scope\"\n                  className=\"w-full\"\n                  aria-invalid={isInvalid}\n                >\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              {isInvalid && <FieldError errors={field.state.meta.errors} />}\n            </Field>\n          )\n        }}\n      </form.Field>\n      {values.scope !== \"any\" ? (\n        <form.Field\n          name=\"scopeValue\"\n          validators={{ onChange: scopeValueSchema }}\n        >\n          {(field) => {\n            const isInvalid =\n              field.state.meta.isTouched && !field.state.meta.isValid\n            return (\n              <Field data-invalid={isInvalid}>\n                <FieldLabel htmlFor=\"gh-scopeValue\" className=\"w-full\">\n                  {values.scope === \"branch\"\n                    ? \"Branch name\"\n                    : values.scope === \"tag\"\n                      ? \"Tag name\"\n                      : \"Environment name\"}{\" \"}\n                  <ClaimChip>\n                    {values.scope === \"environment\" ? \"environment\" : \"ref\"}\n                  </ClaimChip>\n                </FieldLabel>\n                <Input\n                  id=\"gh-scopeValue\"\n                  value={field.state.value}\n                  placeholder={\n                    values.scope === \"environment\" ? \"production\" : \"main\"\n                  }\n                  aria-invalid={isInvalid}\n                  onBlur={field.handleBlur}\n                  onChange={(event) => field.handleChange(event.target.value)}\n                />\n                {isInvalid && <FieldError errors={field.state.meta.errors} />}\n              </Field>\n            )\n          }}\n        </form.Field>\n      ) : null}\n      {parseRepository(values.repository)?.owner ? (\n        <FieldDescription>\n          Audience derived automatically:{\" \"}\n          <span className=\"font-mono\">\n            https://github.com/{parseRepository(values.repository)?.owner}\n          </span>\n        </FieldDescription>\n      ) : null}\n      <form.Field name=\"extraClaims\">\n        {(field) => (\n          <ClaimsList\n            value={field.state.value}\n            onChange={field.handleChange}\n            reserved={FIRST_CLASS_CLAIMS}\n          />\n        )}\n      </form.Field>\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"
}