BitBucket app password deprecation and private dependencies

In the “How do I use private dependencies with Vercel?” docs (link How do I use private dependencies with Vercel?)

It says that for BitBucket I need to generate a Bitbucket app password and then use the following format:

"package-name": "git+https://<user>:<app-password>@bitbucket.org/<user>/<repo>.git"

However, in BitBucket’s docs it says that the recommended way of auth is to use API Token, which are the long term replacement for App passwords.

It seems that app password is deprecated. From BitBucket docs (link: Revoke an App password | Bitbucket Cloud | Atlassian Support)

As of Sept 9, 2025, app passwords can no longer be created. Use API tokens with scopes instead. All existing app passwords will be disabled on June 9, 2026. Migrate any integrations before then to avoid disruptions.

My question is: Anyone managed to get Vercel’s CI working with API Tokens and pnpm? If so, how?

Hi, thanks for raising the issue! We have updated the docs with a proper flow for Bitbucket

"package-name": "git+https://x-token-auth:<access-token>@bitbucket.org/<user>/<repo>.git""
2 Likes

Thank you @jacobparis

Actually I had some trouble getting this to work with pnpm because it prefers to use SSH instead of HTTP.

The problem is that with current pnpm version (v10.17.0 at time of writing) even if I add something like in my package.json

"my-repo": "git+https://x-token-auth:<access-token>@bitbucket.org/<my-org>/<my-repo>.git""

I end up with following entries in pnpm-lock.yaml

my-repo@git+https://git@bitbucket.org:my-org/my-repo.git#1e37d6cc8bb4a22a897b01774e6ffd04a8a2617b:
  resolution: {commit: 1e37d6cc8bb4a22a897b01774e6ffd04a8a2617b, repo: git@bitbucket.org:my-org/my-repo.git, type: git}
  version: 0.1.0
  engines: {node: '>= 20.x.x'}

my-repo@git+https://git@bitbucket.org:my-org/my-repo.git#1e37d6cc8bb4a22a897b01774e6ffd04a8a2617b:
  dependencies:
    date-fns: 4.1.0
    date-fns-tz: 3.2.0(date-fns@4.1.0)
    lodash: 4.17.21  

I’m assuming that by default pnpm “prefers” to use SSH. Since locally I have my SSH keys configured, it works without any issues and pnpm is able to clone the repo which is then registered in the lockfile with SSH URL. This happens even if I explicitly add the repo with HTTPS URL.

This is problematic in 2 ways:

  • it exposes the token in package.json
  • it won’t work at Vercel’s CI/CD because pnpm will look at the lockfile and try to clone via SSH which is not configured in Vercel’s environment.

The solution I’ve come up with is to use pnpmfile.cjs and a preResolution hook (docs) to modify the lockfile before installation. The idea is to check if the repo can be accessed via SSH, and if not, modify the resolution to use HTTPS with access token.

This works locally and in Vercel’s CI/CD.

In the scenario that I’m working on, our NextJs-based project depends on a private Bitbucket repo called main-dependency-package which in turn depends on another private Bitbucket repo called sub-dependency-package. So the pnpmfile.cjs modifies the resolution of both packages.

The only thing that need to be set in the environment are the access tokens for both repos:

  • MAIN_DEPENDENCY_TOKEN
  • SUB_DEPENDENCY_TOKEN

Code:

const { execSync } = require("child_process");

const ORG_NAME = "my-org";
const MAIN_DEPENDENCY_PACKAGE_NAME = "main-dependency-package";
const SUB_DEPENDENCY_PACKAGE_NAME = "sub-dependency-package";

const MAIN_DEPENDENCY_TOKEN = process.env.MAIN_DEPENDENCY_TOKEN;
const SUB_DEPENDENCY_TOKEN = process.env.SUB_DEPENDENCY_TOKEN;

const MAIN_DEPENDENCY_REPO_URL = `https://x-token-auth:${encodeURIComponent(
  MAIN_DEPENDENCY_TOKEN
)}@bitbucket.org/${ORG_NAME}/${MAIN_DEPENDENCY_PACKAGE_NAME}.git`;

const SUB_DEPENDENCY_REPO_URL = `https://x-token-auth:${encodeURIComponent(
  SUB_DEPENDENCY_TOKEN
)}@bitbucket.org/${ORG_NAME}/${SUB_DEPENDENCY_PACKAGE_NAME}.git`;

/**
 * Check if we can access a Bitbucket repository via SSH
 *
 * @param {Object} params
 * @param {string} params.orgName
 * @param {string} params.repoName
 * @returns
 */
