{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-bitbucket-tanstack",
  "title": "Bitbucket Provider (TanStack Form)",
  "description": "A self-contained form component that emits an OidcPolicy for a Bitbucket Pipelines caller: paste-in identity provider URL and audience from repository settings, plus repository UUID scoping. Note: the discovery document only exists after Pipelines OIDC is enabled and one pipeline has run. Built with TanStack Form.",
  "dependencies": [
    "@tanstack/react-form"
  ],
  "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/flavors/tanstack-none/provider-bitbucket.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useForm, useStore } from \"@tanstack/react-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 { 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 BitbucketIcon({ ...props }: React.ComponentProps<\"svg\">) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" {...props}>\n      <path d=\"M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z\" />\n    </svg>\n  )\n}\n\ntype Fields = {\n  issuer: string\n  audience: string\n  repositoryUuid: string\n  extraClaims: ClaimRow[]\n}\n\nconst DEFAULTS: Fields = {\n  issuer: \"\",\n  audience: \"\",\n  repositoryUuid: \"\",\n  extraClaims: [],\n}\n\nconst FIRST_CLASS_CLAIMS = [\"aud\", \"repositoryUuid\"]\n\nconst WORKSPACE_AUD_PREFIX = \"ari:cloud:bitbucket::workspace/\"\n\n// Bitbucket UUID claims are brace-wrapped; accept bare pastes too.\nfunction braced(uuid: string) {\n  if (!uuid) {\n    return uuid\n  }\n  return uuid.startsWith(\"{\") ? uuid : `{${uuid.replace(/}$/, \"\")}}`\n}\n\nfunction compile(fields: Fields) {\n  const claims: Record<string, string[]> = {}\n  if (fields.audience) {\n    claims.aud = [fields.audience]\n  }\n  if (fields.repositoryUuid) {\n    claims.repositoryUuid = [braced(fields.repositoryUuid)]\n  }\n  mergeClaimRows(claims, fields.extraClaims, FIRST_CLASS_CLAIMS)\n  return { issuer: fields.issuer, claims }\n}\n\nfunction parse(policy: OidcPolicy) {\n  if (\n    !policy.issuer.startsWith(\"https://api.bitbucket.org/2.0/workspaces/\") ||\n    !policy.issuer.endsWith(\"/pipelines-config/identity/oidc\")\n  ) {\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  return {\n    issuer: policy.issuer,\n    audience: policy.claims.aud?.[0] ?? \"\",\n    repositoryUuid: policy.claims.repositoryUuid?.[0] ?? \"\",\n    extraClaims,\n  }\n}\n\n/** True when this form can represent the policy — for routing an edit. */\nfunction matchesBitbucketPolicy(policy: OidcPolicy) {\n  return parse(policy) !== null\n}\n\nfunction BitbucketPolicyForm({\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        Boolean(fields.issuer && fields.audience) &&\n        claimsListValid(fields.extraClaims)\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    audience: useStore(form.store, (state) => state.values.audience),\n    repositoryUuid: useStore(\n      form.store,\n      (state) => state.values.repositoryUuid\n    ),\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=\"bitbucket-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      <form.Field name=\"issuer\">\n        {(field) => (\n          <Field>\n            <FieldLabel htmlFor=\"bb-issuer\" className=\"w-full\">\n              Identity provider URL <ClaimChip>iss</ClaimChip>\n            </FieldLabel>\n            <Input\n              id=\"bb-issuer\"\n              value={field.state.value}\n              placeholder=\"https://api.bitbucket.org/2.0/workspaces/acme/pipelines-config/identity/oidc\"\n              onBlur={field.handleBlur}\n              onChange={(event) =>\n                field.handleChange(event.target.value.trim())\n              }\n            />\n            <FieldDescription>\n              Repository → Settings → Pipelines → OpenID Connect. Copy it\n              exactly.\n            </FieldDescription>\n          </Field>\n        )}\n      </form.Field>\n      <form.Field name=\"audience\">\n        {(field) => (\n          <Field>\n            <FieldLabel htmlFor=\"bb-audience\" className=\"w-full\">\n              Audience <ClaimChip>aud</ClaimChip>\n            </FieldLabel>\n            <Input\n              id=\"bb-audience\"\n              value={field.state.value}\n              placeholder=\"ari:cloud:bitbucket::workspace/…\"\n              onBlur={field.handleBlur}\n              onChange={(event) =>\n                field.handleChange(event.target.value.trim())\n              }\n            />\n            <FieldDescription>\n              Same settings page. Pipelines can also declare custom audiences in\n              the step&apos;s oidc config.\n            </FieldDescription>\n          </Field>\n        )}\n      </form.Field>\n      <form.Field name=\"repositoryUuid\">\n        {(field) => (\n          <Field>\n            <FieldLabel htmlFor=\"bb-repositoryUuid\" className=\"w-full\">\n              Repository UUID <ClaimChip>repositoryUuid</ClaimChip>\n            </FieldLabel>\n            <Input\n              id=\"bb-repositoryUuid\"\n              value={field.state.value}\n              placeholder=\"{9a8b7c6d-…}\"\n              className=\"font-mono\"\n              onBlur={field.handleBlur}\n              onChange={(event) =>\n                field.handleChange(event.target.value.trim())\n              }\n            />\n            <FieldDescription>\n              From the example payload on the same page. Optional to narrow\n              trust to one repository.\n            </FieldDescription>\n          </Field>\n        )}\n      </form.Field>\n      {values.audience &&\n      !values.audience.startsWith(WORKSPACE_AUD_PREFIX) &&\n      !values.repositoryUuid ? (\n        <Alert>\n          <AlertTitle>\n            A custom audience doesn&apos;t name the workspace — add the\n            repository UUID so this rule still pins an identity.\n          </AlertTitle>\n        </Alert>\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 { BitbucketIcon, BitbucketPolicyForm, matchesBitbucketPolicy }\n",
      "type": "registry:component",
      "target": "@components/oidc/provider-bitbucket.tsx"
    }
  ],
  "meta": {
    "tagline": "Trust a workspace's pipelines"
  },
  "categories": [
    "auth",
    "forms"
  ],
  "type": "registry:component"
}