@laikacms/bitbucket
v2.0.0
Published
Bitbucket-backed StorageRepository for Laika CMS via the Cloud REST v2 API. Authenticates with an app password or OAuth2 bearer token. Runtime-agnostic — only depends on `fetch`.
Maintainers
Readme
@laikacms/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
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: '[email protected]' },
});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
| Operation | Bitbucket call |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| getObject | GET /src/{branch}/{path} for content + GET /src/{branch}/{path}?format=meta for metadata |
| createObject / updateObject / createOrUpdateObject | POST /src with the path as a form-field name |
| removeAtoms | POST /src with files=<path> for each delete |
| getFolder | GET /src/{branch}/{path}/ (trailing slash) — Bitbucket only returns a listing for trailing-slash URLs |
| listAtomSummaries | same; paginates through next until exhausted |
| createFolder | writes 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.oauthTokenorauth.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 theAuthorizationheader 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 aUser-Agentoverride:auth: { oauthToken: process.env.BITBUCKET_OAUTH_TOKEN!, headers: { 'X-Tenant-Id': 'esstudio' }, },
Advanced options
fetch— a customfetchimplementation. Defaults toglobalThis.fetch. Useful for tests (inject a mock/spy) or non-standard runtimes that don't expose a globalfetch:const repo = new BitbucketStorageRepository({ // ... fetch: mySpyFetch, });apiUrl— overrides the API base URL. Defaults tohttps://api.bitbucket.org/2.0. Useful for pointing at a self-hosted Bitbucket Data Center mirror or a test double: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 **/.laikacmscommitAuthor—{ 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-indefaultDetermineExtensionlogic 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.revisionIdis the commit hash that most recently touched the file. No native optimistic-concurrency on update — Bitbucket's commit endpoint doesn't accept anIf-Matchparallel.- Pagination.
next-URL drained to completion, then in-memoryoffset/pagestyles applied. - Errors. 401 →
UpstreamUnAuthorizedError(Bitbucket is an upstream, so a rejected credential surfaces asUpstreamUnAuthorizedError, notAuthenticationError), 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.