function statBitbucketRepo({ orgName, repoName }) {
  try {
    const command = `git ls-remote git@bitbucket.org:${orgName}/${repoName}.git`;
    execSync(command, { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
}

/**
 * Check if we can clone the Git repositories via SSH
 * @returns {boolean}
 */
function canGitClone() {
  const canAccessMainDep = statBitbucketRepo({
    orgName: ORG_NAME,
    repoName: MAIN_DEPENDENCY_PACKAGE_NAME,
  });
  const canAccessSubDep = statBitbucketRepo({
    orgName: ORG_NAME,
    repoName: SUB_DEPENDENCY_PACKAGE_NAME,
  });

  return canAccessMainDep && canAccessSubDep;
}

/**
 * Pnpm preResolution hook to modify dependencies in the lockfile
 * to use HTTPS with access tokens instead of SSH.
 *
 * Docs: https://pnpm.io/pnpmfile#hookspreresolutionoptions-promisevoid
 *
 * @param {Object} options
 * @param {Object} options.wantedLockfile - The current lockfile
 * @returns {Object} The modified options
 */
function preResolution(options) {
  const lockfile = options.wantedLockfile;
  if (!lockfile) return options;
  if (!lockfile.packages) return options;

  if (canGitClone()) {
    console.log(
      `Can access "${MAIN_DEPENDENCY_PACKAGE_NAME}" and "${SUB_DEPENDENCY_PACKAGE_NAME}" via SSH. No patching needed.`
    );
    return options;
  }

  console.log(
    `Patching "${MAIN_DEPENDENCY_PACKAGE_NAME}" and "${SUB_DEPENDENCY_PACKAGE_NAME}" to use HTTPS with access tokens.`
  );

  
  if (!MAIN_DEPENDENCY_TOKEN)
    throw new Error("MAIN_DEPENDENCY_TOKEN is not set");

  if (!SUB_DEPENDENCY_TOKEN) throw new Error("SUB_DEPENDENCY_TOKEN is not set");

  // First, we need to modify the sub-dependency, because the main dependency will reference it
  const subDep = modifyDependency({
    packages: lockfile.packages,
    mainPackageName: SUB_DEPENDENCY_PACKAGE_NAME,
    mainPackageRepoUrl: SUB_DEPENDENCY_REPO_URL,
  });
  if (subDep) {
    const { newKey, oldKey, pkg } = subDep;
    delete lockfile.packages[oldKey];
    lockfile.packages[newKey] = pkg;

    // console.log({ oldKey, newKey, pkg });
  }

  const mainDep = modifyDependency({
    packages: lockfile.packages,
    mainPackageName: MAIN_DEPENDENCY_PACKAGE_NAME,
    mainPackageRepoUrl: MAIN_DEPENDENCY_REPO_URL,
    subDepPackageName: SUB_DEPENDENCY_PACKAGE_NAME,
    subDepPackageRepoUrl: SUB_DEPENDENCY_REPO_URL,
  });
  if (mainDep) {
    const { oldKey, pkg } = mainDep;
    lockfile.packages[oldKey] = pkg;

    // console.log({ oldKey, pkg });
  }

  console.log("Patching done.");

  return options;
}

/**
 * Modify a dependency in the lockfile to use HTTPS with access token
 * instead of SSH.
 *
 * @param {Object} params
 * @param {Record<string, PnpmLockfileEntry>} params.packages
 * @param {string} params.mainPackageName - e.g. `main-dependency-package`
 * @param {string} params.mainPackageRepoUrl - e.g. `https://x-token-auth:...@bitbucket.org/my-org/main-dependency-package.git`
 * @param {string} [params.subDepPackageName] - e.g. `sub-dependency-package`
 * @param {string} [params.subDepPackageRepoUrl] - e.g. `https://x-token-auth:...@bitbucket.org/my-org/sub-dependency-package.git`
 * @returns {Object|null} - Returns an object with oldKey, newKey and modified pkg, or null if not found
 */
function modifyDependency({
  packages,
  mainPackageName,
  mainPackageRepoUrl,
  subDepPackageName,
  subDepPackageRepoUrl,
}) {
  const key = getPackageNameFromKey({
    packages,
    packageName: mainPackageName,
  });

  if (!key) return null;
  /** @type {PnpmLockfileEntry} */
  const pkg = packages[key];
  if (!pkg) return null;

  const newKey = rewriteDependencyKey({
    key: key,
    packageName: mainPackageName,
    repoUrl: mainPackageRepoUrl,
  });
  pkg.resolution.repo = mainPackageRepoUrl;

  if (
    subDepPackageName &&
    pkg.dependencies &&
    pkg.dependencies[subDepPackageName]
  ) {
    const from = pkg.dependencies[subDepPackageName];
    const [_, commit] = from.split("#");
    pkg.dependencies[
      subDepPackageName
    ] = `git+${subDepPackageRepoUrl}#${commit}`;
  }

  return { oldKey: key, newKey, pkg };
}

/**
 * @param {Object} params
 * @param {Record<string, PnpmLockfileEntry>} params.packages
 * @param {string} params.packageName
 * @returns
 */
function getPackageNameFromKey({ packages, packageName }) {
  const key = Object.keys(packages).find((k) => k.includes(packageName));
  return key ? key : null;
}

/**
 * Rewrite a dependency key to use HTTPS with access token instead of SSH.
 * Example:
 * ```js
 * const input ='repo-name@git+https://git@bitbucket.org:org-name/repo-name.git#<COMMIT>'
 * const output = 'repo-name@git+https://x-token-auth:<REPO-TOKEN>@bitbucket.org/my-org/repo-name.git#<COMMIT>'
 * ```
 * @param {Object} params
 * @param {string} params.key - lockfile key that identifies a package
 * @param {string} params.packageName - e.g. `main-dependency-package`
 * @param {string} params.repoUrl - HTTP that includes `x-token-auth:...`
 */
function rewriteDependencyKey({ key, packageName, repoUrl }) {
  const [name] = key.split("@");
  const [_, commit] = key.split("#");

  if (name !== packageName) return key;

  const newKey = commit
    ? `${name}@git+${repoUrl}#${commit}`
    : `${name}@git+${repoUrl}`;

  return newKey;
}

module.exports = {
  hooks: {
    preResolution,
  },
};

/**
 * @typedef PnpmLockfileEntry
 * @property {string} version
 * @property {Record<string, string>} [dependencies]
 * @property {Object} resolution
 * @property {string} resolution.commit
 * @property {string} resolution.repo
 * @property {string} resolution.type
 */

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.