Skip to content

Bitbucket ​

Storage ​

Source: packages/bitbucket

A Bitbucket-backed StorageRepository for Laika CMS via the Cloud REST v2 API. Completes the git-platform triumvirate alongside @laikacms/github and @laikacms/gitlab.

Runtime-agnostic — only depends on fetch. Works on Node, Bun, Deno, Cloudflare Workers, and the browser.

@laikacms/bitbucket/storage-bb ​

ts
import { BitbucketStorageRepository } from '@laikacms/bitbucket/storage-bb';
import { markdownSerializer } from 'laikacms/storage-serializers-markdown';

const repo = new BitbucketStorageRepository({
  workspace: 'esstudio',
  repo: 'content',
  branch: 'main',
  auth: {
    appPassword: { username: 'alice', password: process.env.BITBUCKET_APP_PW! },
    // or: oauthToken: process.env.BITBUCKET_OAUTH_TOKEN!,
    // or: tokenProvider: () => refreshedAccessToken(),
  },
  serializerRegistry: { md: markdownSerializer },
  defaultFileExtension: 'md',
  commitAuthor: { name: 'Laika Bot', email: 'bot@example.com' },
});

The Bitbucket-shaped quirk: one endpoint for every write ​

GitHub and GitLab each expose separate endpoints for createOrUpdateFileContents / deleteFile. Bitbucket folds them into one call: POST /repositories/{ws}/{repo}/src with a multipart body. Each form field whose name is a file path adds or updates that file; each form field literally named files whose value is a path deletes that path. The entire commit lands atomically.

This repository keeps the storage-contract surface one-file-at-a-time for parity with the other git platforms, but the underlying dataSource.commit({puts, deletes, commitMessage, author}) is a single round-trip multi-file commit you can call directly when you want one.

How operations map ​

OperationBitbucket call
getObjectGET /src/{branch}/{path} for content + GET /src/{branch}/{path}?format=meta for metadata
createObject / updateObject / createOrUpdateObjectPOST /src with the path as a form-field name
removeAtomsPOST /src with files=<path> for each delete
getFolderGET /src/{branch}/{path}/ (trailing slash) — Bitbucket only returns a listing for trailing-slash URLs
listAtomSummariessame; paginates through next until exhausted
createFolderwrites a .keep placeholder (git tracks files, not folders)

Auth model ​

Two modes, both handled behind the scenes:

  • App password (auth.appPassword) — Bitbucket's pre-OAuth credential format. Username + app-password tuple sent as HTTP Basic.
  • OAuth 2.0 (auth.oauthToken or auth.tokenProvider) — modern flow. Token sent as Bearer.

The end-to-end auth-header test verifies that the right scheme reaches the wire (Basic <b64> for app passwords, Bearer <token> for OAuth), so misconfiguration surfaces early.

  • Extra headers (auth.headers) — a plain object of headers merged into every request, after the Authorization header and before any per-call override. No default. Useful for Bitbucket-adjacent proxies or gateways that require an additional header such as a tenant ID or a User-Agent override:

    ts
    auth: {
      oauthToken: process.env.BITBUCKET_OAUTH_TOKEN!,
      headers: { 'X-Tenant-Id': 'esstudio' },
    },

Advanced options ​

  • fetch — a custom fetch implementation. Defaults to globalThis.fetch. Useful for tests (inject a mock/spy) or non-standard runtimes that don't expose a global fetch:

    ts
    const repo = new BitbucketStorageRepository({
      // ...
      fetch: mySpyFetch,
    });
  • apiUrl — overrides the API base URL. Defaults to https://api.bitbucket.org/2.0. Useful for pointing at a self-hosted Bitbucket Data Center mirror or a test double:

    ts
    const repo = new BitbucketStorageRepository({
      // ...
      apiUrl: 'https://bitbucket.internal.example.com/2.0',
    });
  • ignoreList — glob patterns for files excluded from directory listings. When supplied, overrides the built-in list entirely. Default:

    **/.keep
    **/.DS_Store
    **/Thumbs.db
    **/desktop.ini
    **/.catalog
    **/.laikacms
  • commitAuthor — { name: string; email: string } stamped as both the author and committer on every write call. Omit to let Bitbucket infer the identity from the auth credential.

  • determineExtension — custom resolver that picks the file extension for a new object given its key and metadata. Replaces the built-in defaultDetermineExtension logic when provided.

Behaviour notes ​

  • Extension hiding. Keys are extension-free at the boundary; the on-server file name is <key>.<ext> where <ext> is picked from the registered serializers (matches every other git-platform repository in the suite).
  • metadata.revisionId is the commit hash that most recently touched the file. No native optimistic-concurrency on update — Bitbucket's commit endpoint doesn't accept an If-Match parallel.
  • Pagination. next-URL drained to completion, then in-memory offset/page styles applied.
  • Errors. 401 → UpstreamUnAuthorizedError (Bitbucket is an upstream, so a rejected credential surfaces as UpstreamUnAuthorizedError, not AuthenticationError), 403 → ForbiddenError, 404 → NotFoundError, 429 → TooManyRequestsError, 5xx → ServiceUnavailableError.

What this does not do ​

  • No commit signing.
  • No PR / merge-request integration. Writes go directly to the configured branch.
  • No webhook subscriptions.

Released under the MIT License.