From 9d0e5ecf51b532277a2e0465c11719ec3ef9cf23 Mon Sep 17 00:00:00 2001 From: abhishek-2k23 Date: Sun, 16 Aug 2026 21:26:07 +0530 Subject: [PATCH 1/5] feat(bitbucket): add bitbucket plugin --- packages/bitbucket/client.test.ts | 57 + packages/bitbucket/client.ts | 219 ++ packages/bitbucket/endpoints/factory.ts | 121 + packages/bitbucket/endpoints/index.ts | 211 ++ packages/bitbucket/endpoints/logging.ts | 35 + packages/bitbucket/endpoints/operations.ts | 3082 ++++++++++++++++++++ packages/bitbucket/endpoints/types.ts | 914 ++++++ packages/bitbucket/error-handlers.ts | 59 + packages/bitbucket/index.ts | 1121 +++++++ packages/bitbucket/integration.test.ts | 13 + packages/bitbucket/jest.config.cjs | 39 + packages/bitbucket/package.json | 44 + packages/bitbucket/routing.test.ts | 93 + packages/bitbucket/schema/database.ts | 2 + packages/bitbucket/schema/index.ts | 1 + packages/bitbucket/tsconfig.json | 20 + packages/bitbucket/tsup.config.ts | 14 + packages/corsair/core/constants.ts | 3 + pnpm-lock.yaml | 106 +- 19 files changed, 6099 insertions(+), 55 deletions(-) create mode 100644 packages/bitbucket/client.test.ts create mode 100644 packages/bitbucket/client.ts create mode 100644 packages/bitbucket/endpoints/factory.ts create mode 100644 packages/bitbucket/endpoints/index.ts create mode 100644 packages/bitbucket/endpoints/logging.ts create mode 100644 packages/bitbucket/endpoints/operations.ts create mode 100644 packages/bitbucket/endpoints/types.ts create mode 100644 packages/bitbucket/error-handlers.ts create mode 100644 packages/bitbucket/index.ts create mode 100644 packages/bitbucket/integration.test.ts create mode 100644 packages/bitbucket/jest.config.cjs create mode 100644 packages/bitbucket/package.json create mode 100644 packages/bitbucket/routing.test.ts create mode 100644 packages/bitbucket/schema/database.ts create mode 100644 packages/bitbucket/schema/index.ts create mode 100644 packages/bitbucket/tsconfig.json create mode 100644 packages/bitbucket/tsup.config.ts diff --git a/packages/bitbucket/client.test.ts b/packages/bitbucket/client.test.ts new file mode 100644 index 000000000..481f55627 --- /dev/null +++ b/packages/bitbucket/client.test.ts @@ -0,0 +1,57 @@ +import { request } from 'corsair/http'; +import { + BitbucketAPIError, + getValidBitbucketAccessToken, + makeAuthenticatedBitbucketRequest, + refreshBitbucketAccessToken, +} from './client'; + +jest.mock('corsair/http', () => { + const actual = jest.requireActual('corsair/http'); + return { ...actual, request: jest.fn() }; +}); +const mockRequest = request as jest.MockedFunction; +describe('Bitbucket OAuth client', () => { + beforeEach(() => mockRequest.mockReset()); + it('reuses an access token outside the expiry skew', async () => { + const result = await getValidBitbucketAccessToken({ + accessToken: 'access', + expiresAt: String(Math.floor(Date.now() / 1000) + 1800), + }); + expect(result).toMatchObject({ accessToken: 'access', refreshed: false }); + expect(mockRequest).not.toHaveBeenCalled(); + }); + it('refreshes with HTTP Basic client authentication and rotates the refresh token', async () => { + mockRequest.mockResolvedValueOnce({ + access_token: 'fresh', + refresh_token: 'rotated', + expires_in: 3600, + }); + const result = await refreshBitbucketAccessToken( + 'client', + 'secret', + 'refresh', + ); + expect(result.refresh_token).toBe('rotated'); + const [config, options] = mockRequest.mock.calls[0] ?? []; + const headers = config?.HEADERS as Record | undefined; + expect(headers?.Authorization).toBe( + 'Basic ' + Buffer.from('client:secret').toString('base64'), + ); + expect(options?.body).toContain('grant_type=refresh_token'); + expect(options?.body).toContain('refresh_token=refresh'); + }); + it('retries once with a refreshed token after a 401', async () => { + const refresh = jest.fn().mockResolvedValue('fresh'); + mockRequest + .mockRejectedValueOnce(new BitbucketAPIError('unauthorized', 401)) + .mockResolvedValueOnce({ ok: true }); + const result = await makeAuthenticatedBitbucketRequest( + '/user', + { key: 'stale', _refreshAuth: refresh }, + { method: 'GET', retrySafe: true }, + ); + expect(result).toEqual({ ok: true }); + expect(refresh).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/bitbucket/client.ts b/packages/bitbucket/client.ts new file mode 100644 index 000000000..c06db9f8a --- /dev/null +++ b/packages/bitbucket/client.ts @@ -0,0 +1,219 @@ +import type { + ApiRequestOptions, + OpenAPIConfig, + RateLimitConfig, +} from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; + +export const BITBUCKET_API_BASE = 'https://api.bitbucket.org/2.0'; +export const BITBUCKET_AUTH_URL = 'https://bitbucket.org/site/oauth2/authorize'; +export const BITBUCKET_TOKEN_URL = + 'https://bitbucket.org/site/oauth2/access_token'; + +function rateLimitConfig(retrySafe: boolean): RateLimitConfig { + return { + enabled: true, + maxRetries: retrySafe ? 3 : 0, + initialRetryDelay: 1000, + backoffMultiplier: 2, + headerNames: { + retryAfter: 'Retry-After', + resetTime: 'X-RateLimit-Reset', + remaining: 'X-RateLimit-Remaining', + limit: 'X-RateLimit-Limit', + }, + }; +} +export class BitbucketOAuthError extends Error { + constructor(message: string) { + super(message); + this.name = 'BitbucketOAuthError'; + } +} +export class BitbucketAPIError extends Error { + constructor( + message: string, + public readonly status?: number, + public readonly retryAfter?: number, + ) { + super(message); + this.name = 'BitbucketAPIError'; + } +} +export class BitbucketSchemaError extends Error { + constructor( + message: string, + public readonly direction: 'input' | 'output', + public readonly issues: { path: string; message: string }[] = [], + ) { + super(message); + this.name = 'BitbucketSchemaError'; + } +} + +export type BitbucketTokenResult = { + access_token: string; + refresh_token?: string; + expires_in?: number; + token_type?: string; + scopes?: string; +}; +export async function refreshBitbucketAccessToken( + clientId: string, + clientSecret: string, + refreshToken: string, +): Promise { + const config: OpenAPIConfig = { + BASE: 'https://bitbucket.org', + VERSION: '2', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: undefined, + HEADERS: { + Authorization: + 'Basic ' + + Buffer.from(clientId + ':' + clientSecret).toString('base64'), + Accept: 'application/json', + }, + }; + const body = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }).toString(); + try { + return await request( + config, + { + method: 'POST', + url: '/site/oauth2/access_token', + body, + mediaType: 'application/x-www-form-urlencoded', + }, + { rateLimitConfig: rateLimitConfig(false) }, + ); + } catch (error) { + throw new BitbucketOAuthError( + 'Failed to refresh Bitbucket access token: ' + + (error instanceof Error ? error.message : String(error)), + ); + } +} +export async function getValidBitbucketAccessToken({ + accessToken, + expiresAt, + refreshToken, + clientId, + clientSecret, + forceRefresh = false, +}: { + accessToken?: string | null; + expiresAt?: string | null; + refreshToken?: string | null; + clientId?: string | null; + clientSecret?: string | null; + forceRefresh?: boolean; +}): Promise<{ + accessToken: string; + refreshToken?: string; + expiresAt: number; + refreshed: boolean; +}> { + const now = Math.floor(Date.now() / 1000); + if ( + !forceRefresh && + accessToken && + (!expiresAt || Number(expiresAt) > now + 300) + ) + return { + accessToken, + refreshToken: refreshToken ?? undefined, + expiresAt: expiresAt ? Number(expiresAt) : now + 3600, + refreshed: false, + }; + if (!refreshToken || !clientId || !clientSecret) + throw new BitbucketOAuthError( + 'Bitbucket refresh token and OAuth client credentials are required', + ); + const token = await refreshBitbucketAccessToken( + clientId, + clientSecret, + refreshToken, + ); + return { + accessToken: token.access_token, + refreshToken: token.refresh_token ?? refreshToken, + expiresAt: now + (token.expires_in ?? 3600), + refreshed: true, + }; +} +export type BitbucketRequestOptions = { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + body?: unknown; + query?: Record; + mediaType?: string; + retrySafe?: boolean; +}; +export async function makeBitbucketRequest( + endpoint: string, + accessToken: string, + options: BitbucketRequestOptions, +): Promise { + const config: OpenAPIConfig = { + BASE: BITBUCKET_API_BASE, + VERSION: '2.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: undefined, + HEADERS: { + Authorization: 'Bearer ' + accessToken, + Accept: 'application/json', + }, + }; + const requestOptions: ApiRequestOptions = { + method: options.method, + url: endpoint, + body: options.body, + query: options.query, + mediaType: + options.body === undefined + ? undefined + : (options.mediaType ?? 'application/json'), + }; + try { + const response = await request(config, requestOptions, { + rateLimitConfig: rateLimitConfig(options.retrySafe ?? false), + }); + return (response === undefined ? null : response) as T; + } catch (error) { + if (error instanceof ApiError) + throw new BitbucketAPIError( + 'Bitbucket API request failed with status ' + error.status, + error.status, + error.retryAfter, + ); + throw error; + } +} +export type BitbucketAuthContext = { + key: string; + _refreshAuth?: () => Promise; +}; +export async function makeAuthenticatedBitbucketRequest( + endpoint: string, + ctx: BitbucketAuthContext, + options: BitbucketRequestOptions, +): Promise { + try { + return await makeBitbucketRequest(endpoint, ctx.key, options); + } catch (error) { + if ( + error instanceof BitbucketAPIError && + error.status === 401 && + ctx._refreshAuth + ) { + const freshToken = await ctx._refreshAuth(); + return await makeBitbucketRequest(endpoint, freshToken, options); + } + throw error; + } +} diff --git a/packages/bitbucket/endpoints/factory.ts b/packages/bitbucket/endpoints/factory.ts new file mode 100644 index 000000000..3b0964e71 --- /dev/null +++ b/packages/bitbucket/endpoints/factory.ts @@ -0,0 +1,121 @@ +import type { CorsairEndpoint } from 'corsair/core'; +import { logEventFromContext } from 'corsair/core'; +import type { ZodType } from 'zod'; +import type { BitbucketAuthContext } from '../client'; +import { + BitbucketSchemaError, + makeAuthenticatedBitbucketRequest, +} from '../client'; +import type { BitbucketContext } from '../index'; +import { bitbucketAuditPayload } from './logging'; +import type { BitbucketEndpointKey, BitbucketOperation } from './operations'; +import { bitbucketOperationByKey } from './operations'; +import type { + BitbucketEndpointInputs, + BitbucketEndpointOutputs, +} from './types'; +import { + BitbucketEndpointInputSchemas, + BitbucketEndpointOutputSchemas, +} from './types'; + +function parseWithSchema( + schema: ZodType, + value: unknown, + direction: 'input' | 'output', + endpointPath: string, +): T { + const result = schema.safeParse(value); + if (result.success) return result.data as T; + const issues = result.error.issues.map((issue) => ({ + path: issue.path.join('.') || '(root)', + message: issue.message, + })); + throw new BitbucketSchemaError( + '[BITBUCKET] Invalid ' + + direction + + ' for ' + + endpointPath + + ': ' + + issues.map((issue) => issue.path + ' — ' + issue.message).join('; '), + direction, + issues, + ); +} +function pathValue(value: unknown, name: string): string { + if (typeof value !== 'string' && typeof value !== 'number') + throw new Error('[BITBUCKET] Missing required path parameter: ' + name); + return encodeURIComponent(String(value)).replaceAll('%2F', '/'); +} +export function buildBitbucketWireRequest( + definition: BitbucketOperation, + input: Record, +) { + const values = { ...input, ...definition.fixedPathValues } as Record< + string, + unknown + >; + const url = definition.apiPath.replace(/\{([^}]+)\}/g, (_match, name) => + pathValue(values[name], name), + ); + const query = Object.fromEntries( + definition.queryFields + .map((name) => [name, input[name]]) + .filter(([, value]) => value !== undefined), + ); + if (definition.projectFilterField) { + const projectKey = input[definition.projectFilterField]; + if (typeof projectKey !== 'string' || !projectKey) + throw new Error('[BITBUCKET] Missing project key'); + const escaped = projectKey.replaceAll('\\', '\\\\').replaceAll('"', '\\"'); + const projectQuery = 'project.key="' + escaped + '"'; + query.q = + typeof query.q === 'string' && query.q + ? '(' + query.q + ') AND ' + projectQuery + : projectQuery; + } + return { + url, + method: definition.httpMethod, + query, + body: definition.acceptsBody ? input.body : undefined, + mediaType: definition.mediaType, + retrySafe: definition.riskLevel === 'read', + } as const; +} +export function createBitbucketEndpoint( + key: K, +): CorsairEndpoint< + BitbucketContext, + BitbucketEndpointInputs[K], + BitbucketEndpointOutputs[K] +> { + return async (ctx, typedInput) => { + const definition = bitbucketOperationByKey[key]; + const input = parseWithSchema>( + BitbucketEndpointInputSchemas[key], + typedInput, + 'input', + definition.path, + ); + const wire = buildBitbucketWireRequest(definition, input); + const raw = await makeAuthenticatedBitbucketRequest( + wire.url, + ctx as unknown as BitbucketAuthContext, + wire, + ); + const response = parseWithSchema( + BitbucketEndpointOutputSchemas[key], + raw, + 'output', + definition.path, + ); + await logEventFromContext( + ctx, + 'bitbucket.' + definition.path, + bitbucketAuditPayload(input), + 'completed', + ); + return response; + }; +} diff --git a/packages/bitbucket/endpoints/index.ts b/packages/bitbucket/endpoints/index.ts new file mode 100644 index 000000000..40781b5c8 --- /dev/null +++ b/packages/bitbucket/endpoints/index.ts @@ -0,0 +1,211 @@ +import { createBitbucketEndpoint } from './factory'; +export const BitbucketEndpoints = { + pullRequests: { + approvePullRequest: createBitbucketEndpoint('approvePullRequest'), + createPullRequest: createBitbucketEndpoint('createPullRequest'), + createPullRequestComment: createBitbucketEndpoint( + 'createPullRequestComment', + ), + getPullRequest: createBitbucketEndpoint('getPullRequest'), + getPullRequestCommits: createBitbucketEndpoint('getPullRequestCommits'), + getPullRequestDiff: createBitbucketEndpoint('getPullRequestDiff'), + getPullRequestDiffstat: createBitbucketEndpoint('getPullRequestDiffstat'), + getPullRequestComment: createBitbucketEndpoint('getPullRequestComment'), + getRepositoriesPullrequestsComments: createBitbucketEndpoint( + 'getRepositoriesPullrequestsComments', + ), + getRepositoriesPullrequestsStatuses: createBitbucketEndpoint( + 'getRepositoriesPullrequestsStatuses', + ), + getRepositoriesPullrequestsActivity: createBitbucketEndpoint( + 'getRepositoriesPullrequestsActivity', + ), + listPullRequestTasks: createBitbucketEndpoint('listPullRequestTasks'), + listPullRequests: createBitbucketEndpoint('listPullRequests'), + requestPullRequestChanges: createBitbucketEndpoint( + 'requestPullRequestChanges', + ), + }, + sourceAndRefs: { + browseRepositoryPath: createBitbucketEndpoint('browseRepositoryPath'), + createBranch: createBitbucketEndpoint('createBranch'), + getRepositoriesBranchingModel: createBitbucketEndpoint( + 'getRepositoriesBranchingModel', + ), + getBranch: createBitbucketEndpoint('getBranch'), + getRepositoriesEffectiveBranchingModel: createBitbucketEndpoint( + 'getRepositoriesEffectiveBranchingModel', + ), + getRepositoriesFilehistory: createBitbucketEndpoint( + 'getRepositoriesFilehistory', + ), + getFileFromRepository: createBitbucketEndpoint('getFileFromRepository'), + getRawFileContent: createBitbucketEndpoint('getRawFileContent'), + getRepositoriesSrc: createBitbucketEndpoint('getRepositoriesSrc'), + getRepositoriesRefs: createBitbucketEndpoint('getRepositoriesRefs'), + getRepositoriesRefsTags: createBitbucketEndpoint('getRepositoriesRefsTags'), + listBranches: createBitbucketEndpoint('listBranches'), + listRepositoryPaths: createBitbucketEndpoint('listRepositoryPaths'), + listTags: createBitbucketEndpoint('listTags'), + }, + issues: { + getRepositoriesIssuesVote: createBitbucketEndpoint( + 'getRepositoriesIssuesVote', + ), + createIssue: createBitbucketEndpoint('createIssue'), + createIssueComment: createBitbucketEndpoint('createIssueComment'), + deleteIssue: createBitbucketEndpoint('deleteIssue'), + listIssues: createBitbucketEndpoint('listIssues'), + listVersions: createBitbucketEndpoint('listVersions'), + updateIssue: createBitbucketEndpoint('updateIssue'), + }, + commitsAndInsights: { + createRepositoriesCommitReportsAnnotations: createBitbucketEndpoint( + 'createRepositoriesCommitReportsAnnotations', + ), + deleteCommitComment: createBitbucketEndpoint('deleteCommitComment'), + deleteRepositoriesCommitReportsAnnotations: createBitbucketEndpoint( + 'deleteRepositoriesCommitReportsAnnotations', + ), + getCommitBuildStatus: createBitbucketEndpoint('getCommitBuildStatus'), + getCommitChanges: createBitbucketEndpoint('getCommitChanges'), + getCommitDiff: createBitbucketEndpoint('getCommitDiff'), + getRepositoriesCommitReports: createBitbucketEndpoint( + 'getRepositoriesCommitReports', + ), + getRepositoriesMergeBase: createBitbucketEndpoint( + 'getRepositoriesMergeBase', + ), + getRepositoriesCommit: createBitbucketEndpoint('getRepositoriesCommit'), + getRepositoryPatch: createBitbucketEndpoint('getRepositoryPatch'), + getCommitComment: createBitbucketEndpoint('getCommitComment'), + getRepositoriesCommitComments: createBitbucketEndpoint( + 'getRepositoriesCommitComments', + ), + getRepositoriesCommitReport: createBitbucketEndpoint( + 'getRepositoriesCommitReport', + ), + getRepositoriesCommitReportsAnnotations: createBitbucketEndpoint( + 'getRepositoriesCommitReportsAnnotations', + ), + getRepositoriesCommitStatuses: createBitbucketEndpoint( + 'getRepositoriesCommitStatuses', + ), + listCommits: createBitbucketEndpoint('listCommits'), + listCommitsFromRevision: createBitbucketEndpoint('listCommitsFromRevision'), + createRepositoriesCommits2: createBitbucketEndpoint( + 'createRepositoriesCommits2', + ), + listCommitsOnMaster: createBitbucketEndpoint('listCommitsOnMaster'), + updateRepositoriesCommitComments: createBitbucketEndpoint( + 'updateRepositoriesCommitComments', + ), + updateInsightsProjectsReposCommitsReports: createBitbucketEndpoint( + 'updateInsightsProjectsReposCommitsReports', + ), + updateRepositoriesCommitReportsAnnotations: createBitbucketEndpoint( + 'updateRepositoriesCommitReportsAnnotations', + ), + }, + repositories: { + createRepository: createBitbucketEndpoint('createRepository'), + deleteRepository: createBitbucketEndpoint('deleteRepository'), + getRepository: createBitbucketEndpoint('getRepository'), + getRepositoriesWatchers: createBitbucketEndpoint('getRepositoriesWatchers'), + listRepositories: createBitbucketEndpoint('listRepositories'), + listRepositoriesInWorkspace: createBitbucketEndpoint( + 'listRepositoriesInWorkspace', + ), + }, + snippets: { + createSnippetComment: createBitbucketEndpoint('createSnippetComment'), + deleteSnippetsWatch: createBitbucketEndpoint('deleteSnippetsWatch'), + getSnippet: createBitbucketEndpoint('getSnippet'), + getSnippetsWatch: createBitbucketEndpoint('getSnippetsWatch'), + listSnippets: createBitbucketEndpoint('listSnippets'), + }, + pipelinesAndDeployments: { + createTeamsPipelinesConfigVariables: createBitbucketEndpoint( + 'createTeamsPipelinesConfigVariables', + ), + createUsersPipelinesConfigVariables: createBitbucketEndpoint( + 'createUsersPipelinesConfigVariables', + ), + deleteUserPipelineVariable: createBitbucketEndpoint( + 'deleteUserPipelineVariable', + ), + getOpenidConfiguration: createBitbucketEndpoint('getOpenidConfiguration'), + getRepositoriesEnvironments2: createBitbucketEndpoint( + 'getRepositoriesEnvironments2', + ), + getDeploymentEnvironmentVariables: createBitbucketEndpoint( + 'getDeploymentEnvironmentVariables', + ), + getRepositoriesPipelinesSteps: createBitbucketEndpoint( + 'getRepositoriesPipelinesSteps', + ), + getRepositoriesPipelinesConfigSshKnownHosts: createBitbucketEndpoint( + 'getRepositoriesPipelinesConfigSshKnownHosts', + ), + getRepositoriesPipelinesConfigRunners: createBitbucketEndpoint( + 'getRepositoriesPipelinesConfigRunners', + ), + getRepositoriesPipelinesConfigSchedules: createBitbucketEndpoint( + 'getRepositoriesPipelinesConfigSchedules', + ), + getRepositoriesPipelinesConfigVariables: createBitbucketEndpoint( + 'getRepositoriesPipelinesConfigVariables', + ), + getRepositoriesPipelinesConfigCaches: createBitbucketEndpoint( + 'getRepositoriesPipelinesConfigCaches', + ), + getRepositoriesPipelines2: createBitbucketEndpoint( + 'getRepositoriesPipelines2', + ), + listDeployments: createBitbucketEndpoint('listDeployments'), + listPipelines: createBitbucketEndpoint('listPipelines'), + listRepositoriesEnvironments: createBitbucketEndpoint( + 'listRepositoriesEnvironments', + ), + updateTeamsPipelinesConfigVariables: createBitbucketEndpoint( + 'updateTeamsPipelinesConfigVariables', + ), + updateUsersPipelinesConfigVariables: createBitbucketEndpoint( + 'updateUsersPipelinesConfigVariables', + ), + }, + usersAndPermissions: { + getSshLatestKeys: createBitbucketEndpoint('getSshLatestKeys'), + getCurrentUser2: createBitbucketEndpoint('getCurrentUser2'), + getUser: createBitbucketEndpoint('getUser'), + getUserEmails2: createBitbucketEndpoint('getUserEmails2'), + getUserEmails: createBitbucketEndpoint('getUserEmails'), + getUserPermissionsRepositories: createBitbucketEndpoint( + 'getUserPermissionsRepositories', + ), + getUserPermissionsWorkspaces: createBitbucketEndpoint( + 'getUserPermissionsWorkspaces', + ), + getUserWorkspaces: createBitbucketEndpoint('getUserWorkspaces'), + }, + workspacesAndProjects: { + getWorkspacesPullrequests: createBitbucketEndpoint( + 'getWorkspacesPullrequests', + ), + getProjectsRepos: createBitbucketEndpoint('getProjectsRepos'), + getWorkspace: createBitbucketEndpoint('getWorkspace'), + listWorkspaceMembers: createBitbucketEndpoint('listWorkspaceMembers'), + listWorkspaceProjects: createBitbucketEndpoint('listWorkspaceProjects'), + listWorkspaces: createBitbucketEndpoint('listWorkspaces'), + }, + searchAndDiscovery: { + getHookEvents: createBitbucketEndpoint('getHookEvents'), + searchTeamCode: createBitbucketEndpoint('searchTeamCode'), + searchUserRepositoriesCode: createBitbucketEndpoint( + 'searchUserRepositoriesCode', + ), + getWorkspacesSearchCode: createBitbucketEndpoint('getWorkspacesSearchCode'), + }, +} as const; +export * from './operations'; +export * from './types'; diff --git a/packages/bitbucket/endpoints/logging.ts b/packages/bitbucket/endpoints/logging.ts new file mode 100644 index 000000000..14db2c251 --- /dev/null +++ b/packages/bitbucket/endpoints/logging.ts @@ -0,0 +1,35 @@ +const safeNames = new Set([ + 'workspace', + 'repo_slug', + 'commit', + 'revision', + 'spec', + 'revspec', + 'pull_request_id', + 'issue_id', + 'comment_id', + 'reportId', + 'annotationId', + 'encoded_id', + 'environment_uuid', + 'pipeline_uuid', + 'variable_uuid', + 'page', + 'pagelen', + 'state', + 'role', + 'sort', + 'max_depth', + 'subject_type', +]); +export function bitbucketAuditPayload( + input: Record, +): Record { + return Object.fromEntries( + Object.entries(input).filter( + ([name, value]) => + safeNames.has(name) && + ['string', 'number', 'boolean'].includes(typeof value), + ), + ) as Record; +} diff --git a/packages/bitbucket/endpoints/operations.ts b/packages/bitbucket/endpoints/operations.ts new file mode 100644 index 000000000..55e0551f4 --- /dev/null +++ b/packages/bitbucket/endpoints/operations.ts @@ -0,0 +1,3082 @@ +export const bitbucketOperationCatalog = [ + { + code: 'BITBUCKET_APPROVE_PULL_REQUEST', + title: 'Approve Pull Request', + description: + 'Tool to approve a pull request as the authenticated user. Use when you need to formally approve changes in a pull request review process.', + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/approve', + key: 'approvePullRequest', + group: 'pullRequests', + path: 'pullRequests.approvePullRequest', + httpMethod: 'POST', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/approve', + riskLevel: 'write', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest:write'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_BROWSE_REPOSITORY_PATH', + title: 'Browse repository path', + description: + 'Tool to retrieve content for a file path or browse directory contents at a specified revision in a Bitbucket repository. Use when you need flexible access to repository content - returns raw file data for files or paginated directory listings for directories.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + key: 'browseRepositoryPath', + group: 'sourceAndRefs', + path: 'sourceAndRefs.browseRepositoryPath', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + riskLevel: 'read', + pathFields: ['commit', 'path', 'repo_slug', 'workspace'], + queryFields: ['format', 'q', 'sort', 'max_depth'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + path: 'README.md', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_ISSUES_VOTE', + title: 'Check if user voted for issue', + description: + 'Tool to check whether the authenticated user has voted for a specific issue in a Bitbucket repository. Use when you need to verify if the current user has already voted on an issue before attempting to vote or unvote.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/issues/{issue_id}/vote', + key: 'getRepositoriesIssuesVote', + group: 'issues', + path: 'issues.getRepositoriesIssuesVote', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/issues/{issue_id}/vote', + riskLevel: 'read', + pathFields: ['issue_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account', 'issue'], + deprecated: true, + exampleInput: { + issue_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_BRANCH', + title: 'Create a branch', + description: + "Creates a new branch in a Bitbucket repository from a target commit hash; the branch name must be unique, adhere to Bitbucket's naming conventions, and not include the 'refs/heads/' prefix.", + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/refs/branches', + key: 'createBranch', + group: 'sourceAndRefs', + path: 'sourceAndRefs.createBranch', + httpMethod: 'POST', + apiPath: '/repositories/{workspace}/{repo_slug}/refs/branches', + riskLevel: 'write', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository:write'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_PULL_REQUEST', + title: 'Create a pull request', + description: + 'Creates a new pull request in a specified Bitbucket repository, ensuring the source branch exists and is distinct from the (optional) destination branch.', + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/pullrequests', + key: 'createPullRequest', + group: 'pullRequests', + path: 'pullRequests.createPullRequest', + httpMethod: 'POST', + apiPath: '/repositories/{workspace}/{repo_slug}/pullrequests', + riskLevel: 'write', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: false, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['pullrequest:write'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_ISSUE', + title: 'Create an issue', + description: + 'Creates a new issue in a Bitbucket repository, setting the authenticated user as reporter; ensures assignee (if provided) has repository access, and that any specified milestone, version, or component IDs exist.', + providerOperationId: 'POST /repositories/{workspace}/{repo_slug}/issues', + key: 'createIssue', + group: 'issues', + path: 'issues.createIssue', + httpMethod: 'POST', + apiPath: '/repositories/{workspace}/{repo_slug}/issues', + riskLevel: 'write', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['issue:write'], + deprecated: true, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_ISSUE_COMMENT', + title: 'Create an issue comment', + description: + 'Adds a new comment with markdown support to an existing Bitbucket issue.', + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/issues/{issue_id}/comments', + key: 'createIssueComment', + group: 'issues', + path: 'issues.createIssueComment', + httpMethod: 'POST', + apiPath: '/repositories/{workspace}/{repo_slug}/issues/{issue_id}/comments', + riskLevel: 'write', + pathFields: ['issue_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['issue:write'], + deprecated: true, + exampleInput: { + issue_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_REPOSITORIES_COMMIT_REPORTS_ANNOTATIONS', + title: 'Create commit report annotations', + description: + 'Adds multiple annotations to a commit report in bulk. Use when you need to add code analysis findings (vulnerabilities, code smells, bugs) to a report attached to a specific commit.', + providerOperationId: 'bulkCreateOrUpdateAnnotations', + key: 'createRepositoriesCommitReportsAnnotations', + group: 'commitsAndInsights', + path: 'commitsAndInsights.createRepositoriesCommitReportsAnnotations', + httpMethod: 'POST', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations', + riskLevel: 'write', + pathFields: ['workspace', 'repo_slug', 'commit', 'reportId'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + reportId: 'corsair-report', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_PULL_REQUEST_COMMENT', + title: 'Create pull request comment', + description: + 'Creates a new comment on a Bitbucket pull request. Supports top-level comments, threaded replies, and inline code comments. Use when providing feedback on a PR, replying to existing comments, or commenting on specific code lines.', + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments', + key: 'createPullRequestComment', + group: 'pullRequests', + path: 'pullRequests.createPullRequestComment', + httpMethod: 'POST', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments', + riskLevel: 'write', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_REPOSITORY', + title: 'Create repository', + description: + "Creates a new Bitbucket 'git' repository in a specified workspace, defaulting to the workspace's oldest project if `project_key` is not provided.", + providerOperationId: 'POST /repositories/{workspace}/{repo_slug}', + key: 'createRepository', + group: 'repositories', + path: 'repositories.createRepository', + httpMethod: 'POST', + apiPath: '/repositories/{workspace}/{repo_slug}', + riskLevel: 'write', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: false, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository:admin'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_SNIPPET_COMMENT', + title: 'Create snippet comment', + description: + 'Posts a new top-level comment or a threaded reply to an existing comment on a specified Bitbucket snippet.', + providerOperationId: 'POST /snippets/{workspace}/{encoded_id}/comments', + key: 'createSnippetComment', + group: 'snippets', + path: 'snippets.createSnippetComment', + httpMethod: 'POST', + apiPath: '/snippets/{workspace}/{encoded_id}/comments', + riskLevel: 'write', + pathFields: ['encoded_id', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['snippet'], + deprecated: false, + exampleInput: { + encoded_id: 'snippet-id', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_TEAMS_PIPELINES_CONFIG_VARIABLES', + title: 'Create team pipeline variable', + description: + 'Creates a team-level pipeline configuration variable in Bitbucket. Use when you need to add environment variables or configuration values that should be available to all pipelines within a team.', + providerOperationId: 'createPipelineVariableForTeam', + key: 'createTeamsPipelinesConfigVariables', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.createTeamsPipelinesConfigVariables', + httpMethod: 'POST', + apiPath: '/teams/{username}/pipelines_config/variables', + riskLevel: 'write', + pathFields: ['username'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: false, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['pipeline:variable'], + deprecated: true, + exampleInput: { + username: 'team', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_USERS_PIPELINES_CONFIG_VARIABLES', + title: 'Create user pipeline variable', + description: + 'Creates a user-level pipeline variable for Bitbucket pipelines. Use when you need to create account-level configuration variables that can be used across all repositories owned by the user.', + providerOperationId: 'createPipelineVariableForUser', + key: 'createUsersPipelinesConfigVariables', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.createUsersPipelinesConfigVariables', + httpMethod: 'POST', + apiPath: '/users/{selected_user}/pipelines_config/variables', + riskLevel: 'write', + pathFields: ['selected_user'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: false, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['pipeline:variable'], + deprecated: true, + exampleInput: { + selected_user: 'user', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_DELETE_COMMIT_COMMENT', + title: 'Delete commit comment', + description: + 'Permanently deletes a specific comment on a commit. Use when removing outdated, incorrect, or unwanted feedback on a commit.', + providerOperationId: + 'DELETE /repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}', + key: 'deleteCommitComment', + group: 'commitsAndInsights', + path: 'commitsAndInsights.deleteCommitComment', + httpMethod: 'DELETE', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}', + riskLevel: 'destructive', + pathFields: ['comment_id', 'commit', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'empty', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + comment_id: 1, + commit: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: null, + }, + { + code: 'BITBUCKET_DELETE_REPOSITORIES_COMMIT_REPORTS_ANNOTATIONS', + title: 'Delete commit report annotation', + description: + 'Deletes a single annotation matching the provided ID from a commit report. Use when you need to remove a specific annotation from a code analysis report.', + providerOperationId: 'deleteAnnotation', + key: 'deleteRepositoriesCommitReportsAnnotations', + group: 'commitsAndInsights', + path: 'commitsAndInsights.deleteRepositoriesCommitReportsAnnotations', + httpMethod: 'DELETE', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations/{annotationId}', + riskLevel: 'destructive', + pathFields: [ + 'workspace', + 'repo_slug', + 'commit', + 'reportId', + 'annotationId', + ], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'empty', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + reportId: 'corsair-report', + annotationId: 'annotation-1', + }, + exampleOutput: null, + }, + { + code: 'BITBUCKET_DELETE_ISSUE', + title: 'Delete issue', + description: + 'Permanently deletes a specific issue, identified by its `issue_id`, from the repository specified by `repo_slug` within the given `workspace`.', + providerOperationId: + 'DELETE /repositories/{workspace}/{repo_slug}/issues/{issue_id}', + key: 'deleteIssue', + group: 'issues', + path: 'issues.deleteIssue', + httpMethod: 'DELETE', + apiPath: '/repositories/{workspace}/{repo_slug}/issues/{issue_id}', + riskLevel: 'destructive', + pathFields: ['issue_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'empty', + mediaType: undefined, + scopes: ['issue:write'], + deprecated: true, + exampleInput: { + issue_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: null, + }, + { + code: 'BITBUCKET_DELETE_REPOSITORY', + title: 'Delete repository', + description: + 'Permanently deletes a specified Bitbucket repository; this action is irreversible and does not affect forks.', + providerOperationId: 'DELETE /repositories/{workspace}/{repo_slug}', + key: 'deleteRepository', + group: 'repositories', + path: 'repositories.deleteRepository', + httpMethod: 'DELETE', + apiPath: '/repositories/{workspace}/{repo_slug}', + riskLevel: 'destructive', + pathFields: ['repo_slug', 'workspace'], + queryFields: ['redirect_to'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'empty', + mediaType: undefined, + scopes: ['repository:delete'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: null, + }, + { + code: 'BITBUCKET_DELETE_SNIPPETS_WATCH', + title: 'Delete snippet watch', + description: + 'Stops watching a specific snippet. Use when you want to unsubscribe from notifications for a snippet.', + providerOperationId: 'DELETE /snippets/{workspace}/{encoded_id}/watch', + key: 'deleteSnippetsWatch', + group: 'snippets', + path: 'snippets.deleteSnippetsWatch', + httpMethod: 'DELETE', + apiPath: '/snippets/{workspace}/{encoded_id}/watch', + riskLevel: 'write', + pathFields: ['encoded_id', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'empty', + mediaType: undefined, + scopes: ['snippet:write'], + deprecated: false, + exampleInput: { + encoded_id: 'snippet-id', + workspace: 'workspace', + }, + exampleOutput: null, + }, + { + code: 'BITBUCKET_DELETE_USER_PIPELINE_VARIABLE', + title: 'Delete user pipeline variable', + description: + 'Permanently deletes a user-level pipeline configuration variable identified by its UUID. Use this to remove pipeline variables that are no longer needed at the account level.', + providerOperationId: 'deletePipelineVariableForUser', + key: 'deleteUserPipelineVariable', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.deleteUserPipelineVariable', + httpMethod: 'DELETE', + apiPath: + '/users/{selected_user}/pipelines_config/variables/{variable_uuid}', + riskLevel: 'destructive', + pathFields: ['selected_user', 'variable_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'empty', + mediaType: undefined, + scopes: ['pipeline:variable'], + deprecated: true, + exampleInput: { + selected_user: 'user', + variable_uuid: '{00000000-0000-4000-8000-000000000003}', + }, + exampleOutput: null, + }, + { + code: 'BITBUCKET_GET_COMMIT_BUILD_STATUS', + title: 'Get Commit Build Status', + description: + 'Get a specific build status for a commit in Bitbucket. Use when you need to check the status of a particular build/CI run for a commit.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commit/{commit}/statuses/build/{key}', + key: 'getCommitBuildStatus', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getCommitBuildStatus', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses/build/{key}', + riskLevel: 'read', + pathFields: ['commit', 'key', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + key: 'build-key', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_COMMIT_CHANGES', + title: 'Get Commit Changes', + description: + 'Tool to retrieve a page of changes made in a specified commit, showing all changed files with their change statistics (lines added/removed, status). Use when you need to enumerate files modified in a specific commit or commit range.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/diffstat/{spec}', + key: 'getCommitChanges', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getCommitChanges', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/diffstat/{spec}', + riskLevel: 'read', + pathFields: ['repo_slug', 'spec', 'workspace'], + queryFields: ['ignore_whitespace', 'merge', 'path', 'renames', 'topic'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + spec: 'main..feature', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_COMMIT_DIFF', + title: 'Get Commit Diff', + description: + 'Tool to retrieve the unified diff between two provided revisions or for a single commit in a Bitbucket repository. Use when you need to see the actual code changes in a commit or between two commits. Supports filtering by file path and various diff options.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/diff/{spec}', + key: 'getCommitDiff', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getCommitDiff', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/diff/{spec}', + riskLevel: 'read', + pathFields: ['repo_slug', 'spec', 'workspace'], + queryFields: [ + 'context', + 'path', + 'ignore_whitespace', + 'binary', + 'renames', + 'merge', + 'topic', + ], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'text', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + spec: 'main..feature', + workspace: 'workspace', + }, + exampleOutput: 'example content', + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_COMMIT_REPORTS', + title: 'Get Commit Reports', + description: + 'Tool to get reports linked to a specific commit. Use when you need to retrieve analysis results, test reports, security scans, or code coverage data associated with a commit.', + providerOperationId: 'getReportsForCommit', + key: 'getRepositoriesCommitReports', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesCommitReports', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug', 'commit'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_OPENID_CONFIGURATION', + title: 'Get OpenID configuration for OIDC in Pipelines', + description: + 'Retrieves the OpenID Connect discovery configuration for Bitbucket Pipelines OIDC. Use when integrating Bitbucket Pipelines with resource servers (AWS, GCP, Vault) using OpenID Connect authentication. Returns issuer URL, JWKS URI, and supported capabilities.', + providerOperationId: 'getOIDCConfiguration', + key: 'getOpenidConfiguration', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getOpenidConfiguration', + httpMethod: 'GET', + apiPath: + '/workspaces/{workspace}/pipelines-config/identity/oidc/.well-known/openid-configuration', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: [], + deprecated: false, + exampleInput: { + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_PULL_REQUEST', + title: 'Get Pull Request', + description: 'Get a single pull request by ID with complete details.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}', + key: 'getPullRequest', + group: 'pullRequests', + path: 'pullRequests.getPullRequest', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_PULL_REQUEST_COMMITS', + title: 'Get Pull Request Commits', + description: + 'Tool to retrieve commits for a specified pull request. Use when reviewing the commit history of a PR or analyzing changes included in a pull request.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/commits', + key: 'getPullRequestCommits', + group: 'pullRequests', + path: 'pullRequests.getPullRequestCommits', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/commits', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_PULL_REQUEST_DIFF', + title: 'Get Pull Request Diff', + description: + 'Tool to fetch the unified diff for a Bitbucket pull request (follows 302 redirect to repository diff). Use when reviewing code changes in a PR. Supports optional truncation for large diffs via max_chars parameter.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diff', + key: 'getPullRequestDiff', + group: 'pullRequests', + path: 'pullRequests.getPullRequestDiff', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diff', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'text', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: 'example content', + }, + { + code: 'BITBUCKET_GET_PULL_REQUEST_DIFFSTAT', + title: 'Get Pull Request Diffstat', + description: + 'Tool to get the diffstat for a Bitbucket pull request, showing all changed files with their change statistics (lines added/removed, status). Use when you need to enumerate files modified in a PR.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat', + key: 'getPullRequestDiffstat', + group: 'pullRequests', + path: 'pullRequests.getPullRequestDiffstat', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_MERGE_BASE', + title: 'Get Repositories Merge Base', + description: + 'Get the merge base (best common ancestor) between two commits in a Bitbucket repository. Use when you need to find the common ancestor commit between two branches or commits for comparison or merge operations.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/merge-base/{revspec}', + key: 'getRepositoriesMergeBase', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesMergeBase', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/merge-base/{revspec}', + riskLevel: 'read', + pathFields: ['repo_slug', 'revspec', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + revspec: 'main..feature', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_BRANCHING_MODEL', + title: 'Get Repository Branching Model', + description: + "Return the branching model as applied to the repository. Use when you need to understand the repository's branch workflow configuration, including development/production branches and branch type prefixes.", + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/branching-model', + key: 'getRepositoriesBranchingModel', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRepositoriesBranchingModel', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/branching-model', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_COMMIT', + title: 'Get Repository Commit', + description: + 'Tool to retrieve detailed information about a specific commit in a Bitbucket repository. Use when you need to get complete commit details including author, message, date, parents, and related links.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commit/{commit}', + key: 'getRepositoriesCommit', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesCommit', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}', + riskLevel: 'read', + pathFields: ['commit', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_ENVIRONMENTS2', + title: 'Get Repository Environment', + description: + 'Retrieve detailed information about a specific deployment environment in a Bitbucket repository. Use when you need to get environment configuration, deployment settings, or check environment properties like locks and restrictions.', + providerOperationId: 'getEnvironmentForRepository', + key: 'getRepositoriesEnvironments2', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesEnvironments2', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/environments/{environment_uuid}', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug', 'environment_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + environment_uuid: '{00000000-0000-4000-8000-000000000001}', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORY_PATCH', + title: 'Get Repository Patch', + description: + 'Tool to retrieve the git patch content for a Bitbucket repository at a specified revision or commit range. Use when you need to review code changes, generate diffs, or analyze modifications between commits. Returns raw patch in unified diff format.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/patch/{spec}', + key: 'getRepositoryPatch', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoryPatch', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/patch/{spec}', + riskLevel: 'read', + pathFields: ['repo_slug', 'spec', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'text', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + spec: 'main..feature', + workspace: 'workspace', + }, + exampleOutput: 'example content', + }, + { + code: 'BITBUCKET_GET_SSH_LATEST_KEYS', + title: 'Get SSH keys for user', + description: + 'Retrieves a paginated list of SSH keys for a specified Bitbucket user. Use when you need to view or audit SSH keys configured for a user account.', + providerOperationId: 'GET /users/{selected_user}/ssh-keys', + key: 'getSshLatestKeys', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getSshLatestKeys', + httpMethod: 'GET', + apiPath: '/users/{selected_user}/ssh-keys', + riskLevel: 'read', + pathFields: ['selected_user'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account'], + deprecated: false, + exampleInput: { + selected_user: 'user', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_WORKSPACES_PULLREQUESTS', + title: 'Get Workspace Pull Requests by User', + description: + 'Tool to get all workspace pull requests authored by a specified user. Use when you need to retrieve pull requests created by a specific user across all repositories in a workspace.', + providerOperationId: + 'GET /workspaces/{workspace}/pullrequests/{selected_user}', + key: 'getWorkspacesPullrequests', + group: 'workspacesAndProjects', + path: 'workspacesAndProjects.getWorkspacesPullrequests', + httpMethod: 'GET', + apiPath: '/workspaces/{workspace}/pullrequests/{selected_user}', + riskLevel: 'read', + pathFields: ['selected_user', 'workspace'], + queryFields: ['state'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + selected_user: 'user', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_BRANCH', + title: 'Get branch', + description: + 'Retrieves detailed information about a specific branch in a Bitbucket repository. Use when you need to get branch metadata, the commit it points to, or verify a branch exists.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/refs/branches/{name}', + key: 'getBranch', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getBranch', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/refs/branches/{name}', + riskLevel: 'read', + pathFields: ['name', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + name: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_COMMIT_COMMENT', + title: 'Get commit comment', + description: + 'Retrieves a specific comment from a commit by its ID. Use when you need to fetch details of a particular commit comment including content, author, timestamps, and inline location.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}', + key: 'getCommitComment', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getCommitComment', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}', + riskLevel: 'read', + pathFields: ['comment_id', 'commit', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + comment_id: 1, + commit: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_COMMIT_COMMENTS', + title: 'Get commit comments', + description: + 'Retrieves all comments on a specific commit in a Bitbucket repository. Returns both global and inline code comments. Use when you need to view feedback, discussions, or notes left on a specific commit.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commit/{commit}/comments', + key: 'getRepositoriesCommitComments', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesCommitComments', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}/comments', + riskLevel: 'read', + pathFields: ['commit', 'repo_slug', 'workspace'], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_COMMIT_REPORT', + title: 'Get commit report', + description: + 'Returns a single report matching the provided ID from a commit. Use when you need to retrieve details of a specific analysis report (e.g., security scan, code coverage, test results, or bug report) for a commit.', + providerOperationId: 'getReport', + key: 'getRepositoriesCommitReport', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesCommitReport', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug', 'commit', 'reportId'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + reportId: 'corsair-report', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_COMMIT_REPORTS_ANNOTATIONS', + title: 'Get commit report annotation', + description: + 'Returns a single annotation matching the provided ID from a commit report. Use when you need to retrieve details of a specific code analysis finding (e.g., vulnerability, code smell, or bug) identified in a commit.', + providerOperationId: 'getAnnotation', + key: 'getRepositoriesCommitReportsAnnotations', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesCommitReportsAnnotations', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations/{annotationId}', + riskLevel: 'read', + pathFields: [ + 'workspace', + 'repo_slug', + 'commit', + 'reportId', + 'annotationId', + ], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + reportId: 'corsair-report', + annotationId: 'annotation-1', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_COMMIT_STATUSES', + title: 'Get commit statuses', + description: + 'Returns all build statuses (e.g., CI/CD pipeline results) for a specific commit. Use when you need to check build status, verify test results, or monitor deployment pipelines for a particular commit.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commit/{commit}/statuses', + key: 'getRepositoriesCommitStatuses', + group: 'commitsAndInsights', + path: 'commitsAndInsights.getRepositoriesCommitStatuses', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses', + riskLevel: 'read', + pathFields: ['commit', 'repo_slug', 'workspace'], + queryFields: ['refname', 'q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_CURRENT_USER2', + title: 'Get current user (v2)', + description: + 'Tool to retrieve complete profile information for the currently authenticated Bitbucket user. Use when you need comprehensive user details including account_id, username, nickname, and other profile fields.', + providerOperationId: 'GET /user', + key: 'getCurrentUser2', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getCurrentUser2', + httpMethod: 'GET', + apiPath: '/user', + riskLevel: 'read', + pathFields: [], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account'], + deprecated: false, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_DEPLOYMENT_ENVIRONMENT_VARIABLES', + title: 'Get deployment environment variables', + description: + 'Retrieves deployment environment level variables for a specific Bitbucket repository environment. Use when you need to view or audit environment-specific configuration variables for deployments.', + providerOperationId: 'getDeploymentVariables', + key: 'getDeploymentEnvironmentVariables', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getDeploymentEnvironmentVariables', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/deployments_config/environments/{environment_uuid}/variables', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug', 'environment_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + environment_uuid: '{00000000-0000-4000-8000-000000000001}', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_EFFECTIVE_BRANCHING_MODEL', + title: 'Get effective branching model', + description: + "Retrieves the effective branching model for a Bitbucket repository, showing which branching model is currently applied (including any inheritance from project-level settings). Use when you need to understand the repository's branch workflow configuration, including development and production branches and branch type prefixes.", + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/effective-branching-model', + key: 'getRepositoriesEffectiveBranchingModel', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRepositoriesEffectiveBranchingModel', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/effective-branching-model', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_FILEHISTORY', + title: 'Get file commit history', + description: + 'Returns a paginated list of commits that modified the specified file. Use when you need to track file changes over time, find who modified a file, or determine when a file was created or last changed.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/filehistory/{commit}/{path}', + key: 'getRepositoriesFilehistory', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRepositoriesFilehistory', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/filehistory/{commit}/{path}', + riskLevel: 'read', + pathFields: ['commit', 'path', 'repo_slug', 'workspace'], + queryFields: ['renames', 'q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + path: 'README.md', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_FILE_FROM_REPOSITORY', + title: 'Get file from repository', + description: + "Retrieves a specific file's content from a Bitbucket repository at a given commit (hash, branch, or tag), failing if the file path is invalid for that commit.", + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + key: 'getFileFromRepository', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getFileFromRepository', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + riskLevel: 'read', + pathFields: ['commit', 'path', 'repo_slug', 'workspace'], + queryFields: ['format', 'q', 'sort', 'max_depth'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + path: 'README.md', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_HOOK_EVENTS', + title: 'Get hook events', + description: + 'Retrieves a paginated list of all valid webhook events for a specified entity type (repository or workspace). Use when you need to discover available webhook event types for subscription or webhook configuration.', + providerOperationId: 'GET /hook_events/{subject_type}', + key: 'getHookEvents', + group: 'searchAndDiscovery', + path: 'searchAndDiscovery.getHookEvents', + httpMethod: 'GET', + apiPath: '/hook_events/{subject_type}', + riskLevel: 'read', + pathFields: ['subject_type'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: [], + deprecated: false, + exampleInput: { + subject_type: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES_STEPS', + title: 'Get pipeline steps', + description: + 'Retrieves all steps for a given pipeline. Use when you need to inspect the individual steps of a pipeline execution, including their state, duration, and commands.', + providerOperationId: 'getPipelineStepsForRepository', + key: 'getRepositoriesPipelinesSteps', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelinesSteps', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug', 'pipeline_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + pipeline_uuid: '{00000000-0000-4000-8000-000000000002}', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_PROJECTS_REPOS', + title: 'Get project repositories', + description: + 'Retrieves repositories from a project in a Bitbucket workspace. Use when you need to list all repositories belonging to a specific project key within a workspace.', + providerOperationId: 'GET /repositories/{workspace}', + key: 'getProjectsRepos', + group: 'workspacesAndProjects', + path: 'workspacesAndProjects.getProjectsRepos', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: ['role', 'q', 'sort'], + fixedPathValues: {}, + projectFilterField: 'project_key', + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + project_key: 'PROJECT', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_PULL_REQUEST_COMMENT', + title: 'Get pull request comment', + description: + 'Tool to retrieve a specific comment from a pull request by its ID. Use when you need to fetch details of a particular pull request comment including content, author, timestamps, and inline location.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments/{comment_id}', + key: 'getPullRequestComment', + group: 'pullRequests', + path: 'pullRequests.getPullRequestComment', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments/{comment_id}', + riskLevel: 'read', + pathFields: ['comment_id', 'pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + comment_id: 1, + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PULLREQUESTS_COMMENTS', + title: 'Get pull request comments', + description: + 'Retrieves a paginated list of comments on a specific pull request in a Bitbucket repository. Returns global, inline, and threaded comments. Use when you need to view feedback, discussions, or reviews left on a pull request.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments', + key: 'getRepositoriesPullrequestsComments', + group: 'pullRequests', + path: 'pullRequests.getRepositoriesPullrequestsComments', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PULLREQUESTS_STATUSES', + title: 'Get pull request statuses', + description: + 'Returns all build statuses (e.g., CI/CD pipeline results) for a specific pull request. Use when you need to check build status, verify test results, or monitor deployment pipelines for a pull request.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/statuses', + key: 'getRepositoriesPullrequestsStatuses', + group: 'pullRequests', + path: 'pullRequests.getRepositoriesPullrequestsStatuses', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/statuses', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PULLREQUESTS_ACTIVITY', + title: 'Get pull requests activity log', + description: + 'Get paginated activity log for all pull requests in a repository. Returns comments, updates, approvals, and request changes. Use when you need to track pull request activity history.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/activity', + key: 'getRepositoriesPullrequestsActivity', + group: 'pullRequests', + path: 'pullRequests.getRepositoriesPullrequestsActivity', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pullrequests/activity', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_RAW_FILE_CONTENT', + title: 'Get raw file content', + description: + 'Tool to retrieve the raw content of a file from a Bitbucket repository at a specified commit, branch, or tag. Use when you need to read file contents from a specific revision.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + key: 'getRawFileContent', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRawFileContent', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + riskLevel: 'read', + pathFields: ['commit', 'path', 'repo_slug', 'workspace'], + queryFields: ['format', 'q', 'sort', 'max_depth'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'text', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + path: 'README.md', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: 'example content', + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_SRC', + title: 'Get repositories src', + description: + "Lists the contents of the root directory on the repository's main branch without needing to specify a commit or branch. This endpoint redirects to the main branch automatically.", + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/src', + key: 'getRepositoriesSrc', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRepositoriesSrc', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/src', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: ['format'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORY', + title: 'Get repository', + description: + 'Retrieves detailed information about a specific repository in a Bitbucket workspace. Use when you need to get repository metadata, settings, or details.', + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}', + key: 'getRepository', + group: 'repositories', + path: 'repositories.getRepository', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES_CONFIG_SSH_KNOWN_HOSTS', + title: 'Get repository SSH known hosts', + description: + 'Retrieves repository-level SSH known hosts configured for Bitbucket Pipelines. Use when you need to list or verify SSH known hosts that Pipelines can connect to during builds.', + providerOperationId: 'getRepositoryPipelineKnownHosts', + key: 'getRepositoriesPipelinesConfigSshKnownHosts', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelinesConfigSshKnownHosts', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pipelines_config/ssh/known_hosts', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES_CONFIG_RUNNERS', + title: 'Get repository pipeline runners', + description: + "Retrieves the list of self-hosted runners configured for a repository's pipelines. Use when you need to view available runners for pipeline execution.", + providerOperationId: 'getRepositoryRunners', + key: 'getRepositoriesPipelinesConfigRunners', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelinesConfigRunners', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pipelines-config/runners', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['runner'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES_CONFIG_SCHEDULES', + title: 'Get repository pipeline schedules', + description: + 'Retrieves configured pipeline schedules for a Bitbucket repository. Use when you need to view scheduled pipeline runs and their cron patterns.', + providerOperationId: 'getRepositoryPipelineSchedules', + key: 'getRepositoriesPipelinesConfigSchedules', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelinesConfigSchedules', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pipelines_config/schedules', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES_CONFIG_VARIABLES', + title: 'Get repository pipeline variables', + description: + 'Retrieves repository-level pipeline variables for a specific Bitbucket repository. Use when you need to view or audit pipeline configuration variables that are scoped to a repository.', + providerOperationId: 'getRepositoryPipelineVariables', + key: 'getRepositoriesPipelinesConfigVariables', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelinesConfigVariables', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pipelines_config/variables', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES_CONFIG_CACHES', + title: 'Get repository pipelines caches', + description: + 'Retrieves the repository pipelines caches. Use when you need to list all caches configured for Bitbucket Pipelines in a specific repository.', + providerOperationId: 'getRepositoryPipelineCaches', + key: 'getRepositoriesPipelinesConfigCaches', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelinesConfigCaches', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pipelines-config/caches', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_REFS', + title: 'Get repository refs', + description: + 'Returns the branches and tags in the repository. Use when you need to list all refs (both branches and tags) in a single API call with optional filtering by type, name pattern, or commit hash.', + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/refs', + key: 'getRepositoriesRefs', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRepositoriesRefs', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/refs', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_WATCHERS', + title: 'Get repository watchers', + description: + 'Retrieves a paginated list of all the watchers on the specified repository. Use when you need to see who is watching a particular repository.', + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/watchers', + key: 'getRepositoriesWatchers', + group: 'repositories', + path: 'repositories.getRepositoriesWatchers', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/watchers', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_SNIPPET', + title: 'Get snippet', + description: + 'Retrieves a specific Bitbucket snippet by its encoded ID from an existing workspace, returning its metadata and file structure.', + providerOperationId: 'GET /snippets/{workspace}/{encoded_id}', + key: 'getSnippet', + group: 'snippets', + path: 'snippets.getSnippet', + httpMethod: 'GET', + apiPath: '/snippets/{workspace}/{encoded_id}', + riskLevel: 'read', + pathFields: ['encoded_id', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['snippet'], + deprecated: false, + exampleInput: { + encoded_id: 'snippet-id', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_SNIPPETS_WATCH', + title: 'Get snippet watch status', + description: + 'Checks if the current user is watching a specific snippet. Use when you need to verify watch status for a snippet.', + providerOperationId: 'GET /snippets/{workspace}/{encoded_id}/watch', + key: 'getSnippetsWatch', + group: 'snippets', + path: 'snippets.getSnippetsWatch', + httpMethod: 'GET', + apiPath: '/snippets/{workspace}/{encoded_id}/watch', + riskLevel: 'read', + pathFields: ['encoded_id', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['snippet'], + deprecated: false, + exampleInput: { + encoded_id: 'snippet-id', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_PIPELINES2', + title: 'Get specific pipeline', + description: + 'Retrieve a specified pipeline from a Bitbucket repository. Use when you need to get detailed information about a specific pipeline execution including its status, build number, and results.', + providerOperationId: 'getPipelineForRepository', + key: 'getRepositoriesPipelines2', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.getRepositoriesPipelines2', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug', 'pipeline_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + pipeline_uuid: '{00000000-0000-4000-8000-000000000002}', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_REPOSITORIES_REFS_TAGS', + title: 'Get tag', + description: + 'Retrieves detailed information about a specific tag in a Bitbucket repository. Use when you need to get tag metadata, target commit details, or verify a tag exists.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/refs/tags/{name}', + key: 'getRepositoriesRefsTags', + group: 'sourceAndRefs', + path: 'sourceAndRefs.getRepositoriesRefsTags', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/refs/tags/{name}', + riskLevel: 'read', + pathFields: ['name', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + name: 'main', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_USER', + title: 'Get user', + description: + 'Retrieves public profile information for a specific Bitbucket user by username or UUID. Use when you need to get user details like display name, avatar, creation date, and links to related resources.', + providerOperationId: 'GET /users/{selected_user}', + key: 'getUser', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getUser', + httpMethod: 'GET', + apiPath: '/users/{selected_user}', + riskLevel: 'read', + pathFields: ['selected_user'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: [], + deprecated: false, + exampleInput: { + selected_user: 'user', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_USER_EMAILS2', + title: 'Get user email details', + description: + 'Retrieves details about a specific email address for the authenticated user. Use when you need to check if an email is primary, confirmed, or verify email ownership.', + providerOperationId: 'GET /user/emails/{email}', + key: 'getUserEmails2', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getUserEmails2', + httpMethod: 'GET', + apiPath: '/user/emails/{email}', + riskLevel: 'read', + pathFields: ['email'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['email'], + deprecated: false, + exampleInput: { + email: 'dev@example.invalid', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_USER_EMAILS', + title: 'Get user emails', + description: + "Returns all the authenticated user's email addresses, both confirmed and unconfirmed. Use when you need to retrieve all email addresses associated with the current user's account.", + providerOperationId: 'GET /user/emails', + key: 'getUserEmails', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getUserEmails', + httpMethod: 'GET', + apiPath: '/user/emails', + riskLevel: 'read', + pathFields: [], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['email'], + deprecated: false, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_USER_PERMISSIONS_REPOSITORIES', + title: 'Get user permissions for repositories', + description: + 'Returns an object for each repository the caller has explicit access to, including their permission level. Use when you need to discover which repositories the authenticated user can access and their specific permissions.', + providerOperationId: 'GET /user/permissions/repositories', + key: 'getUserPermissionsRepositories', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getUserPermissionsRepositories', + httpMethod: 'GET', + apiPath: '/user/permissions/repositories', + riskLevel: 'read', + pathFields: [], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account', 'repository'], + deprecated: true, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_USER_PERMISSIONS_WORKSPACES', + title: 'Get user permissions for workspaces', + description: + 'Retrieves workspace memberships and permission levels for the authenticated user. Returns an object for each workspace the caller is a member of, along with their effective role (highest privilege level). Use when you need to determine which workspaces a user can access and their permission level in each.', + providerOperationId: 'GET /user/permissions/workspaces', + key: 'getUserPermissionsWorkspaces', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getUserPermissionsWorkspaces', + httpMethod: 'GET', + apiPath: '/user/permissions/workspaces', + riskLevel: 'read', + pathFields: [], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account'], + deprecated: true, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_USER_WORKSPACES', + title: 'Get user workspaces', + description: + 'Tool to retrieve all workspaces accessible to the authenticated user. Use when you need to list workspaces the current user can access, optionally filtered by workspace slug or permission level.', + providerOperationId: 'GET /user/workspaces', + key: 'getUserWorkspaces', + group: 'usersAndPermissions', + path: 'usersAndPermissions.getUserWorkspaces', + httpMethod: 'GET', + apiPath: '/user/workspaces', + riskLevel: 'read', + pathFields: [], + queryFields: ['sort', 'administrator'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account'], + deprecated: false, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_WORKSPACE', + title: 'Get workspace', + description: + 'Retrieves detailed information about a specific Bitbucket workspace. Use when you need to get workspace metadata, settings, or details.', + providerOperationId: 'GET /workspaces/{workspace}', + key: 'getWorkspace', + group: 'workspacesAndProjects', + path: 'workspacesAndProjects.getWorkspace', + httpMethod: 'GET', + apiPath: '/workspaces/{workspace}', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: [], + deprecated: false, + exampleInput: { + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_REPOSITORIES', + title: 'List all public repositories', + description: + 'Retrieves a paginated list of all public repositories on Bitbucket. Use when you need to discover or search across public repositories, optionally filtered by role, query string, creation date, or sorted by various fields.', + providerOperationId: 'GET /repositories', + key: 'listRepositories', + group: 'repositories', + path: 'repositories.listRepositories', + httpMethod: 'GET', + apiPath: '/repositories', + riskLevel: 'read', + pathFields: [], + queryFields: ['after', 'role', 'q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: true, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_BRANCHES', + title: 'List branches', + description: + 'Lists branches in a Bitbucket repository with optional server-side filtering by name pattern (BBQL) and sorting. Use when you need to discover available branches, search for specific branch patterns, or navigate repository branch structure.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/refs/branches', + key: 'listBranches', + group: 'sourceAndRefs', + path: 'sourceAndRefs.listBranches', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/refs/branches', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_COMMITS', + title: 'List commits', + description: + 'Tool to retrieve a page of commits from a Bitbucket repository. Returns commits in reverse chronological order (newest first), similar to git log. Use when you need to browse commit history, filter commits by branch/tag, or restrict to commits affecting a specific path.', + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/commits', + key: 'listCommits', + group: 'commitsAndInsights', + path: 'commitsAndInsights.listCommits', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commits', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_COMMITS_FROM_REVISION', + title: 'List commits from revision', + description: + 'Tool to list commits starting from a specific revision in a Bitbucket repository. Commits are paginated and returned in reverse chronological order. Use when you need to retrieve commit history from a specific commit, branch, or tag.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commits/{revision}', + key: 'listCommitsFromRevision', + group: 'commitsAndInsights', + path: 'commitsAndInsights.listCommitsFromRevision', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commits/{revision}', + riskLevel: 'read', + pathFields: ['repo_slug', 'revision', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + revision: 'main', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_CREATE_REPOSITORIES_COMMITS2', + title: 'List commits from revision (POST)', + description: + 'Tool to list commits from a revision using POST method. Identical to GET endpoint but allows sending include/exclude parameters in request body to avoid URL length limits. Use when include/exclude parameters are too long for query strings.', + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/commits/{revision}', + key: 'createRepositoriesCommits2', + group: 'commitsAndInsights', + path: 'commitsAndInsights.createRepositoriesCommits2', + httpMethod: 'POST', + apiPath: '/repositories/{workspace}/{repo_slug}/commits/{revision}', + riskLevel: 'write', + pathFields: ['repo_slug', 'revision', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: false, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + revision: 'main', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_COMMITS_ON_MASTER', + title: 'List commits on master', + description: + 'Lists commits on the master branch of a Bitbucket repository. Use when you need to retrieve the commit history for the master branch, including commit messages, authors, dates, and parent relationships.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/commits/{revision}', + key: 'listCommitsOnMaster', + group: 'commitsAndInsights', + path: 'commitsAndInsights.listCommitsOnMaster', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/commits/{revision}', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: { + revision: 'master', + }, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_DEPLOYMENTS', + title: 'List deployments', + description: + 'Lists deployments for a specified Bitbucket repository. Use when you need to view deployment history and status across environments.', + providerOperationId: 'getDeploymentsForRepository', + key: 'listDeployments', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.listDeployments', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/deployments', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_ISSUES', + title: 'List issues in a repository', + description: + 'Lists issues in a Bitbucket repository with optional filtering by state, priority, kind, or assignee. Use when you need to discover issue IDs or get an overview of repository issues.', + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/issues', + key: 'listIssues', + group: 'issues', + path: 'issues.listIssues', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/issues', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['issue'], + deprecated: true, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_PIPELINES', + title: 'List pipelines', + description: + 'Tool to find pipelines in a Bitbucket repository. Returns pipeline metadata including state, trigger, and duration. Use when you need to browse pipeline history or check pipeline status.', + providerOperationId: 'getPipelinesForRepository', + key: 'listPipelines', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.listPipelines', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pipelines', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [ + 'creator.uuid', + 'target.ref_type', + 'target.ref_name', + 'target.branch', + 'target.commit.hash', + 'target.selector.pattern', + 'target.selector.type', + 'created_on', + 'trigger_type', + 'status', + 'sort', + 'page', + 'pagelen', + ], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_PULL_REQUEST_TASKS', + title: 'List pull request tasks', + description: + 'Lists all tasks associated with a pull request in a Bitbucket repository. Use when you need to view or track tasks on a PR.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/tasks', + key: 'listPullRequestTasks', + group: 'pullRequests', + path: 'pullRequests.listPullRequestTasks', + httpMethod: 'GET', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/tasks', + riskLevel: 'read', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: ['q', 'sort', 'pagelen'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_PULL_REQUESTS', + title: 'List pull requests', + description: + 'Lists pull requests in a specified, accessible Bitbucket repository, optionally filtering by state (OPEN, MERGED, DECLINED).', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/pullrequests', + key: 'listPullRequests', + group: 'pullRequests', + path: 'pullRequests.listPullRequests', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/pullrequests', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: ['state'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_REPOSITORIES_IN_WORKSPACE', + title: 'List repositories in workspace', + description: + 'Lists repositories in a specified Bitbucket workspace, accessible to the authenticated user, with options to filter by role or query string, and sort results. Responses are paginated; iterate using the `next` field in each response until it is absent to retrieve all repositories.', + providerOperationId: 'GET /repositories/{workspace}', + key: 'listRepositoriesInWorkspace', + group: 'repositories', + path: 'repositories.listRepositoriesInWorkspace', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: ['role', 'q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_REPOSITORIES_ENVIRONMENTS', + title: 'List repository environments', + description: + 'List all deployment environments configured for a Bitbucket repository. Use when you need to view available environments for deployments, check environment configurations, or select an environment for deployment operations.', + providerOperationId: 'getEnvironmentsForRepository', + key: 'listRepositoriesEnvironments', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.listRepositoriesEnvironments', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/environments', + riskLevel: 'read', + pathFields: ['workspace', 'repo_slug'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pipeline'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_REPOSITORY_PATHS', + title: 'List repository paths', + description: + 'Lists file and directory entries under a repository path at a given revision, with optional breadth-first recursion via max_depth for repository traversal and scanning. Fails if the path points to a file rather than a directory.', + providerOperationId: + 'GET /repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + key: 'listRepositoryPaths', + group: 'sourceAndRefs', + path: 'sourceAndRefs.listRepositoryPaths', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', + riskLevel: 'read', + pathFields: ['commit', 'path', 'repo_slug', 'workspace'], + queryFields: ['format', 'q', 'sort', 'max_depth'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + commit: 'main', + path: 'README.md', + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_SNIPPETS', + title: 'List snippets', + description: + 'Returns all snippets accessible to the authenticated user. Use when you need to discover or list snippets, optionally filtered by role (owner, contributor, or member).', + providerOperationId: 'GET /snippets', + key: 'listSnippets', + group: 'snippets', + path: 'snippets.listSnippets', + httpMethod: 'GET', + apiPath: '/snippets', + riskLevel: 'read', + pathFields: [], + queryFields: ['role'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['snippet'], + deprecated: true, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_TAGS', + title: 'List tags', + description: + 'Lists tags in a Bitbucket repository with optional server-side filtering by name pattern or commit hash (BBQL) and sorting. Use when you need to discover available tags, search for specific tag patterns, or find tags pointing to specific commits.', + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/refs/tags', + key: 'listTags', + group: 'sourceAndRefs', + path: 'sourceAndRefs.listTags', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/refs/tags', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: ['q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: false, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_VERSIONS', + title: 'List versions', + description: + "Lists versions (milestones) in a Bitbucket repository's issue tracker. Use when you need to discover available versions for associating with issues, or to retrieve version IDs for use with create_issue.", + providerOperationId: 'GET /repositories/{workspace}/{repo_slug}/versions', + key: 'listVersions', + group: 'issues', + path: 'issues.listVersions', + httpMethod: 'GET', + apiPath: '/repositories/{workspace}/{repo_slug}/versions', + riskLevel: 'read', + pathFields: ['repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['issue'], + deprecated: true, + exampleInput: { + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_WORKSPACE_MEMBERS', + title: 'List workspace members', + description: + 'Lists all members of a specified Bitbucket workspace; the workspace must exist.', + providerOperationId: 'GET /workspaces/{workspace}/members', + key: 'listWorkspaceMembers', + group: 'workspacesAndProjects', + path: 'workspacesAndProjects.listWorkspaceMembers', + httpMethod: 'GET', + apiPath: '/workspaces/{workspace}/members', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_WORKSPACE_PROJECTS', + title: 'List workspace projects', + description: + 'Lists projects in a specified Bitbucket workspace. Use when you need to retrieve all projects belonging to a workspace.', + providerOperationId: 'GET /workspaces/{workspace}/projects', + key: 'listWorkspaceProjects', + group: 'workspacesAndProjects', + path: 'workspacesAndProjects.listWorkspaceProjects', + httpMethod: 'GET', + apiPath: '/workspaces/{workspace}/projects', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['project'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_LIST_WORKSPACES', + title: 'List workspaces', + description: + 'Lists Bitbucket workspaces accessible to the authenticated user, optionally filtered and sorted. Results are paginated; follow the `next` field in each response to retrieve subsequent pages until `next` is absent. When multiple workspaces are returned, verify the correct `slug` or UUID before passing to downstream tools.', + providerOperationId: 'GET /workspaces', + key: 'listWorkspaces', + group: 'workspacesAndProjects', + path: 'workspacesAndProjects.listWorkspaces', + httpMethod: 'GET', + apiPath: '/workspaces', + riskLevel: 'read', + pathFields: [], + queryFields: ['role', 'q', 'sort'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['account'], + deprecated: true, + exampleInput: {}, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_REQUEST_PULL_REQUEST_CHANGES', + title: 'Request Pull Request Changes', + description: + 'Tool to request changes on a pull request as the authenticated user. Use when you need to formally request changes in a pull request review process, indicating the PR needs modifications before approval.', + providerOperationId: + 'POST /repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/request-changes', + key: 'requestPullRequestChanges', + group: 'pullRequests', + path: 'pullRequests.requestPullRequestChanges', + httpMethod: 'POST', + apiPath: + '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/request-changes', + riskLevel: 'write', + pathFields: ['pull_request_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['pullrequest:write'], + deprecated: false, + exampleInput: { + pull_request_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_SEARCH_TEAM_CODE', + title: 'Search code in team repositories', + description: + 'Search for code in repositories of a specified team. Matches can occur in file content or paths. Note: Teams endpoints were deprecated in Oct 2020; use workspace search endpoints for new integrations.', + providerOperationId: 'searchTeam', + key: 'searchTeamCode', + group: 'searchAndDiscovery', + path: 'searchAndDiscovery.searchTeamCode', + httpMethod: 'GET', + apiPath: '/teams/{username}/search/code', + riskLevel: 'read', + pathFields: ['username'], + queryFields: ['search_query', 'page', 'pagelen'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: true, + exampleInput: { + username: 'team', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_SEARCH_USER_REPOSITORIES_CODE', + title: 'Search code in user repositories', + description: + 'Tool to search for code in the repositories of a specified user. Use when you need to find specific code patterns, functions, or text across all repositories owned by a user.', + providerOperationId: 'searchAccount', + key: 'searchUserRepositoriesCode', + group: 'searchAndDiscovery', + path: 'searchAndDiscovery.searchUserRepositoriesCode', + httpMethod: 'GET', + apiPath: '/users/{selected_user}/search/code', + riskLevel: 'read', + pathFields: ['selected_user'], + queryFields: ['search_query', 'page', 'pagelen'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: true, + exampleInput: { + selected_user: 'user', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_GET_WORKSPACES_SEARCH_CODE', + title: 'Search code in workspace', + description: + 'Tool to search for code in the repositories of the specified workspace. Use when you need to find specific code patterns, function definitions, or text across all repositories in a workspace.', + providerOperationId: 'searchWorkspace', + key: 'getWorkspacesSearchCode', + group: 'searchAndDiscovery', + path: 'searchAndDiscovery.getWorkspacesSearchCode', + httpMethod: 'GET', + apiPath: '/workspaces/{workspace}/search/code', + riskLevel: 'read', + pathFields: ['workspace'], + queryFields: ['search_query', 'page', 'pagelen'], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['repository'], + deprecated: true, + exampleInput: { + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_UPDATE_ISSUE', + title: 'Update an issue', + description: + 'Updates an existing issue in a Bitbucket repository by modifying specified attributes; requires `workspace`, `repo_slug`, `issue_id`, and at least one attribute to update.', + providerOperationId: + 'PUT /repositories/{workspace}/{repo_slug}/issues/{issue_id}', + key: 'updateIssue', + group: 'issues', + path: 'issues.updateIssue', + httpMethod: 'PUT', + apiPath: '/repositories/{workspace}/{repo_slug}/issues/{issue_id}', + riskLevel: 'write', + pathFields: ['issue_id', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: false, + bodyRequired: false, + responseKind: 'json', + mediaType: undefined, + scopes: ['issue:write'], + deprecated: true, + exampleInput: { + issue_id: 1, + repo_slug: 'repository', + workspace: 'workspace', + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_UPDATE_REPOSITORIES_COMMIT_COMMENTS', + title: 'Update commit comment', + description: + "Updates the contents of a comment on a commit. Use when you need to modify an existing comment's text on a commit.", + providerOperationId: + 'PUT /repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}', + key: 'updateRepositoriesCommitComments', + group: 'commitsAndInsights', + path: 'commitsAndInsights.updateRepositoriesCommitComments', + httpMethod: 'PUT', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}', + riskLevel: 'write', + pathFields: ['comment_id', 'commit', 'repo_slug', 'workspace'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository'], + deprecated: false, + exampleInput: { + comment_id: 1, + commit: 'main', + repo_slug: 'repository', + workspace: 'workspace', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_UPDATE_INSIGHTS_PROJECTS_REPOS_COMMITS_REPORTS', + title: 'Update commit insight report', + description: + "Create or update an insight report for a commit. Creates a new report if it doesn't exist, or replaces the existing one if a report already exists for the given repository, commit, and report key. Note: replacing an existing report will be rejected if the authenticated user was not the creator of the specified report.", + providerOperationId: 'createOrUpdateReport', + key: 'updateInsightsProjectsReposCommitsReports', + group: 'commitsAndInsights', + path: 'commitsAndInsights.updateInsightsProjectsReposCommitsReports', + httpMethod: 'PUT', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}', + riskLevel: 'write', + pathFields: ['workspace', 'repo_slug', 'commit', 'reportId'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + reportId: 'corsair-report', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_UPDATE_REPOSITORIES_COMMIT_REPORTS_ANNOTATIONS', + title: 'Update commit report annotation', + description: + 'Creates or updates an individual annotation for a commit report. Use when you need to add or modify code analysis findings (vulnerabilities, code smells, or bugs) identified in a commit.', + providerOperationId: 'createOrUpdateAnnotation', + key: 'updateRepositoriesCommitReportsAnnotations', + group: 'commitsAndInsights', + path: 'commitsAndInsights.updateRepositoriesCommitReportsAnnotations', + httpMethod: 'PUT', + apiPath: + '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations/{annotationId}', + riskLevel: 'write', + pathFields: [ + 'workspace', + 'repo_slug', + 'commit', + 'reportId', + 'annotationId', + ], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['repository'], + deprecated: false, + exampleInput: { + workspace: 'workspace', + repo_slug: 'repository', + commit: 'main', + reportId: 'corsair-report', + annotationId: 'annotation-1', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_UPDATE_TEAMS_PIPELINES_CONFIG_VARIABLES', + title: 'Update team pipeline variable', + description: + 'Updates a team-level pipeline configuration variable in Bitbucket. Use when you need to modify existing environment variables or configuration values that are available to all pipelines within a team.', + providerOperationId: 'updatePipelineVariableForTeam', + key: 'updateTeamsPipelinesConfigVariables', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.updateTeamsPipelinesConfigVariables', + httpMethod: 'PUT', + apiPath: '/teams/{username}/pipelines_config/variables/{variable_uuid}', + riskLevel: 'write', + pathFields: ['username', 'variable_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['pipeline:variable'], + deprecated: true, + exampleInput: { + username: 'team', + variable_uuid: '{00000000-0000-4000-8000-000000000003}', + body: {}, + }, + exampleOutput: {}, + }, + { + code: 'BITBUCKET_UPDATE_USERS_PIPELINES_CONFIG_VARIABLES', + title: 'Update user pipeline variable', + description: + 'Updates a user-level pipeline variable for Bitbucket pipelines. Use when you need to modify account-level configuration variables such as changing the value, key name, or security status.', + providerOperationId: 'updatePipelineVariableForUser', + key: 'updateUsersPipelinesConfigVariables', + group: 'pipelinesAndDeployments', + path: 'pipelinesAndDeployments.updateUsersPipelinesConfigVariables', + httpMethod: 'PUT', + apiPath: + '/users/{selected_user}/pipelines_config/variables/{variable_uuid}', + riskLevel: 'write', + pathFields: ['selected_user', 'variable_uuid'], + queryFields: [], + fixedPathValues: {}, + projectFilterField: undefined, + acceptsBody: true, + bodyRequired: true, + responseKind: 'json', + mediaType: 'application/json', + scopes: ['pipeline:variable'], + deprecated: true, + exampleInput: { + selected_user: 'user', + variable_uuid: '{00000000-0000-4000-8000-000000000003}', + body: {}, + }, + exampleOutput: {}, + }, +] as const; + +export type BitbucketOperation = (typeof bitbucketOperationCatalog)[number]; +export type BitbucketEndpointKey = BitbucketOperation['key']; +export const bitbucketOperationByKey = Object.fromEntries( + bitbucketOperationCatalog.map((operation) => [operation.key, operation]), +) as { [K in BitbucketEndpointKey]: Extract }; +export const bitbucketReadOperationPaths = new Set( + bitbucketOperationCatalog + .filter((operation) => operation.riskLevel === 'read') + .map((operation) => operation.path), +); diff --git a/packages/bitbucket/endpoints/types.ts b/packages/bitbucket/endpoints/types.ts new file mode 100644 index 000000000..5da2bc4ac --- /dev/null +++ b/packages/bitbucket/endpoints/types.ts @@ -0,0 +1,914 @@ +import { z } from 'zod'; + +export const BitbucketRequestBodySchema = z.union([ + z.object({}).loose(), + z.array(z.object({}).loose()), +]); +export const BitbucketResponseSchema = z.union([ + z.object({}).loose(), + z.array(z.unknown()), + z.string(), + z.number(), + z.boolean(), + z.null(), +]); + +export const BitbucketEndpointInputSchemas = { + approvePullRequest: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + browseRepositoryPath: z + .object({ + commit: z.string(), + path: z.string(), + repo_slug: z.string(), + workspace: z.string(), + format: z.enum(['meta', 'rendered']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + max_depth: z.number().int().optional(), + }) + .strict(), + getRepositoriesIssuesVote: z + .object({ + issue_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + createBranch: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + createPullRequest: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema.optional(), + }) + .strict(), + createIssue: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + createIssueComment: z + .object({ + issue_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + createRepositoriesCommitReportsAnnotations: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + reportId: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + createPullRequestComment: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + createRepository: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema.optional(), + }) + .strict(), + createSnippetComment: z + .object({ + encoded_id: z.union([z.string(), z.number().int()]), + workspace: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + createTeamsPipelinesConfigVariables: z + .object({ + username: z.string(), + body: BitbucketRequestBodySchema.optional(), + }) + .strict(), + createUsersPipelinesConfigVariables: z + .object({ + selected_user: z.string(), + body: BitbucketRequestBodySchema.optional(), + }) + .strict(), + deleteCommitComment: z + .object({ + comment_id: z.union([z.string(), z.number().int()]), + commit: z.string(), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + deleteRepositoriesCommitReportsAnnotations: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + reportId: z.string(), + annotationId: z.string(), + }) + .strict(), + deleteIssue: z + .object({ + issue_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + deleteRepository: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + redirect_to: z.string().optional(), + }) + .strict(), + deleteSnippetsWatch: z + .object({ + encoded_id: z.union([z.string(), z.number().int()]), + workspace: z.string(), + }) + .strict(), + deleteUserPipelineVariable: z + .object({ + selected_user: z.string(), + variable_uuid: z.string(), + }) + .strict(), + getCommitBuildStatus: z + .object({ + commit: z.string(), + key: z.string(), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getCommitChanges: z + .object({ + repo_slug: z.string(), + spec: z.string(), + workspace: z.string(), + ignore_whitespace: z.boolean().optional(), + merge: z.boolean().optional(), + path: z.string().optional(), + renames: z.boolean().optional(), + topic: z.boolean().optional(), + }) + .strict(), + getCommitDiff: z + .object({ + repo_slug: z.string(), + spec: z.string(), + workspace: z.string(), + context: z.number().int().optional(), + path: z.string().optional(), + ignore_whitespace: z.boolean().optional(), + binary: z.boolean().optional(), + renames: z.boolean().optional(), + merge: z.boolean().optional(), + topic: z.boolean().optional(), + }) + .strict(), + getRepositoriesCommitReports: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + }) + .strict(), + getOpenidConfiguration: z + .object({ + workspace: z.string(), + }) + .strict(), + getPullRequest: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getPullRequestCommits: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getPullRequestDiff: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getPullRequestDiffstat: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesMergeBase: z + .object({ + repo_slug: z.string(), + revspec: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesBranchingModel: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesCommit: z + .object({ + commit: z.string(), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesEnvironments2: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + environment_uuid: z.string(), + }) + .strict(), + getRepositoryPatch: z + .object({ + repo_slug: z.string(), + spec: z.string(), + workspace: z.string(), + }) + .strict(), + getSshLatestKeys: z + .object({ + selected_user: z.string(), + }) + .strict(), + getWorkspacesPullrequests: z + .object({ + selected_user: z.string(), + workspace: z.string(), + state: z.enum(['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED']).optional(), + }) + .strict(), + getBranch: z + .object({ + name: z.string(), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getCommitComment: z + .object({ + comment_id: z.union([z.string(), z.number().int()]), + commit: z.string(), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesCommitComments: z + .object({ + commit: z.string(), + repo_slug: z.string(), + workspace: z.string(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getRepositoriesCommitReport: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + reportId: z.string(), + }) + .strict(), + getRepositoriesCommitReportsAnnotations: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + reportId: z.string(), + annotationId: z.string(), + }) + .strict(), + getRepositoriesCommitStatuses: z + .object({ + commit: z.string(), + repo_slug: z.string(), + workspace: z.string(), + refname: z.string().optional(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getCurrentUser2: z.object({}).strict(), + getDeploymentEnvironmentVariables: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + environment_uuid: z.string(), + }) + .strict(), + getRepositoriesEffectiveBranchingModel: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesFilehistory: z + .object({ + commit: z.string(), + path: z.string(), + repo_slug: z.string(), + workspace: z.string(), + renames: z.string().optional(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getFileFromRepository: z + .object({ + commit: z.string(), + path: z.string(), + repo_slug: z.string(), + workspace: z.string(), + format: z.enum(['meta', 'rendered']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + max_depth: z.number().int().optional(), + }) + .strict(), + getHookEvents: z + .object({ + subject_type: z.enum(['repository', 'workspace']), + }) + .strict(), + getRepositoriesPipelinesSteps: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + pipeline_uuid: z.string(), + }) + .strict(), + getProjectsRepos: z + .object({ + workspace: z.string(), + role: z.enum(['admin', 'contributor', 'member', 'owner']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + project_key: z.string().min(1), + }) + .strict(), + getPullRequestComment: z + .object({ + comment_id: z.union([z.string(), z.number().int()]), + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesPullrequestsComments: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesPullrequestsStatuses: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getRepositoriesPullrequestsActivity: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRawFileContent: z + .object({ + commit: z.string(), + path: z.string(), + repo_slug: z.string(), + workspace: z.string(), + format: z.enum(['meta', 'rendered']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + max_depth: z.number().int().optional(), + }) + .strict(), + getRepositoriesSrc: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + format: z.enum(['meta']).optional(), + }) + .strict(), + getRepository: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getRepositoriesPipelinesConfigSshKnownHosts: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + getRepositoriesPipelinesConfigRunners: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + getRepositoriesPipelinesConfigSchedules: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + getRepositoriesPipelinesConfigVariables: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + getRepositoriesPipelinesConfigCaches: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + getRepositoriesRefs: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getRepositoriesWatchers: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getSnippet: z + .object({ + encoded_id: z.union([z.string(), z.number().int()]), + workspace: z.string(), + }) + .strict(), + getSnippetsWatch: z + .object({ + encoded_id: z.union([z.string(), z.number().int()]), + workspace: z.string(), + }) + .strict(), + getRepositoriesPipelines2: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + pipeline_uuid: z.string(), + }) + .strict(), + getRepositoriesRefsTags: z + .object({ + name: z.string(), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + getUser: z + .object({ + selected_user: z.string(), + }) + .strict(), + getUserEmails2: z + .object({ + email: z.string(), + }) + .strict(), + getUserEmails: z.object({}).strict(), + getUserPermissionsRepositories: z + .object({ + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getUserPermissionsWorkspaces: z + .object({ + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + getUserWorkspaces: z + .object({ + sort: z.string().optional(), + administrator: z.boolean().optional(), + }) + .strict(), + getWorkspace: z + .object({ + workspace: z.string(), + }) + .strict(), + listRepositories: z + .object({ + after: z.string().optional(), + role: z.enum(['admin', 'contributor', 'member', 'owner']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + listBranches: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + listCommits: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + listCommitsFromRevision: z + .object({ + repo_slug: z.string(), + revision: z.string(), + workspace: z.string(), + }) + .strict(), + createRepositoriesCommits2: z + .object({ + repo_slug: z.string(), + revision: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema.optional(), + }) + .strict(), + listCommitsOnMaster: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + listDeployments: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + listIssues: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + listPipelines: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + 'creator.uuid': z.string().optional(), + 'target.ref_type': z.enum(['BRANCH', 'TAG', 'ANNOTATED_TAG']).optional(), + 'target.ref_name': z.string().optional(), + 'target.branch': z.string().optional(), + 'target.commit.hash': z.string().optional(), + 'target.selector.pattern': z.string().optional(), + 'target.selector.type': z + .enum(['BRANCH', 'TAG', 'CUSTOM', 'PULLREQUESTS', 'DEFAULT']) + .optional(), + created_on: z.string().optional(), + trigger_type: z + .enum(['PUSH', 'MANUAL', 'SCHEDULED', 'PARENT_STEP']) + .optional(), + status: z + .enum([ + 'PARSING', + 'PENDING', + 'PAUSED', + 'HALTED', + 'BUILDING', + 'ERROR', + 'PASSED', + 'FAILED', + 'STOPPED', + 'UNKNOWN', + ]) + .optional(), + sort: z + .enum(['creator.uuid', 'created_on', 'run_creation_date']) + .optional(), + page: z.number().int().optional(), + pagelen: z.number().int().optional(), + }) + .strict(), + listPullRequestTasks: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + q: z.string().optional(), + sort: z.string().optional(), + pagelen: z.number().int().optional(), + }) + .strict(), + listPullRequests: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + state: z.enum(['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED']).optional(), + }) + .strict(), + listRepositoriesInWorkspace: z + .object({ + workspace: z.string(), + role: z.enum(['admin', 'contributor', 'member', 'owner']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + listRepositoriesEnvironments: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + }) + .strict(), + listRepositoryPaths: z + .object({ + commit: z.string(), + path: z.string(), + repo_slug: z.string(), + workspace: z.string(), + format: z.enum(['meta', 'rendered']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + max_depth: z.number().int().optional(), + }) + .strict(), + listSnippets: z + .object({ + role: z.enum(['owner', 'contributor', 'member']).optional(), + }) + .strict(), + listTags: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + listVersions: z + .object({ + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + listWorkspaceMembers: z + .object({ + workspace: z.string(), + }) + .strict(), + listWorkspaceProjects: z + .object({ + workspace: z.string(), + }) + .strict(), + listWorkspaces: z + .object({ + role: z.enum(['owner', 'collaborator', 'member']).optional(), + q: z.string().optional(), + sort: z.string().optional(), + }) + .strict(), + requestPullRequestChanges: z + .object({ + pull_request_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + searchTeamCode: z + .object({ + username: z.string(), + search_query: z.string().optional(), + page: z.number().int().optional(), + pagelen: z.number().int().optional(), + }) + .strict(), + searchUserRepositoriesCode: z + .object({ + selected_user: z.string(), + search_query: z.string().optional(), + page: z.number().int().optional(), + pagelen: z.number().int().optional(), + }) + .strict(), + getWorkspacesSearchCode: z + .object({ + workspace: z.string(), + search_query: z.string().optional(), + page: z.number().int().optional(), + pagelen: z.number().int().optional(), + }) + .strict(), + updateIssue: z + .object({ + issue_id: z.union([z.string(), z.number().int()]), + repo_slug: z.string(), + workspace: z.string(), + }) + .strict(), + updateRepositoriesCommitComments: z + .object({ + comment_id: z.union([z.string(), z.number().int()]), + commit: z.string(), + repo_slug: z.string(), + workspace: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + updateInsightsProjectsReposCommitsReports: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + reportId: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + updateRepositoriesCommitReportsAnnotations: z + .object({ + workspace: z.string(), + repo_slug: z.string(), + commit: z.string(), + reportId: z.string(), + annotationId: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + updateTeamsPipelinesConfigVariables: z + .object({ + username: z.string(), + variable_uuid: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), + updateUsersPipelinesConfigVariables: z + .object({ + selected_user: z.string(), + variable_uuid: z.string(), + body: BitbucketRequestBodySchema, + }) + .strict(), +} as const; +export const BitbucketEndpointOutputSchemas = { + approvePullRequest: BitbucketResponseSchema, + browseRepositoryPath: BitbucketResponseSchema, + getRepositoriesIssuesVote: BitbucketResponseSchema, + createBranch: BitbucketResponseSchema, + createPullRequest: BitbucketResponseSchema, + createIssue: BitbucketResponseSchema, + createIssueComment: BitbucketResponseSchema, + createRepositoriesCommitReportsAnnotations: BitbucketResponseSchema, + createPullRequestComment: BitbucketResponseSchema, + createRepository: BitbucketResponseSchema, + createSnippetComment: BitbucketResponseSchema, + createTeamsPipelinesConfigVariables: BitbucketResponseSchema, + createUsersPipelinesConfigVariables: BitbucketResponseSchema, + deleteCommitComment: z.null(), + deleteRepositoriesCommitReportsAnnotations: z.null(), + deleteIssue: z.null(), + deleteRepository: z.null(), + deleteSnippetsWatch: z.null(), + deleteUserPipelineVariable: z.null(), + getCommitBuildStatus: BitbucketResponseSchema, + getCommitChanges: BitbucketResponseSchema, + getCommitDiff: z.string(), + getRepositoriesCommitReports: BitbucketResponseSchema, + getOpenidConfiguration: BitbucketResponseSchema, + getPullRequest: BitbucketResponseSchema, + getPullRequestCommits: BitbucketResponseSchema, + getPullRequestDiff: z.string(), + getPullRequestDiffstat: BitbucketResponseSchema, + getRepositoriesMergeBase: BitbucketResponseSchema, + getRepositoriesBranchingModel: BitbucketResponseSchema, + getRepositoriesCommit: BitbucketResponseSchema, + getRepositoriesEnvironments2: BitbucketResponseSchema, + getRepositoryPatch: z.string(), + getSshLatestKeys: BitbucketResponseSchema, + getWorkspacesPullrequests: BitbucketResponseSchema, + getBranch: BitbucketResponseSchema, + getCommitComment: BitbucketResponseSchema, + getRepositoriesCommitComments: BitbucketResponseSchema, + getRepositoriesCommitReport: BitbucketResponseSchema, + getRepositoriesCommitReportsAnnotations: BitbucketResponseSchema, + getRepositoriesCommitStatuses: BitbucketResponseSchema, + getCurrentUser2: BitbucketResponseSchema, + getDeploymentEnvironmentVariables: BitbucketResponseSchema, + getRepositoriesEffectiveBranchingModel: BitbucketResponseSchema, + getRepositoriesFilehistory: BitbucketResponseSchema, + getFileFromRepository: BitbucketResponseSchema, + getHookEvents: BitbucketResponseSchema, + getRepositoriesPipelinesSteps: BitbucketResponseSchema, + getProjectsRepos: BitbucketResponseSchema, + getPullRequestComment: BitbucketResponseSchema, + getRepositoriesPullrequestsComments: BitbucketResponseSchema, + getRepositoriesPullrequestsStatuses: BitbucketResponseSchema, + getRepositoriesPullrequestsActivity: BitbucketResponseSchema, + getRawFileContent: z.string(), + getRepositoriesSrc: BitbucketResponseSchema, + getRepository: BitbucketResponseSchema, + getRepositoriesPipelinesConfigSshKnownHosts: BitbucketResponseSchema, + getRepositoriesPipelinesConfigRunners: BitbucketResponseSchema, + getRepositoriesPipelinesConfigSchedules: BitbucketResponseSchema, + getRepositoriesPipelinesConfigVariables: BitbucketResponseSchema, + getRepositoriesPipelinesConfigCaches: BitbucketResponseSchema, + getRepositoriesRefs: BitbucketResponseSchema, + getRepositoriesWatchers: BitbucketResponseSchema, + getSnippet: BitbucketResponseSchema, + getSnippetsWatch: BitbucketResponseSchema, + getRepositoriesPipelines2: BitbucketResponseSchema, + getRepositoriesRefsTags: BitbucketResponseSchema, + getUser: BitbucketResponseSchema, + getUserEmails2: BitbucketResponseSchema, + getUserEmails: BitbucketResponseSchema, + getUserPermissionsRepositories: BitbucketResponseSchema, + getUserPermissionsWorkspaces: BitbucketResponseSchema, + getUserWorkspaces: BitbucketResponseSchema, + getWorkspace: BitbucketResponseSchema, + listRepositories: BitbucketResponseSchema, + listBranches: BitbucketResponseSchema, + listCommits: BitbucketResponseSchema, + listCommitsFromRevision: BitbucketResponseSchema, + createRepositoriesCommits2: BitbucketResponseSchema, + listCommitsOnMaster: BitbucketResponseSchema, + listDeployments: BitbucketResponseSchema, + listIssues: BitbucketResponseSchema, + listPipelines: BitbucketResponseSchema, + listPullRequestTasks: BitbucketResponseSchema, + listPullRequests: BitbucketResponseSchema, + listRepositoriesInWorkspace: BitbucketResponseSchema, + listRepositoriesEnvironments: BitbucketResponseSchema, + listRepositoryPaths: BitbucketResponseSchema, + listSnippets: BitbucketResponseSchema, + listTags: BitbucketResponseSchema, + listVersions: BitbucketResponseSchema, + listWorkspaceMembers: BitbucketResponseSchema, + listWorkspaceProjects: BitbucketResponseSchema, + listWorkspaces: BitbucketResponseSchema, + requestPullRequestChanges: BitbucketResponseSchema, + searchTeamCode: BitbucketResponseSchema, + searchUserRepositoriesCode: BitbucketResponseSchema, + getWorkspacesSearchCode: BitbucketResponseSchema, + updateIssue: BitbucketResponseSchema, + updateRepositoriesCommitComments: BitbucketResponseSchema, + updateInsightsProjectsReposCommitsReports: BitbucketResponseSchema, + updateRepositoriesCommitReportsAnnotations: BitbucketResponseSchema, + updateTeamsPipelinesConfigVariables: BitbucketResponseSchema, + updateUsersPipelinesConfigVariables: BitbucketResponseSchema, +} as const; +export type BitbucketEndpointInputs = { + [K in keyof typeof BitbucketEndpointInputSchemas]: z.infer< + (typeof BitbucketEndpointInputSchemas)[K] + >; +}; +export type BitbucketEndpointOutputs = { + [K in keyof typeof BitbucketEndpointOutputSchemas]: z.infer< + (typeof BitbucketEndpointOutputSchemas)[K] + >; +}; diff --git a/packages/bitbucket/error-handlers.ts b/packages/bitbucket/error-handlers.ts new file mode 100644 index 000000000..ef155dd6b --- /dev/null +++ b/packages/bitbucket/error-handlers.ts @@ -0,0 +1,59 @@ +import type { CorsairErrorHandler } from 'corsair/core'; +import { + BitbucketAPIError, + BitbucketOAuthError, + BitbucketSchemaError, +} from './client'; +import { bitbucketReadOperationPaths } from './endpoints/operations'; + +const mayRetry = (context: { operation: string }) => + bitbucketReadOperationPaths.has(context.operation as never); +export const errorHandlers = { + RATE_LIMIT_ERROR: { + match: (error: Error) => + error instanceof BitbucketAPIError && error.status === 429, + handler: async (error: Error, context) => ({ + maxRetries: mayRetry(context) ? 3 : 0, + headersRetryAfterMs: + error instanceof BitbucketAPIError ? error.retryAfter : undefined, + }), + }, + AUTH_ERROR: { + match: (error: Error) => + error instanceof BitbucketOAuthError || + (error instanceof BitbucketAPIError && error.status === 401), + handler: async () => ({ maxRetries: 0 }), + }, + PERMISSION_ERROR: { + match: (error: Error) => + error instanceof BitbucketAPIError && error.status === 403, + handler: async () => ({ maxRetries: 0 }), + }, + NOT_FOUND_ERROR: { + match: (error: Error) => + error instanceof BitbucketAPIError && error.status === 404, + handler: async () => ({ maxRetries: 0 }), + }, + VALIDATION_ERROR: { + match: (error: Error) => + error instanceof BitbucketSchemaError || + (error instanceof BitbucketAPIError && + [400, 409, 422].includes(error.status ?? 0)), + handler: async () => ({ maxRetries: 0 }), + }, + SERVER_ERROR: { + match: (error: Error) => + error instanceof BitbucketAPIError && (error.status ?? 0) >= 500, + handler: async (_error: Error, context) => ({ + maxRetries: mayRetry(context) ? 3 : 0, + }), + }, + NETWORK_ERROR: { + match: (error: Error) => + /fetch failed|network|econnreset|timeout/i.test(error.message), + handler: async (_error: Error, context) => ({ + maxRetries: mayRetry(context) ? 3 : 0, + }), + }, + DEFAULT: { match: () => true, handler: async () => ({ maxRetries: 0 }) }, +} satisfies CorsairErrorHandler; diff --git a/packages/bitbucket/index.ts b/packages/bitbucket/index.ts new file mode 100644 index 000000000..e26b25adc --- /dev/null +++ b/packages/bitbucket/index.ts @@ -0,0 +1,1121 @@ +import type { + BindEndpoints, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, +} from 'corsair/core'; +import { AuthMissingError } from 'corsair/core'; +import { getValidBitbucketAccessToken } from './client'; +import { BitbucketEndpoints } from './endpoints'; +import { + BitbucketEndpointInputSchemas, + BitbucketEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import { BitbucketSchema } from './schema'; + +const bitbucketEndpointsNested = BitbucketEndpoints; +export const bitbucketAuthConfig = { + oauth_2: { account: ['account_id'] as const }, +} as const satisfies PluginAuthConfig; +export type BitbucketPluginOptions = { + authType?: PickAuth<'oauth_2'>; + key?: string; + hooks?: InternalBitbucketPlugin['hooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; +export type BitbucketContext = CorsairPluginContext< + typeof BitbucketSchema, + BitbucketPluginOptions, + undefined, + typeof bitbucketAuthConfig +>; +export type BitbucketKeyBuilderContext = + KeyBuilderContext; +export type BitbucketBoundEndpoints = BindEndpoints< + typeof bitbucketEndpointsNested +>; +export const bitbucketEndpointSchemas = { + 'pullRequests.approvePullRequest': { + input: BitbucketEndpointInputSchemas.approvePullRequest, + output: BitbucketEndpointOutputSchemas.approvePullRequest, + }, + 'sourceAndRefs.browseRepositoryPath': { + input: BitbucketEndpointInputSchemas.browseRepositoryPath, + output: BitbucketEndpointOutputSchemas.browseRepositoryPath, + }, + 'issues.getRepositoriesIssuesVote': { + input: BitbucketEndpointInputSchemas.getRepositoriesIssuesVote, + output: BitbucketEndpointOutputSchemas.getRepositoriesIssuesVote, + }, + 'sourceAndRefs.createBranch': { + input: BitbucketEndpointInputSchemas.createBranch, + output: BitbucketEndpointOutputSchemas.createBranch, + }, + 'pullRequests.createPullRequest': { + input: BitbucketEndpointInputSchemas.createPullRequest, + output: BitbucketEndpointOutputSchemas.createPullRequest, + }, + 'issues.createIssue': { + input: BitbucketEndpointInputSchemas.createIssue, + output: BitbucketEndpointOutputSchemas.createIssue, + }, + 'issues.createIssueComment': { + input: BitbucketEndpointInputSchemas.createIssueComment, + output: BitbucketEndpointOutputSchemas.createIssueComment, + }, + 'commitsAndInsights.createRepositoriesCommitReportsAnnotations': { + input: + BitbucketEndpointInputSchemas.createRepositoriesCommitReportsAnnotations, + output: + BitbucketEndpointOutputSchemas.createRepositoriesCommitReportsAnnotations, + }, + 'pullRequests.createPullRequestComment': { + input: BitbucketEndpointInputSchemas.createPullRequestComment, + output: BitbucketEndpointOutputSchemas.createPullRequestComment, + }, + 'repositories.createRepository': { + input: BitbucketEndpointInputSchemas.createRepository, + output: BitbucketEndpointOutputSchemas.createRepository, + }, + 'snippets.createSnippetComment': { + input: BitbucketEndpointInputSchemas.createSnippetComment, + output: BitbucketEndpointOutputSchemas.createSnippetComment, + }, + 'pipelinesAndDeployments.createTeamsPipelinesConfigVariables': { + input: BitbucketEndpointInputSchemas.createTeamsPipelinesConfigVariables, + output: BitbucketEndpointOutputSchemas.createTeamsPipelinesConfigVariables, + }, + 'pipelinesAndDeployments.createUsersPipelinesConfigVariables': { + input: BitbucketEndpointInputSchemas.createUsersPipelinesConfigVariables, + output: BitbucketEndpointOutputSchemas.createUsersPipelinesConfigVariables, + }, + 'commitsAndInsights.deleteCommitComment': { + input: BitbucketEndpointInputSchemas.deleteCommitComment, + output: BitbucketEndpointOutputSchemas.deleteCommitComment, + }, + 'commitsAndInsights.deleteRepositoriesCommitReportsAnnotations': { + input: + BitbucketEndpointInputSchemas.deleteRepositoriesCommitReportsAnnotations, + output: + BitbucketEndpointOutputSchemas.deleteRepositoriesCommitReportsAnnotations, + }, + 'issues.deleteIssue': { + input: BitbucketEndpointInputSchemas.deleteIssue, + output: BitbucketEndpointOutputSchemas.deleteIssue, + }, + 'repositories.deleteRepository': { + input: BitbucketEndpointInputSchemas.deleteRepository, + output: BitbucketEndpointOutputSchemas.deleteRepository, + }, + 'snippets.deleteSnippetsWatch': { + input: BitbucketEndpointInputSchemas.deleteSnippetsWatch, + output: BitbucketEndpointOutputSchemas.deleteSnippetsWatch, + }, + 'pipelinesAndDeployments.deleteUserPipelineVariable': { + input: BitbucketEndpointInputSchemas.deleteUserPipelineVariable, + output: BitbucketEndpointOutputSchemas.deleteUserPipelineVariable, + }, + 'commitsAndInsights.getCommitBuildStatus': { + input: BitbucketEndpointInputSchemas.getCommitBuildStatus, + output: BitbucketEndpointOutputSchemas.getCommitBuildStatus, + }, + 'commitsAndInsights.getCommitChanges': { + input: BitbucketEndpointInputSchemas.getCommitChanges, + output: BitbucketEndpointOutputSchemas.getCommitChanges, + }, + 'commitsAndInsights.getCommitDiff': { + input: BitbucketEndpointInputSchemas.getCommitDiff, + output: BitbucketEndpointOutputSchemas.getCommitDiff, + }, + 'commitsAndInsights.getRepositoriesCommitReports': { + input: BitbucketEndpointInputSchemas.getRepositoriesCommitReports, + output: BitbucketEndpointOutputSchemas.getRepositoriesCommitReports, + }, + 'pipelinesAndDeployments.getOpenidConfiguration': { + input: BitbucketEndpointInputSchemas.getOpenidConfiguration, + output: BitbucketEndpointOutputSchemas.getOpenidConfiguration, + }, + 'pullRequests.getPullRequest': { + input: BitbucketEndpointInputSchemas.getPullRequest, + output: BitbucketEndpointOutputSchemas.getPullRequest, + }, + 'pullRequests.getPullRequestCommits': { + input: BitbucketEndpointInputSchemas.getPullRequestCommits, + output: BitbucketEndpointOutputSchemas.getPullRequestCommits, + }, + 'pullRequests.getPullRequestDiff': { + input: BitbucketEndpointInputSchemas.getPullRequestDiff, + output: BitbucketEndpointOutputSchemas.getPullRequestDiff, + }, + 'pullRequests.getPullRequestDiffstat': { + input: BitbucketEndpointInputSchemas.getPullRequestDiffstat, + output: BitbucketEndpointOutputSchemas.getPullRequestDiffstat, + }, + 'commitsAndInsights.getRepositoriesMergeBase': { + input: BitbucketEndpointInputSchemas.getRepositoriesMergeBase, + output: BitbucketEndpointOutputSchemas.getRepositoriesMergeBase, + }, + 'sourceAndRefs.getRepositoriesBranchingModel': { + input: BitbucketEndpointInputSchemas.getRepositoriesBranchingModel, + output: BitbucketEndpointOutputSchemas.getRepositoriesBranchingModel, + }, + 'commitsAndInsights.getRepositoriesCommit': { + input: BitbucketEndpointInputSchemas.getRepositoriesCommit, + output: BitbucketEndpointOutputSchemas.getRepositoriesCommit, + }, + 'pipelinesAndDeployments.getRepositoriesEnvironments2': { + input: BitbucketEndpointInputSchemas.getRepositoriesEnvironments2, + output: BitbucketEndpointOutputSchemas.getRepositoriesEnvironments2, + }, + 'commitsAndInsights.getRepositoryPatch': { + input: BitbucketEndpointInputSchemas.getRepositoryPatch, + output: BitbucketEndpointOutputSchemas.getRepositoryPatch, + }, + 'usersAndPermissions.getSshLatestKeys': { + input: BitbucketEndpointInputSchemas.getSshLatestKeys, + output: BitbucketEndpointOutputSchemas.getSshLatestKeys, + }, + 'workspacesAndProjects.getWorkspacesPullrequests': { + input: BitbucketEndpointInputSchemas.getWorkspacesPullrequests, + output: BitbucketEndpointOutputSchemas.getWorkspacesPullrequests, + }, + 'sourceAndRefs.getBranch': { + input: BitbucketEndpointInputSchemas.getBranch, + output: BitbucketEndpointOutputSchemas.getBranch, + }, + 'commitsAndInsights.getCommitComment': { + input: BitbucketEndpointInputSchemas.getCommitComment, + output: BitbucketEndpointOutputSchemas.getCommitComment, + }, + 'commitsAndInsights.getRepositoriesCommitComments': { + input: BitbucketEndpointInputSchemas.getRepositoriesCommitComments, + output: BitbucketEndpointOutputSchemas.getRepositoriesCommitComments, + }, + 'commitsAndInsights.getRepositoriesCommitReport': { + input: BitbucketEndpointInputSchemas.getRepositoriesCommitReport, + output: BitbucketEndpointOutputSchemas.getRepositoriesCommitReport, + }, + 'commitsAndInsights.getRepositoriesCommitReportsAnnotations': { + input: + BitbucketEndpointInputSchemas.getRepositoriesCommitReportsAnnotations, + output: + BitbucketEndpointOutputSchemas.getRepositoriesCommitReportsAnnotations, + }, + 'commitsAndInsights.getRepositoriesCommitStatuses': { + input: BitbucketEndpointInputSchemas.getRepositoriesCommitStatuses, + output: BitbucketEndpointOutputSchemas.getRepositoriesCommitStatuses, + }, + 'usersAndPermissions.getCurrentUser2': { + input: BitbucketEndpointInputSchemas.getCurrentUser2, + output: BitbucketEndpointOutputSchemas.getCurrentUser2, + }, + 'pipelinesAndDeployments.getDeploymentEnvironmentVariables': { + input: BitbucketEndpointInputSchemas.getDeploymentEnvironmentVariables, + output: BitbucketEndpointOutputSchemas.getDeploymentEnvironmentVariables, + }, + 'sourceAndRefs.getRepositoriesEffectiveBranchingModel': { + input: BitbucketEndpointInputSchemas.getRepositoriesEffectiveBranchingModel, + output: + BitbucketEndpointOutputSchemas.getRepositoriesEffectiveBranchingModel, + }, + 'sourceAndRefs.getRepositoriesFilehistory': { + input: BitbucketEndpointInputSchemas.getRepositoriesFilehistory, + output: BitbucketEndpointOutputSchemas.getRepositoriesFilehistory, + }, + 'sourceAndRefs.getFileFromRepository': { + input: BitbucketEndpointInputSchemas.getFileFromRepository, + output: BitbucketEndpointOutputSchemas.getFileFromRepository, + }, + 'searchAndDiscovery.getHookEvents': { + input: BitbucketEndpointInputSchemas.getHookEvents, + output: BitbucketEndpointOutputSchemas.getHookEvents, + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesSteps': { + input: BitbucketEndpointInputSchemas.getRepositoriesPipelinesSteps, + output: BitbucketEndpointOutputSchemas.getRepositoriesPipelinesSteps, + }, + 'workspacesAndProjects.getProjectsRepos': { + input: BitbucketEndpointInputSchemas.getProjectsRepos, + output: BitbucketEndpointOutputSchemas.getProjectsRepos, + }, + 'pullRequests.getPullRequestComment': { + input: BitbucketEndpointInputSchemas.getPullRequestComment, + output: BitbucketEndpointOutputSchemas.getPullRequestComment, + }, + 'pullRequests.getRepositoriesPullrequestsComments': { + input: BitbucketEndpointInputSchemas.getRepositoriesPullrequestsComments, + output: BitbucketEndpointOutputSchemas.getRepositoriesPullrequestsComments, + }, + 'pullRequests.getRepositoriesPullrequestsStatuses': { + input: BitbucketEndpointInputSchemas.getRepositoriesPullrequestsStatuses, + output: BitbucketEndpointOutputSchemas.getRepositoriesPullrequestsStatuses, + }, + 'pullRequests.getRepositoriesPullrequestsActivity': { + input: BitbucketEndpointInputSchemas.getRepositoriesPullrequestsActivity, + output: BitbucketEndpointOutputSchemas.getRepositoriesPullrequestsActivity, + }, + 'sourceAndRefs.getRawFileContent': { + input: BitbucketEndpointInputSchemas.getRawFileContent, + output: BitbucketEndpointOutputSchemas.getRawFileContent, + }, + 'sourceAndRefs.getRepositoriesSrc': { + input: BitbucketEndpointInputSchemas.getRepositoriesSrc, + output: BitbucketEndpointOutputSchemas.getRepositoriesSrc, + }, + 'repositories.getRepository': { + input: BitbucketEndpointInputSchemas.getRepository, + output: BitbucketEndpointOutputSchemas.getRepository, + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigSshKnownHosts': { + input: + BitbucketEndpointInputSchemas.getRepositoriesPipelinesConfigSshKnownHosts, + output: + BitbucketEndpointOutputSchemas.getRepositoriesPipelinesConfigSshKnownHosts, + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigRunners': { + input: BitbucketEndpointInputSchemas.getRepositoriesPipelinesConfigRunners, + output: + BitbucketEndpointOutputSchemas.getRepositoriesPipelinesConfigRunners, + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigSchedules': { + input: + BitbucketEndpointInputSchemas.getRepositoriesPipelinesConfigSchedules, + output: + BitbucketEndpointOutputSchemas.getRepositoriesPipelinesConfigSchedules, + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigVariables': { + input: + BitbucketEndpointInputSchemas.getRepositoriesPipelinesConfigVariables, + output: + BitbucketEndpointOutputSchemas.getRepositoriesPipelinesConfigVariables, + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigCaches': { + input: BitbucketEndpointInputSchemas.getRepositoriesPipelinesConfigCaches, + output: BitbucketEndpointOutputSchemas.getRepositoriesPipelinesConfigCaches, + }, + 'sourceAndRefs.getRepositoriesRefs': { + input: BitbucketEndpointInputSchemas.getRepositoriesRefs, + output: BitbucketEndpointOutputSchemas.getRepositoriesRefs, + }, + 'repositories.getRepositoriesWatchers': { + input: BitbucketEndpointInputSchemas.getRepositoriesWatchers, + output: BitbucketEndpointOutputSchemas.getRepositoriesWatchers, + }, + 'snippets.getSnippet': { + input: BitbucketEndpointInputSchemas.getSnippet, + output: BitbucketEndpointOutputSchemas.getSnippet, + }, + 'snippets.getSnippetsWatch': { + input: BitbucketEndpointInputSchemas.getSnippetsWatch, + output: BitbucketEndpointOutputSchemas.getSnippetsWatch, + }, + 'pipelinesAndDeployments.getRepositoriesPipelines2': { + input: BitbucketEndpointInputSchemas.getRepositoriesPipelines2, + output: BitbucketEndpointOutputSchemas.getRepositoriesPipelines2, + }, + 'sourceAndRefs.getRepositoriesRefsTags': { + input: BitbucketEndpointInputSchemas.getRepositoriesRefsTags, + output: BitbucketEndpointOutputSchemas.getRepositoriesRefsTags, + }, + 'usersAndPermissions.getUser': { + input: BitbucketEndpointInputSchemas.getUser, + output: BitbucketEndpointOutputSchemas.getUser, + }, + 'usersAndPermissions.getUserEmails2': { + input: BitbucketEndpointInputSchemas.getUserEmails2, + output: BitbucketEndpointOutputSchemas.getUserEmails2, + }, + 'usersAndPermissions.getUserEmails': { + input: BitbucketEndpointInputSchemas.getUserEmails, + output: BitbucketEndpointOutputSchemas.getUserEmails, + }, + 'usersAndPermissions.getUserPermissionsRepositories': { + input: BitbucketEndpointInputSchemas.getUserPermissionsRepositories, + output: BitbucketEndpointOutputSchemas.getUserPermissionsRepositories, + }, + 'usersAndPermissions.getUserPermissionsWorkspaces': { + input: BitbucketEndpointInputSchemas.getUserPermissionsWorkspaces, + output: BitbucketEndpointOutputSchemas.getUserPermissionsWorkspaces, + }, + 'usersAndPermissions.getUserWorkspaces': { + input: BitbucketEndpointInputSchemas.getUserWorkspaces, + output: BitbucketEndpointOutputSchemas.getUserWorkspaces, + }, + 'workspacesAndProjects.getWorkspace': { + input: BitbucketEndpointInputSchemas.getWorkspace, + output: BitbucketEndpointOutputSchemas.getWorkspace, + }, + 'repositories.listRepositories': { + input: BitbucketEndpointInputSchemas.listRepositories, + output: BitbucketEndpointOutputSchemas.listRepositories, + }, + 'sourceAndRefs.listBranches': { + input: BitbucketEndpointInputSchemas.listBranches, + output: BitbucketEndpointOutputSchemas.listBranches, + }, + 'commitsAndInsights.listCommits': { + input: BitbucketEndpointInputSchemas.listCommits, + output: BitbucketEndpointOutputSchemas.listCommits, + }, + 'commitsAndInsights.listCommitsFromRevision': { + input: BitbucketEndpointInputSchemas.listCommitsFromRevision, + output: BitbucketEndpointOutputSchemas.listCommitsFromRevision, + }, + 'commitsAndInsights.createRepositoriesCommits2': { + input: BitbucketEndpointInputSchemas.createRepositoriesCommits2, + output: BitbucketEndpointOutputSchemas.createRepositoriesCommits2, + }, + 'commitsAndInsights.listCommitsOnMaster': { + input: BitbucketEndpointInputSchemas.listCommitsOnMaster, + output: BitbucketEndpointOutputSchemas.listCommitsOnMaster, + }, + 'pipelinesAndDeployments.listDeployments': { + input: BitbucketEndpointInputSchemas.listDeployments, + output: BitbucketEndpointOutputSchemas.listDeployments, + }, + 'issues.listIssues': { + input: BitbucketEndpointInputSchemas.listIssues, + output: BitbucketEndpointOutputSchemas.listIssues, + }, + 'pipelinesAndDeployments.listPipelines': { + input: BitbucketEndpointInputSchemas.listPipelines, + output: BitbucketEndpointOutputSchemas.listPipelines, + }, + 'pullRequests.listPullRequestTasks': { + input: BitbucketEndpointInputSchemas.listPullRequestTasks, + output: BitbucketEndpointOutputSchemas.listPullRequestTasks, + }, + 'pullRequests.listPullRequests': { + input: BitbucketEndpointInputSchemas.listPullRequests, + output: BitbucketEndpointOutputSchemas.listPullRequests, + }, + 'repositories.listRepositoriesInWorkspace': { + input: BitbucketEndpointInputSchemas.listRepositoriesInWorkspace, + output: BitbucketEndpointOutputSchemas.listRepositoriesInWorkspace, + }, + 'pipelinesAndDeployments.listRepositoriesEnvironments': { + input: BitbucketEndpointInputSchemas.listRepositoriesEnvironments, + output: BitbucketEndpointOutputSchemas.listRepositoriesEnvironments, + }, + 'sourceAndRefs.listRepositoryPaths': { + input: BitbucketEndpointInputSchemas.listRepositoryPaths, + output: BitbucketEndpointOutputSchemas.listRepositoryPaths, + }, + 'snippets.listSnippets': { + input: BitbucketEndpointInputSchemas.listSnippets, + output: BitbucketEndpointOutputSchemas.listSnippets, + }, + 'sourceAndRefs.listTags': { + input: BitbucketEndpointInputSchemas.listTags, + output: BitbucketEndpointOutputSchemas.listTags, + }, + 'issues.listVersions': { + input: BitbucketEndpointInputSchemas.listVersions, + output: BitbucketEndpointOutputSchemas.listVersions, + }, + 'workspacesAndProjects.listWorkspaceMembers': { + input: BitbucketEndpointInputSchemas.listWorkspaceMembers, + output: BitbucketEndpointOutputSchemas.listWorkspaceMembers, + }, + 'workspacesAndProjects.listWorkspaceProjects': { + input: BitbucketEndpointInputSchemas.listWorkspaceProjects, + output: BitbucketEndpointOutputSchemas.listWorkspaceProjects, + }, + 'workspacesAndProjects.listWorkspaces': { + input: BitbucketEndpointInputSchemas.listWorkspaces, + output: BitbucketEndpointOutputSchemas.listWorkspaces, + }, + 'pullRequests.requestPullRequestChanges': { + input: BitbucketEndpointInputSchemas.requestPullRequestChanges, + output: BitbucketEndpointOutputSchemas.requestPullRequestChanges, + }, + 'searchAndDiscovery.searchTeamCode': { + input: BitbucketEndpointInputSchemas.searchTeamCode, + output: BitbucketEndpointOutputSchemas.searchTeamCode, + }, + 'searchAndDiscovery.searchUserRepositoriesCode': { + input: BitbucketEndpointInputSchemas.searchUserRepositoriesCode, + output: BitbucketEndpointOutputSchemas.searchUserRepositoriesCode, + }, + 'searchAndDiscovery.getWorkspacesSearchCode': { + input: BitbucketEndpointInputSchemas.getWorkspacesSearchCode, + output: BitbucketEndpointOutputSchemas.getWorkspacesSearchCode, + }, + 'issues.updateIssue': { + input: BitbucketEndpointInputSchemas.updateIssue, + output: BitbucketEndpointOutputSchemas.updateIssue, + }, + 'commitsAndInsights.updateRepositoriesCommitComments': { + input: BitbucketEndpointInputSchemas.updateRepositoriesCommitComments, + output: BitbucketEndpointOutputSchemas.updateRepositoriesCommitComments, + }, + 'commitsAndInsights.updateInsightsProjectsReposCommitsReports': { + input: + BitbucketEndpointInputSchemas.updateInsightsProjectsReposCommitsReports, + output: + BitbucketEndpointOutputSchemas.updateInsightsProjectsReposCommitsReports, + }, + 'commitsAndInsights.updateRepositoriesCommitReportsAnnotations': { + input: + BitbucketEndpointInputSchemas.updateRepositoriesCommitReportsAnnotations, + output: + BitbucketEndpointOutputSchemas.updateRepositoriesCommitReportsAnnotations, + }, + 'pipelinesAndDeployments.updateTeamsPipelinesConfigVariables': { + input: BitbucketEndpointInputSchemas.updateTeamsPipelinesConfigVariables, + output: BitbucketEndpointOutputSchemas.updateTeamsPipelinesConfigVariables, + }, + 'pipelinesAndDeployments.updateUsersPipelinesConfigVariables': { + input: BitbucketEndpointInputSchemas.updateUsersPipelinesConfigVariables, + output: BitbucketEndpointOutputSchemas.updateUsersPipelinesConfigVariables, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof bitbucketEndpointsNested +>; +const bitbucketEndpointMeta = { + 'pullRequests.approvePullRequest': { + riskLevel: 'write', + description: + 'Tool to approve a pull request as the authenticated user. Use when you need to formally approve changes in a pull request review process.', + }, + 'sourceAndRefs.browseRepositoryPath': { + riskLevel: 'read', + description: + 'Tool to retrieve content for a file path or browse directory contents at a specified revision in a Bitbucket repository. Use when you need flexible access to repository content - returns raw file data for files or paginated directory listings for directories.', + }, + 'issues.getRepositoriesIssuesVote': { + riskLevel: 'read', + description: + 'Tool to check whether the authenticated user has voted for a specific issue in a Bitbucket repository. Use when you need to verify if the current user has already voted on an issue before attempting to vote or unvote.', + }, + 'sourceAndRefs.createBranch': { + riskLevel: 'write', + description: + "Creates a new branch in a Bitbucket repository from a target commit hash; the branch name must be unique, adhere to Bitbucket's naming conventions, and not include the 'refs/heads/' prefix.", + }, + 'pullRequests.createPullRequest': { + riskLevel: 'write', + description: + 'Creates a new pull request in a specified Bitbucket repository, ensuring the source branch exists and is distinct from the (optional) destination branch.', + }, + 'issues.createIssue': { + riskLevel: 'write', + description: + 'Creates a new issue in a Bitbucket repository, setting the authenticated user as reporter; ensures assignee (if provided) has repository access, and that any specified milestone, version, or component IDs exist.', + }, + 'issues.createIssueComment': { + riskLevel: 'write', + description: + 'Adds a new comment with markdown support to an existing Bitbucket issue.', + }, + 'commitsAndInsights.createRepositoriesCommitReportsAnnotations': { + riskLevel: 'write', + description: + 'Adds multiple annotations to a commit report in bulk. Use when you need to add code analysis findings (vulnerabilities, code smells, bugs) to a report attached to a specific commit.', + }, + 'pullRequests.createPullRequestComment': { + riskLevel: 'write', + description: + 'Creates a new comment on a Bitbucket pull request. Supports top-level comments, threaded replies, and inline code comments. Use when providing feedback on a PR, replying to existing comments, or commenting on specific code lines.', + }, + 'repositories.createRepository': { + riskLevel: 'write', + description: + "Creates a new Bitbucket 'git' repository in a specified workspace, defaulting to the workspace's oldest project if `project_key` is not provided.", + }, + 'snippets.createSnippetComment': { + riskLevel: 'write', + description: + 'Posts a new top-level comment or a threaded reply to an existing comment on a specified Bitbucket snippet.', + }, + 'pipelinesAndDeployments.createTeamsPipelinesConfigVariables': { + riskLevel: 'write', + description: + 'Creates a team-level pipeline configuration variable in Bitbucket. Use when you need to add environment variables or configuration values that should be available to all pipelines within a team.', + }, + 'pipelinesAndDeployments.createUsersPipelinesConfigVariables': { + riskLevel: 'write', + description: + 'Creates a user-level pipeline variable for Bitbucket pipelines. Use when you need to create account-level configuration variables that can be used across all repositories owned by the user.', + }, + 'commitsAndInsights.deleteCommitComment': { + riskLevel: 'destructive', + irreversible: true, + description: + 'Permanently deletes a specific comment on a commit. Use when removing outdated, incorrect, or unwanted feedback on a commit.', + }, + 'commitsAndInsights.deleteRepositoriesCommitReportsAnnotations': { + riskLevel: 'destructive', + irreversible: true, + description: + 'Deletes a single annotation matching the provided ID from a commit report. Use when you need to remove a specific annotation from a code analysis report.', + }, + 'issues.deleteIssue': { + riskLevel: 'destructive', + irreversible: true, + description: + 'Permanently deletes a specific issue, identified by its `issue_id`, from the repository specified by `repo_slug` within the given `workspace`.', + }, + 'repositories.deleteRepository': { + riskLevel: 'destructive', + irreversible: true, + description: + 'Permanently deletes a specified Bitbucket repository; this action is irreversible and does not affect forks.', + }, + 'snippets.deleteSnippetsWatch': { + riskLevel: 'write', + description: + 'Stops watching a specific snippet. Use when you want to unsubscribe from notifications for a snippet.', + }, + 'pipelinesAndDeployments.deleteUserPipelineVariable': { + riskLevel: 'destructive', + irreversible: true, + description: + 'Permanently deletes a user-level pipeline configuration variable identified by its UUID. Use this to remove pipeline variables that are no longer needed at the account level.', + }, + 'commitsAndInsights.getCommitBuildStatus': { + riskLevel: 'read', + description: + 'Get a specific build status for a commit in Bitbucket. Use when you need to check the status of a particular build/CI run for a commit.', + }, + 'commitsAndInsights.getCommitChanges': { + riskLevel: 'read', + description: + 'Tool to retrieve a page of changes made in a specified commit, showing all changed files with their change statistics (lines added/removed, status). Use when you need to enumerate files modified in a specific commit or commit range.', + }, + 'commitsAndInsights.getCommitDiff': { + riskLevel: 'read', + description: + 'Tool to retrieve the unified diff between two provided revisions or for a single commit in a Bitbucket repository. Use when you need to see the actual code changes in a commit or between two commits. Supports filtering by file path and various diff options.', + }, + 'commitsAndInsights.getRepositoriesCommitReports': { + riskLevel: 'read', + description: + 'Tool to get reports linked to a specific commit. Use when you need to retrieve analysis results, test reports, security scans, or code coverage data associated with a commit.', + }, + 'pipelinesAndDeployments.getOpenidConfiguration': { + riskLevel: 'read', + description: + 'Retrieves the OpenID Connect discovery configuration for Bitbucket Pipelines OIDC. Use when integrating Bitbucket Pipelines with resource servers (AWS, GCP, Vault) using OpenID Connect authentication. Returns issuer URL, JWKS URI, and supported capabilities.', + }, + 'pullRequests.getPullRequest': { + riskLevel: 'read', + description: 'Get a single pull request by ID with complete details.', + }, + 'pullRequests.getPullRequestCommits': { + riskLevel: 'read', + description: + 'Tool to retrieve commits for a specified pull request. Use when reviewing the commit history of a PR or analyzing changes included in a pull request.', + }, + 'pullRequests.getPullRequestDiff': { + riskLevel: 'read', + description: + 'Tool to fetch the unified diff for a Bitbucket pull request (follows 302 redirect to repository diff). Use when reviewing code changes in a PR. Supports optional truncation for large diffs via max_chars parameter.', + }, + 'pullRequests.getPullRequestDiffstat': { + riskLevel: 'read', + description: + 'Tool to get the diffstat for a Bitbucket pull request, showing all changed files with their change statistics (lines added/removed, status). Use when you need to enumerate files modified in a PR.', + }, + 'commitsAndInsights.getRepositoriesMergeBase': { + riskLevel: 'read', + description: + 'Get the merge base (best common ancestor) between two commits in a Bitbucket repository. Use when you need to find the common ancestor commit between two branches or commits for comparison or merge operations.', + }, + 'sourceAndRefs.getRepositoriesBranchingModel': { + riskLevel: 'read', + description: + "Return the branching model as applied to the repository. Use when you need to understand the repository's branch workflow configuration, including development/production branches and branch type prefixes.", + }, + 'commitsAndInsights.getRepositoriesCommit': { + riskLevel: 'read', + description: + 'Tool to retrieve detailed information about a specific commit in a Bitbucket repository. Use when you need to get complete commit details including author, message, date, parents, and related links.', + }, + 'pipelinesAndDeployments.getRepositoriesEnvironments2': { + riskLevel: 'read', + description: + 'Retrieve detailed information about a specific deployment environment in a Bitbucket repository. Use when you need to get environment configuration, deployment settings, or check environment properties like locks and restrictions.', + }, + 'commitsAndInsights.getRepositoryPatch': { + riskLevel: 'read', + description: + 'Tool to retrieve the git patch content for a Bitbucket repository at a specified revision or commit range. Use when you need to review code changes, generate diffs, or analyze modifications between commits. Returns raw patch in unified diff format.', + }, + 'usersAndPermissions.getSshLatestKeys': { + riskLevel: 'read', + description: + 'Retrieves a paginated list of SSH keys for a specified Bitbucket user. Use when you need to view or audit SSH keys configured for a user account.', + }, + 'workspacesAndProjects.getWorkspacesPullrequests': { + riskLevel: 'read', + description: + 'Tool to get all workspace pull requests authored by a specified user. Use when you need to retrieve pull requests created by a specific user across all repositories in a workspace.', + }, + 'sourceAndRefs.getBranch': { + riskLevel: 'read', + description: + 'Retrieves detailed information about a specific branch in a Bitbucket repository. Use when you need to get branch metadata, the commit it points to, or verify a branch exists.', + }, + 'commitsAndInsights.getCommitComment': { + riskLevel: 'read', + description: + 'Retrieves a specific comment from a commit by its ID. Use when you need to fetch details of a particular commit comment including content, author, timestamps, and inline location.', + }, + 'commitsAndInsights.getRepositoriesCommitComments': { + riskLevel: 'read', + description: + 'Retrieves all comments on a specific commit in a Bitbucket repository. Returns both global and inline code comments. Use when you need to view feedback, discussions, or notes left on a specific commit.', + }, + 'commitsAndInsights.getRepositoriesCommitReport': { + riskLevel: 'read', + description: + 'Returns a single report matching the provided ID from a commit. Use when you need to retrieve details of a specific analysis report (e.g., security scan, code coverage, test results, or bug report) for a commit.', + }, + 'commitsAndInsights.getRepositoriesCommitReportsAnnotations': { + riskLevel: 'read', + description: + 'Returns a single annotation matching the provided ID from a commit report. Use when you need to retrieve details of a specific code analysis finding (e.g., vulnerability, code smell, or bug) identified in a commit.', + }, + 'commitsAndInsights.getRepositoriesCommitStatuses': { + riskLevel: 'read', + description: + 'Returns all build statuses (e.g., CI/CD pipeline results) for a specific commit. Use when you need to check build status, verify test results, or monitor deployment pipelines for a particular commit.', + }, + 'usersAndPermissions.getCurrentUser2': { + riskLevel: 'read', + description: + 'Tool to retrieve complete profile information for the currently authenticated Bitbucket user. Use when you need comprehensive user details including account_id, username, nickname, and other profile fields.', + }, + 'pipelinesAndDeployments.getDeploymentEnvironmentVariables': { + riskLevel: 'read', + description: + 'Retrieves deployment environment level variables for a specific Bitbucket repository environment. Use when you need to view or audit environment-specific configuration variables for deployments.', + }, + 'sourceAndRefs.getRepositoriesEffectiveBranchingModel': { + riskLevel: 'read', + description: + "Retrieves the effective branching model for a Bitbucket repository, showing which branching model is currently applied (including any inheritance from project-level settings). Use when you need to understand the repository's branch workflow configuration, including development and production branches and branch type prefixes.", + }, + 'sourceAndRefs.getRepositoriesFilehistory': { + riskLevel: 'read', + description: + 'Returns a paginated list of commits that modified the specified file. Use when you need to track file changes over time, find who modified a file, or determine when a file was created or last changed.', + }, + 'sourceAndRefs.getFileFromRepository': { + riskLevel: 'read', + description: + "Retrieves a specific file's content from a Bitbucket repository at a given commit (hash, branch, or tag), failing if the file path is invalid for that commit.", + }, + 'searchAndDiscovery.getHookEvents': { + riskLevel: 'read', + description: + 'Retrieves a paginated list of all valid webhook events for a specified entity type (repository or workspace). Use when you need to discover available webhook event types for subscription or webhook configuration.', + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesSteps': { + riskLevel: 'read', + description: + 'Retrieves all steps for a given pipeline. Use when you need to inspect the individual steps of a pipeline execution, including their state, duration, and commands.', + }, + 'workspacesAndProjects.getProjectsRepos': { + riskLevel: 'read', + description: + 'Retrieves repositories from a project in a Bitbucket workspace. Use when you need to list all repositories belonging to a specific project key within a workspace.', + }, + 'pullRequests.getPullRequestComment': { + riskLevel: 'read', + description: + 'Tool to retrieve a specific comment from a pull request by its ID. Use when you need to fetch details of a particular pull request comment including content, author, timestamps, and inline location.', + }, + 'pullRequests.getRepositoriesPullrequestsComments': { + riskLevel: 'read', + description: + 'Retrieves a paginated list of comments on a specific pull request in a Bitbucket repository. Returns global, inline, and threaded comments. Use when you need to view feedback, discussions, or reviews left on a pull request.', + }, + 'pullRequests.getRepositoriesPullrequestsStatuses': { + riskLevel: 'read', + description: + 'Returns all build statuses (e.g., CI/CD pipeline results) for a specific pull request. Use when you need to check build status, verify test results, or monitor deployment pipelines for a pull request.', + }, + 'pullRequests.getRepositoriesPullrequestsActivity': { + riskLevel: 'read', + description: + 'Get paginated activity log for all pull requests in a repository. Returns comments, updates, approvals, and request changes. Use when you need to track pull request activity history.', + }, + 'sourceAndRefs.getRawFileContent': { + riskLevel: 'read', + description: + 'Tool to retrieve the raw content of a file from a Bitbucket repository at a specified commit, branch, or tag. Use when you need to read file contents from a specific revision.', + }, + 'sourceAndRefs.getRepositoriesSrc': { + riskLevel: 'read', + description: + "Lists the contents of the root directory on the repository's main branch without needing to specify a commit or branch. This endpoint redirects to the main branch automatically.", + }, + 'repositories.getRepository': { + riskLevel: 'read', + description: + 'Retrieves detailed information about a specific repository in a Bitbucket workspace. Use when you need to get repository metadata, settings, or details.', + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigSshKnownHosts': { + riskLevel: 'read', + description: + 'Retrieves repository-level SSH known hosts configured for Bitbucket Pipelines. Use when you need to list or verify SSH known hosts that Pipelines can connect to during builds.', + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigRunners': { + riskLevel: 'read', + description: + "Retrieves the list of self-hosted runners configured for a repository's pipelines. Use when you need to view available runners for pipeline execution.", + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigSchedules': { + riskLevel: 'read', + description: + 'Retrieves configured pipeline schedules for a Bitbucket repository. Use when you need to view scheduled pipeline runs and their cron patterns.', + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigVariables': { + riskLevel: 'read', + description: + 'Retrieves repository-level pipeline variables for a specific Bitbucket repository. Use when you need to view or audit pipeline configuration variables that are scoped to a repository.', + }, + 'pipelinesAndDeployments.getRepositoriesPipelinesConfigCaches': { + riskLevel: 'read', + description: + 'Retrieves the repository pipelines caches. Use when you need to list all caches configured for Bitbucket Pipelines in a specific repository.', + }, + 'sourceAndRefs.getRepositoriesRefs': { + riskLevel: 'read', + description: + 'Returns the branches and tags in the repository. Use when you need to list all refs (both branches and tags) in a single API call with optional filtering by type, name pattern, or commit hash.', + }, + 'repositories.getRepositoriesWatchers': { + riskLevel: 'read', + description: + 'Retrieves a paginated list of all the watchers on the specified repository. Use when you need to see who is watching a particular repository.', + }, + 'snippets.getSnippet': { + riskLevel: 'read', + description: + 'Retrieves a specific Bitbucket snippet by its encoded ID from an existing workspace, returning its metadata and file structure.', + }, + 'snippets.getSnippetsWatch': { + riskLevel: 'read', + description: + 'Checks if the current user is watching a specific snippet. Use when you need to verify watch status for a snippet.', + }, + 'pipelinesAndDeployments.getRepositoriesPipelines2': { + riskLevel: 'read', + description: + 'Retrieve a specified pipeline from a Bitbucket repository. Use when you need to get detailed information about a specific pipeline execution including its status, build number, and results.', + }, + 'sourceAndRefs.getRepositoriesRefsTags': { + riskLevel: 'read', + description: + 'Retrieves detailed information about a specific tag in a Bitbucket repository. Use when you need to get tag metadata, target commit details, or verify a tag exists.', + }, + 'usersAndPermissions.getUser': { + riskLevel: 'read', + description: + 'Retrieves public profile information for a specific Bitbucket user by username or UUID. Use when you need to get user details like display name, avatar, creation date, and links to related resources.', + }, + 'usersAndPermissions.getUserEmails2': { + riskLevel: 'read', + description: + 'Retrieves details about a specific email address for the authenticated user. Use when you need to check if an email is primary, confirmed, or verify email ownership.', + }, + 'usersAndPermissions.getUserEmails': { + riskLevel: 'read', + description: + "Returns all the authenticated user's email addresses, both confirmed and unconfirmed. Use when you need to retrieve all email addresses associated with the current user's account.", + }, + 'usersAndPermissions.getUserPermissionsRepositories': { + riskLevel: 'read', + description: + 'Returns an object for each repository the caller has explicit access to, including their permission level. Use when you need to discover which repositories the authenticated user can access and their specific permissions.', + }, + 'usersAndPermissions.getUserPermissionsWorkspaces': { + riskLevel: 'read', + description: + 'Retrieves workspace memberships and permission levels for the authenticated user. Returns an object for each workspace the caller is a member of, along with their effective role (highest privilege level). Use when you need to determine which workspaces a user can access and their permission level in each.', + }, + 'usersAndPermissions.getUserWorkspaces': { + riskLevel: 'read', + description: + 'Tool to retrieve all workspaces accessible to the authenticated user. Use when you need to list workspaces the current user can access, optionally filtered by workspace slug or permission level.', + }, + 'workspacesAndProjects.getWorkspace': { + riskLevel: 'read', + description: + 'Retrieves detailed information about a specific Bitbucket workspace. Use when you need to get workspace metadata, settings, or details.', + }, + 'repositories.listRepositories': { + riskLevel: 'read', + description: + 'Retrieves a paginated list of all public repositories on Bitbucket. Use when you need to discover or search across public repositories, optionally filtered by role, query string, creation date, or sorted by various fields.', + }, + 'sourceAndRefs.listBranches': { + riskLevel: 'read', + description: + 'Lists branches in a Bitbucket repository with optional server-side filtering by name pattern (BBQL) and sorting. Use when you need to discover available branches, search for specific branch patterns, or navigate repository branch structure.', + }, + 'commitsAndInsights.listCommits': { + riskLevel: 'read', + description: + 'Tool to retrieve a page of commits from a Bitbucket repository. Returns commits in reverse chronological order (newest first), similar to git log. Use when you need to browse commit history, filter commits by branch/tag, or restrict to commits affecting a specific path.', + }, + 'commitsAndInsights.listCommitsFromRevision': { + riskLevel: 'read', + description: + 'Tool to list commits starting from a specific revision in a Bitbucket repository. Commits are paginated and returned in reverse chronological order. Use when you need to retrieve commit history from a specific commit, branch, or tag.', + }, + 'commitsAndInsights.createRepositoriesCommits2': { + riskLevel: 'write', + description: + 'Tool to list commits from a revision using POST method. Identical to GET endpoint but allows sending include/exclude parameters in request body to avoid URL length limits. Use when include/exclude parameters are too long for query strings.', + }, + 'commitsAndInsights.listCommitsOnMaster': { + riskLevel: 'read', + description: + 'Lists commits on the master branch of a Bitbucket repository. Use when you need to retrieve the commit history for the master branch, including commit messages, authors, dates, and parent relationships.', + }, + 'pipelinesAndDeployments.listDeployments': { + riskLevel: 'read', + description: + 'Lists deployments for a specified Bitbucket repository. Use when you need to view deployment history and status across environments.', + }, + 'issues.listIssues': { + riskLevel: 'read', + description: + 'Lists issues in a Bitbucket repository with optional filtering by state, priority, kind, or assignee. Use when you need to discover issue IDs or get an overview of repository issues.', + }, + 'pipelinesAndDeployments.listPipelines': { + riskLevel: 'read', + description: + 'Tool to find pipelines in a Bitbucket repository. Returns pipeline metadata including state, trigger, and duration. Use when you need to browse pipeline history or check pipeline status.', + }, + 'pullRequests.listPullRequestTasks': { + riskLevel: 'read', + description: + 'Lists all tasks associated with a pull request in a Bitbucket repository. Use when you need to view or track tasks on a PR.', + }, + 'pullRequests.listPullRequests': { + riskLevel: 'read', + description: + 'Lists pull requests in a specified, accessible Bitbucket repository, optionally filtering by state (OPEN, MERGED, DECLINED).', + }, + 'repositories.listRepositoriesInWorkspace': { + riskLevel: 'read', + description: + 'Lists repositories in a specified Bitbucket workspace, accessible to the authenticated user, with options to filter by role or query string, and sort results. Responses are paginated; iterate using the `next` field in each response until it is absent to retrieve all repositories.', + }, + 'pipelinesAndDeployments.listRepositoriesEnvironments': { + riskLevel: 'read', + description: + 'List all deployment environments configured for a Bitbucket repository. Use when you need to view available environments for deployments, check environment configurations, or select an environment for deployment operations.', + }, + 'sourceAndRefs.listRepositoryPaths': { + riskLevel: 'read', + description: + 'Lists file and directory entries under a repository path at a given revision, with optional breadth-first recursion via max_depth for repository traversal and scanning. Fails if the path points to a file rather than a directory.', + }, + 'snippets.listSnippets': { + riskLevel: 'read', + description: + 'Returns all snippets accessible to the authenticated user. Use when you need to discover or list snippets, optionally filtered by role (owner, contributor, or member).', + }, + 'sourceAndRefs.listTags': { + riskLevel: 'read', + description: + 'Lists tags in a Bitbucket repository with optional server-side filtering by name pattern or commit hash (BBQL) and sorting. Use when you need to discover available tags, search for specific tag patterns, or find tags pointing to specific commits.', + }, + 'issues.listVersions': { + riskLevel: 'read', + description: + "Lists versions (milestones) in a Bitbucket repository's issue tracker. Use when you need to discover available versions for associating with issues, or to retrieve version IDs for use with create_issue.", + }, + 'workspacesAndProjects.listWorkspaceMembers': { + riskLevel: 'read', + description: + 'Lists all members of a specified Bitbucket workspace; the workspace must exist.', + }, + 'workspacesAndProjects.listWorkspaceProjects': { + riskLevel: 'read', + description: + 'Lists projects in a specified Bitbucket workspace. Use when you need to retrieve all projects belonging to a workspace.', + }, + 'workspacesAndProjects.listWorkspaces': { + riskLevel: 'read', + description: + 'Lists Bitbucket workspaces accessible to the authenticated user, optionally filtered and sorted. Results are paginated; follow the `next` field in each response to retrieve subsequent pages until `next` is absent. When multiple workspaces are returned, verify the correct `slug` or UUID before passing to downstream tools.', + }, + 'pullRequests.requestPullRequestChanges': { + riskLevel: 'write', + description: + 'Tool to request changes on a pull request as the authenticated user. Use when you need to formally request changes in a pull request review process, indicating the PR needs modifications before approval.', + }, + 'searchAndDiscovery.searchTeamCode': { + riskLevel: 'read', + description: + 'Search for code in repositories of a specified team. Matches can occur in file content or paths. Note: Teams endpoints were deprecated in Oct 2020; use workspace search endpoints for new integrations.', + }, + 'searchAndDiscovery.searchUserRepositoriesCode': { + riskLevel: 'read', + description: + 'Tool to search for code in the repositories of a specified user. Use when you need to find specific code patterns, functions, or text across all repositories owned by a user.', + }, + 'searchAndDiscovery.getWorkspacesSearchCode': { + riskLevel: 'read', + description: + 'Tool to search for code in the repositories of the specified workspace. Use when you need to find specific code patterns, function definitions, or text across all repositories in a workspace.', + }, + 'issues.updateIssue': { + riskLevel: 'write', + description: + 'Updates an existing issue in a Bitbucket repository by modifying specified attributes; requires `workspace`, `repo_slug`, `issue_id`, and at least one attribute to update.', + }, + 'commitsAndInsights.updateRepositoriesCommitComments': { + riskLevel: 'write', + description: + "Updates the contents of a comment on a commit. Use when you need to modify an existing comment's text on a commit.", + }, + 'commitsAndInsights.updateInsightsProjectsReposCommitsReports': { + riskLevel: 'write', + description: + "Create or update an insight report for a commit. Creates a new report if it doesn't exist, or replaces the existing one if a report already exists for the given repository, commit, and report key. Note: replacing an existing report will be rejected if the authenticated user was not the creator of the specified report.", + }, + 'commitsAndInsights.updateRepositoriesCommitReportsAnnotations': { + riskLevel: 'write', + description: + 'Creates or updates an individual annotation for a commit report. Use when you need to add or modify code analysis findings (vulnerabilities, code smells, or bugs) identified in a commit.', + }, + 'pipelinesAndDeployments.updateTeamsPipelinesConfigVariables': { + riskLevel: 'write', + description: + 'Updates a team-level pipeline configuration variable in Bitbucket. Use when you need to modify existing environment variables or configuration values that are available to all pipelines within a team.', + }, + 'pipelinesAndDeployments.updateUsersPipelinesConfigVariables': { + riskLevel: 'write', + description: + 'Updates a user-level pipeline variable for Bitbucket pipelines. Use when you need to modify account-level configuration variables such as changing the value, key name, or security status.', + }, +} as const satisfies RequiredPluginEndpointMeta< + typeof bitbucketEndpointsNested +>; +const defaultAuthType = 'oauth_2' as const; +export type BaseBitbucketPlugin = + CorsairPlugin< + 'bitbucket', + typeof BitbucketSchema, + typeof bitbucketEndpointsNested, + {}, + T, + typeof defaultAuthType + >; +export type InternalBitbucketPlugin = + BaseBitbucketPlugin; +export type ExternalBitbucketPlugin = + BaseBitbucketPlugin; +export function bitbucket( + incomingOptions: BitbucketPluginOptions & T = {} as BitbucketPluginOptions & + T, +): ExternalBitbucketPlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + return { + id: 'bitbucket', + schema: BitbucketSchema, + options, + authConfig: bitbucketAuthConfig, + oauthConfig: { + providerName: 'Bitbucket', + authUrl: 'https://bitbucket.org/site/oauth2/authorize', + tokenUrl: 'https://bitbucket.org/site/oauth2/access_token', + scopes: [ + 'account', + 'email', + 'repository', + 'repository:write', + 'repository:admin', + 'repository:delete', + 'pullrequest', + 'pullrequest:write', + 'issue', + 'issue:write', + 'snippet', + 'snippet:write', + 'project', + 'pipeline', + 'pipeline:write', + 'pipeline:variable', + 'runner', + ], + }, + hooks: options.hooks, + endpoints: bitbucketEndpointsNested, + webhooks: {}, + endpointMeta: bitbucketEndpointMeta, + endpointSchemas: bitbucketEndpointSchemas, + pluginWebhookMatcher: undefined, + errorHandlers: { ...errorHandlers, ...options.errorHandlers }, + keyBuilder: async (ctx: BitbucketKeyBuilderContext, source) => { + if (source === 'endpoint' && options.key) return options.key; + if (source !== 'endpoint' || ctx.authType !== 'oauth_2') + throw new AuthMissingError('bitbucket', 'oauth_2'); + const [accessToken, expiresAt, refreshToken, credentials] = + await Promise.all([ + ctx.keys.get_access_token(), + ctx.keys.get_expires_at(), + ctx.keys.get_refresh_token(), + ctx.keys.get_integration_credentials(), + ]); + const result = await getValidBitbucketAccessToken({ + accessToken, + expiresAt, + refreshToken, + clientId: credentials.client_id, + clientSecret: credentials.client_secret, + }); + if (result.refreshed) + await Promise.all([ + ctx.keys.set_access_token(result.accessToken), + ctx.keys.set_expires_at(String(result.expiresAt)), + result.refreshToken + ? ctx.keys.set_refresh_token(result.refreshToken) + : Promise.resolve(), + ]); + ( + ctx as unknown as { _refreshAuth?: () => Promise } + )._refreshAuth = async () => { + const fresh = await getValidBitbucketAccessToken({ + refreshToken: result.refreshToken ?? refreshToken, + clientId: credentials.client_id, + clientSecret: credentials.client_secret, + forceRefresh: true, + }); + await Promise.all([ + ctx.keys.set_access_token(fresh.accessToken), + ctx.keys.set_expires_at(String(fresh.expiresAt)), + fresh.refreshToken + ? ctx.keys.set_refresh_token(fresh.refreshToken) + : Promise.resolve(), + ]); + return fresh.accessToken; + }; + return result.accessToken; + }, + } satisfies InternalBitbucketPlugin; +} +export { bitbucketEndpointsNested }; +export type { + BitbucketEndpointInputs, + BitbucketEndpointOutputs, +} from './endpoints/types'; diff --git a/packages/bitbucket/integration.test.ts b/packages/bitbucket/integration.test.ts new file mode 100644 index 000000000..bd1dc9d10 --- /dev/null +++ b/packages/bitbucket/integration.test.ts @@ -0,0 +1,13 @@ +import { bitbucket } from './index'; + +describe('Bitbucket integration', () => { + it('uses OAuth 2.0 and intentionally exposes no webhooks', () => { + const plugin = bitbucket({ key: 'test-token', authType: 'oauth_2' }); + expect(plugin.options?.authType).toBe('oauth_2'); + expect(plugin.oauthConfig?.authUrl).toBe( + 'https://bitbucket.org/site/oauth2/authorize', + ); + expect(plugin.webhooks).toEqual({}); + expect(plugin.pluginWebhookMatcher).toBeUndefined(); + }); +}); diff --git a/packages/bitbucket/jest.config.cjs b/packages/bitbucket/jest.config.cjs new file mode 100644 index 000000000..9ff5b24ff --- /dev/null +++ b/packages/bitbucket/jest.config.cjs @@ -0,0 +1,39 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: ['**/*.test.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + verbatimModuleSyntax: false, + module: 'ESNext', + moduleResolution: 'Bundler', + }, + }, + ], + '.*\\.js$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { esModuleInterop: true, allowSyntheticDefaultImports: true }, + }, + ], + }, + moduleNameMapper: { + '^corsair/core$': '/../corsair/core.ts', + '^corsair/http$': '/../corsair/http.ts', + '^(\\.\\.?/.*)\\.js$': '$1', + }, + transformIgnorePatterns: ['node_modules/(?!.*uuid.*)'], + extensionsToTreatAsEsm: ['.ts'], + testTimeout: 30000, + watchman: false, + verbose: true, +}; diff --git a/packages/bitbucket/package.json b/packages/bitbucket/package.json new file mode 100644 index 000000000..ad1e1fb58 --- /dev/null +++ b/packages/bitbucket/package.json @@ -0,0 +1,44 @@ +{ + "name": "@corsair-dev/bitbucket", + "version": "0.1.0", + "description": "Bitbucket Cloud OAuth 2.0 plugin for Corsair", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "dev-source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --build --force && tsup", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "peerDependencies": { + "corsair": ">=0.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "corsair": "workspace:*", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "bitbucket", + "oauth" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/bitbucket/routing.test.ts b/packages/bitbucket/routing.test.ts new file mode 100644 index 000000000..1b4e84a6e --- /dev/null +++ b/packages/bitbucket/routing.test.ts @@ -0,0 +1,93 @@ +import { request } from 'corsair/http'; +import { buildBitbucketWireRequest } from './endpoints/factory'; +import { bitbucketOperationCatalog } from './endpoints/operations'; +import { BitbucketEndpointInputSchemas } from './endpoints/types'; +import { bitbucket } from './index'; + +jest.mock('corsair/http', () => { + const actual = jest.requireActual('corsair/http'); + return { ...actual, request: jest.fn() }; +}); +const mockRequest = request as jest.MockedFunction; +type Endpoint = (ctx: unknown, input: unknown) => Promise; +function context() { + return { + key: 'test-access-token', + options: {}, + db: {}, + database: undefined, + $getAccountId: jest.fn().mockResolvedValue('test-account'), + }; +} +describe('Bitbucket routing coverage', () => { + const plugin = bitbucket({ key: 'test-access-token' }); + const groups = plugin.endpoints as unknown as Record< + string, + Record + >; + beforeEach(() => mockRequest.mockReset()); + it('registers exactly 104 unique supplied operations', () => { + expect(bitbucketOperationCatalog).toHaveLength(104); + expect(new Set(bitbucketOperationCatalog.map((row) => row.code)).size).toBe( + 104, + ); + expect(new Set(bitbucketOperationCatalog.map((row) => row.path)).size).toBe( + 104, + ); + }); + it.each(bitbucketOperationCatalog)( + '$code validates and dispatches $httpMethod $apiPath', + async (operation) => { + const parsed = BitbucketEndpointInputSchemas[operation.key].parse( + operation.exampleInput, + ); + const wire = buildBitbucketWireRequest(operation, parsed); + expect(wire.url).not.toMatch(/[{}]/); + expect(wire.url).not.toContain('undefined'); + expect(wire.method).toBe(operation.httpMethod); + expect(wire.retrySafe).toBe(operation.riskLevel === 'read'); + mockRequest.mockResolvedValueOnce( + operation.responseKind === 'empty' + ? undefined + : operation.exampleOutput, + ); + const endpoint = groups[operation.group]?.[operation.key]; + expect(typeof endpoint).toBe('function'); + await endpoint?.(context(), parsed); + expect(mockRequest).toHaveBeenCalledTimes(1); + const [config, requestOptions, transport] = + mockRequest.mock.calls[0] ?? []; + expect(config?.HEADERS).toMatchObject({ + Authorization: 'Bearer test-access-token', + }); + expect(requestOptions?.method).toBe(operation.httpMethod); + expect(requestOptions?.url).toBe(wire.url); + expect(transport?.rateLimitConfig?.maxRetries).toBe( + operation.riskLevel === 'read' ? 3 : 0, + ); + }, + ); + it('filters project repositories by the requested project key', () => { + const operation = bitbucketOperationCatalog.find( + (row) => row.code === 'BITBUCKET_GET_PROJECTS_REPOS', + ); + expect(operation).toBeDefined(); + const wire = buildBitbucketWireRequest(operation!, { + workspace: 'team', + project_key: 'PROJ', + }); + expect(wire.query).toMatchObject({ q: 'project.key="PROJ"' }); + }); + it('marks every DELETE that permanently removes data as destructive and irreversible', () => { + const permanent = bitbucketOperationCatalog.filter( + (row) => row.riskLevel === 'destructive', + ); + expect(permanent.map((row) => row.code)).toEqual( + expect.arrayContaining([ + 'BITBUCKET_DELETE_REPOSITORY', + 'BITBUCKET_DELETE_ISSUE', + 'BITBUCKET_DELETE_COMMIT_COMMENT', + ]), + ); + }); +}); diff --git a/packages/bitbucket/schema/database.ts b/packages/bitbucket/schema/database.ts new file mode 100644 index 000000000..04976508c --- /dev/null +++ b/packages/bitbucket/schema/database.ts @@ -0,0 +1,2 @@ +// Bitbucket source, comment, email, and secret-variable content is deliberately not persisted. +export {}; diff --git a/packages/bitbucket/schema/index.ts b/packages/bitbucket/schema/index.ts new file mode 100644 index 000000000..b600d6629 --- /dev/null +++ b/packages/bitbucket/schema/index.ts @@ -0,0 +1 @@ +export const BitbucketSchema = { version: '1.0.0', entities: {} } as const; diff --git a/packages/bitbucket/tsconfig.json b/packages/bitbucket/tsconfig.json new file mode 100644 index 000000000..15e507a13 --- /dev/null +++ b/packages/bitbucket/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node", "jest"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "incremental": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["./**/*"], + "exclude": ["dist", "node_modules"], + "references": [] +} diff --git a/packages/bitbucket/tsup.config.ts b/packages/bitbucket/tsup.config.ts new file mode 100644 index 000000000..ded6521c4 --- /dev/null +++ b/packages/bitbucket/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; +export default defineConfig({ + clean: false, + dts: false, + format: ['esm'], + target: 'esnext', + platform: 'node', + bundle: true, + splitting: true, + minify: true, + outDir: 'dist', + external: ['corsair', 'zod'], + entry: ['index.ts'], +}); diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index 41011f6e7..5a017b9f8 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -39,6 +39,7 @@ export const BaseProviders = [ 'apisports', 'asana', 'ayrshare', + 'bitbucket', 'bitwarden', 'bluesky', 'boloforms', @@ -169,6 +170,7 @@ export const ProviderDisplayNames = { apisports: 'API-Sports', asana: 'Asana', ayrshare: 'Ayrshare', + bitbucket: 'Bitbucket', bitwarden: 'Bitwarden', bluesky: 'Bluesky', boloforms: 'Boloforms', @@ -306,6 +308,7 @@ export type AllProviders = | 'apisports' | 'asana' | 'ayrshare' + | 'bitbucket' | 'bitwarden' | 'bluesky' | 'boloforms' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5f3e3a29..89d388bd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -970,6 +970,30 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/bitbucket: + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + corsair: + specifier: workspace:* + version: link:../corsair + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.0)(jest-util@30.4.1)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + tsup: + specifier: ^8.0.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: 'catalog:' + version: 5.9.3 + zod: + specifier: 4.4.3 + version: 4.4.3 + packages/bitwarden: devDependencies: '@types/jest': @@ -3749,7 +3773,7 @@ importers: version: link:../packages/slack '@next/third-parties': specifier: 15.3.2 - version: 15.3.2(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) + version: 15.3.2(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) '@phosphor-icons/react': specifier: ^2.1.10 version: 2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -3773,7 +3797,7 @@ importers: version: 11.8.1(typescript@5.9.3) better-auth: specifier: ^1.5.5 - version: 1.6.15(@cloudflare/workers-types@4.20251121.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.5.0)(drizzle-kit@0.31.8)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7))(mysql2@3.15.3)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.21.0)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 1.6.15(@cloudflare/workers-types@4.20251121.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.5.0)(drizzle-kit@0.31.8)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7))(mysql2@3.15.3)(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.21.0)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -3791,13 +3815,13 @@ importers: version: 0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7) inngest: specifier: ^3.54.0 - version: 3.54.2(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(express@5.2.1)(hono@4.12.2)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(zod@4.4.3) + version: 3.54.2(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(express@5.2.1)(hono@4.12.2)(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(zod@4.4.3) next: specifier: ^15.3.2 - version: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) next-sanity: specifier: ^11.6.13 - version: 11.6.13(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(@sanity/types@6.2.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 11.6.13(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(@sanity/types@4.22.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) pg: specifier: ^8.20.0 version: 8.21.0 @@ -17829,9 +17853,9 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.18': optional: true - '@next/third-parties@15.3.2(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': + '@next/third-parties@15.3.2(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': dependencies: - next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 third-party-capital: 1.0.20 @@ -19994,20 +20018,6 @@ snapshots: - '@emotion/is-prop-valid' - styled-components - '@sanity/insert-menu@2.1.0(@emotion/is-prop-valid@1.4.0)(@sanity/types@6.2.0(@types/react@19.2.6))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': - dependencies: - '@sanity/icons': 3.7.4(react@19.2.5) - '@sanity/types': 6.2.0(@types/react@19.2.6) - '@sanity/ui': 3.2.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) - lodash: 4.18.1 - react: 19.2.5 - react-compiler-runtime: 1.0.0(react@19.2.5) - react-dom: 19.2.5(react@19.2.5) - react-is: 19.2.7 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - styled-components - '@sanity/json-match@1.0.5': {} '@sanity/logos@2.2.2(react@19.2.5)': @@ -20104,14 +20114,6 @@ snapshots: - '@sanity/client' - '@sanity/types' - '@sanity/presentation-comlink@2.1.0(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6))': - dependencies: - '@sanity/comlink': 4.0.1 - '@sanity/visual-editing-types': 1.1.8(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6)) - transitivePeerDependencies: - - '@sanity/client' - - '@sanity/types' - '@sanity/preview-url-secret@3.0.0(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))': dependencies: '@sanity/client': 7.23.0 @@ -20332,10 +20334,10 @@ snapshots: - react-dom - react-is - '@sanity/visual-editing-csm@2.0.26(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6))(typescript@5.9.3)': + '@sanity/visual-editing-csm@2.0.26(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6))(typescript@5.9.3)': dependencies: '@sanity/client': 7.23.0 - '@sanity/visual-editing-types': 1.1.8(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6)) + '@sanity/visual-editing-types': 1.1.8(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6)) valibot: 1.4.1(typescript@5.9.3) transitivePeerDependencies: - '@sanity/types' @@ -20347,22 +20349,16 @@ snapshots: optionalDependencies: '@sanity/types': 4.22.0(@types/react@19.2.6) - '@sanity/visual-editing-types@1.1.8(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6))': - dependencies: - '@sanity/client': 7.23.0 - optionalDependencies: - '@sanity/types': 6.2.0(@types/react@19.2.6) - - '@sanity/visual-editing@4.0.3(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)': + '@sanity/visual-editing@4.0.3(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)': dependencies: '@sanity/comlink': 4.0.1 '@sanity/icons': 3.7.4(react@19.2.5) - '@sanity/insert-menu': 2.1.0(@emotion/is-prop-valid@1.4.0)(@sanity/types@6.2.0(@types/react@19.2.6))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@sanity/insert-menu': 2.1.0(@emotion/is-prop-valid@1.4.0)(@sanity/types@4.22.0(@types/react@19.2.6))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@sanity/mutate': 0.11.0-canary.4(xstate@5.32.2) - '@sanity/presentation-comlink': 2.1.0(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6)) + '@sanity/presentation-comlink': 2.1.0(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6)) '@sanity/preview-url-secret': 3.0.0(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0)) '@sanity/ui': 3.2.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) - '@sanity/visual-editing-csm': 2.0.26(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6))(typescript@5.9.3) + '@sanity/visual-editing-csm': 2.0.26(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6))(typescript@5.9.3) '@vercel/stega': 1.0.0 react: 19.2.5 react-compiler-runtime: 1.0.0(react@19.2.5) @@ -20375,7 +20371,7 @@ snapshots: xstate: 5.32.2 optionalDependencies: '@sanity/client': 7.23.0 - next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - '@emotion/is-prop-valid' - '@sanity/types' @@ -21419,7 +21415,7 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.6.15(@cloudflare/workers-types@4.20251121.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.5.0)(drizzle-kit@0.31.8)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7))(mysql2@3.15.3)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.21.0)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + better-auth@1.6.15(@cloudflare/workers-types@4.20251121.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.5.0)(drizzle-kit@0.31.8)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7))(mysql2@3.15.3)(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.21.0)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251121.0)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.1)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/drizzle-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251121.0)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.1)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7)) @@ -21444,7 +21440,7 @@ snapshots: drizzle-kit: 0.31.8 drizzle-orm: 0.44.7(@cloudflare/workers-types@4.20251121.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.5.0)(bun-types@1.3.3)(gel@2.2.0)(kysely@0.28.17)(mysql2@3.15.3)(pg@8.21.0)(postgres@3.4.7)(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(sqlite3@5.1.7) mysql2: 3.15.3 - next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) pg: 8.21.0 prisma: 6.19.0(magicast@0.3.5)(typescript@5.9.3) react: 19.2.5 @@ -23329,7 +23325,7 @@ snapshots: - encoding - supports-color - inngest@3.54.2(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(express@5.2.1)(hono@4.12.2)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(zod@4.4.3): + inngest@3.54.2(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(express@5.2.1)(hono@4.12.2)(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(zod@4.4.3): dependencies: '@bufbuild/protobuf': 2.11.0 '@inngest/ai': 0.1.7 @@ -23360,7 +23356,7 @@ snapshots: optionalDependencies: express: 5.2.1 hono: 4.12.2 - next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) typescript: 5.9.3 transitivePeerDependencies: - '@opentelemetry/core' @@ -24609,18 +24605,18 @@ snapshots: neo-async@2.6.2: {} - next-sanity@11.6.13(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(@sanity/types@6.2.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): + next-sanity@11.6.13(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(@sanity/types@4.22.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): dependencies: '@portabletext/react': 6.2.0(react@19.2.5) '@sanity/client': 7.23.0 '@sanity/comlink': 4.0.1 - '@sanity/presentation-comlink': 2.1.0(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6)) + '@sanity/presentation-comlink': 2.1.0(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6)) '@sanity/preview-url-secret': 3.0.0(@sanity/client@7.23.0)(@sanity/icons@3.7.4(react@19.2.5))(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0)) - '@sanity/visual-editing': 4.0.3(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/types@6.2.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + '@sanity/visual-editing': 4.0.3(@emotion/is-prop-valid@1.4.0)(@sanity/client@7.23.0)(@sanity/types@4.22.0(@types/react@19.2.6))(next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.7)(react@19.2.5)(sanity@4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0))(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) dequal: 2.0.3 groq: 4.22.0 history: 5.3.0 - next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) sanity: 4.22.0(@emotion/is-prop-valid@1.4.0)(@portabletext/sanity-bridge@1.2.14(@types/react@19.2.6))(@types/node@24.10.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(jiti@2.7.0)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) @@ -24664,7 +24660,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@15.5.18(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 15.5.18 '@swc/helpers': 0.5.15 @@ -24672,7 +24668,7 @@ snapshots: postcss: 8.4.31 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.5) + styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.5) optionalDependencies: '@next/swc-darwin-arm64': 15.5.18 '@next/swc-darwin-x64': 15.5.18 @@ -25204,7 +25200,7 @@ snapshots: postcss@8.5.6: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -26523,12 +26519,12 @@ snapshots: client-only: 0.0.1 react: 18.3.1 - styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.5): + styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.5): dependencies: client-only: 0.0.1 react: 19.2.5 optionalDependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.28.6 stylis@4.3.6: {} From b085cc30504c7f5f893dd157998866642e67c323 Mon Sep 17 00:00:00 2001 From: abhishek-2k23 Date: Sun, 16 Aug 2026 23:01:42 +0530 Subject: [PATCH 2/5] feat(bitbucket): addressed bitbucket issues --- packages/bitbucket/client.test.ts | 30 ++++++++++ packages/bitbucket/client.ts | 5 +- packages/bitbucket/endpoints/factory.ts | 69 +++++++++++++++------ packages/bitbucket/endpoints/operations.ts | 7 ++- packages/bitbucket/endpoints/types.ts | 1 + packages/bitbucket/index.ts | 69 ++++++++++++++------- packages/bitbucket/routing.test.ts | 70 +++++++++++++++++++--- 7 files changed, 196 insertions(+), 55 deletions(-) diff --git a/packages/bitbucket/client.test.ts b/packages/bitbucket/client.test.ts index 481f55627..8a39cb627 100644 --- a/packages/bitbucket/client.test.ts +++ b/packages/bitbucket/client.test.ts @@ -54,4 +54,34 @@ describe('Bitbucket OAuth client', () => { expect(result).toEqual({ ok: true }); expect(refresh).toHaveBeenCalledTimes(1); }); + it('propagates a second 401 after a single refresh and retry', async () => { + const refresh = jest.fn().mockResolvedValue('fresh'); + mockRequest + .mockRejectedValueOnce(new BitbucketAPIError('unauthorized', 401)) + .mockRejectedValueOnce(new BitbucketAPIError('unauthorized', 401)); + await expect( + makeAuthenticatedBitbucketRequest( + '/user', + { key: 'stale', _refreshAuth: refresh }, + { method: 'GET', retrySafe: true }, + ), + ).rejects.toThrow('unauthorized'); + expect(refresh).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); + it('propagates a 500 without refreshing the token', async () => { + const refresh = jest.fn().mockResolvedValue('fresh'); + mockRequest.mockRejectedValueOnce( + new BitbucketAPIError('server error', 500), + ); + await expect( + makeAuthenticatedBitbucketRequest( + '/user', + { key: 'stale', _refreshAuth: refresh }, + { method: 'GET', retrySafe: true }, + ), + ).rejects.toThrow('server error'); + expect(refresh).not.toHaveBeenCalled(); + expect(mockRequest).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/bitbucket/client.ts b/packages/bitbucket/client.ts index c06db9f8a..e75c9b0df 100644 --- a/packages/bitbucket/client.ts +++ b/packages/bitbucket/client.ts @@ -63,8 +63,9 @@ export async function refreshBitbucketAccessToken( clientSecret: string, refreshToken: string, ): Promise { + const tokenUrl = new URL(BITBUCKET_TOKEN_URL); const config: OpenAPIConfig = { - BASE: 'https://bitbucket.org', + BASE: tokenUrl.origin, VERSION: '2', WITH_CREDENTIALS: false, CREDENTIALS: 'omit', @@ -85,7 +86,7 @@ export async function refreshBitbucketAccessToken( config, { method: 'POST', - url: '/site/oauth2/access_token', + url: tokenUrl.pathname, body, mediaType: 'application/x-www-form-urlencoded', }, diff --git a/packages/bitbucket/endpoints/factory.ts b/packages/bitbucket/endpoints/factory.ts index 3b0964e71..b325063e3 100644 --- a/packages/bitbucket/endpoints/factory.ts +++ b/packages/bitbucket/endpoints/factory.ts @@ -42,10 +42,31 @@ function parseWithSchema( issues, ); } +// Bitbucket templates only ever span several URL segments for these parameters: +// `{path}` is a repository file path and `{commit}`/`{revision}`/`{revspec}`/ +// `{spec}` are refs, which may be branch names such as `feature/login`. Every +// other parameter is a single identifier and stays fully percent-encoded. +const multiSegmentPathParams = new Set([ + 'path', + 'commit', + 'revision', + 'revspec', + 'spec', +]); function pathValue(value: unknown, name: string): string { if (typeof value !== 'string' && typeof value !== 'number') throw new Error('[BITBUCKET] Missing required path parameter: ' + name); - return encodeURIComponent(String(value)).replaceAll('%2F', '/'); + const raw = String(value); + if (raw.split(/[/\\]/).some((segment) => segment === '.' || segment === '..')) + throw new Error( + '[BITBUCKET] Path parameter ' + + name + + ' must not contain "." or ".." segments', + ); + const encoded = encodeURIComponent(raw); + return multiSegmentPathParams.has(name) + ? encoded.replaceAll('%2F', '/') + : encoded; } export function buildBitbucketWireRequest( definition: BitbucketOperation, @@ -99,23 +120,33 @@ export function createBitbucketEndpoint( definition.path, ); const wire = buildBitbucketWireRequest(definition, input); - const raw = await makeAuthenticatedBitbucketRequest( - wire.url, - ctx as unknown as BitbucketAuthContext, - wire, - ); - const response = parseWithSchema( - BitbucketEndpointOutputSchemas[key], - raw, - 'output', - definition.path, - ); - await logEventFromContext( - ctx, - 'bitbucket.' + definition.path, - bitbucketAuditPayload(input), - 'completed', - ); - return response; + const auditPayload = bitbucketAuditPayload(input); + const eventType = 'bitbucket.' + definition.path; + try { + const raw = await makeAuthenticatedBitbucketRequest( + wire.url, + ctx as unknown as BitbucketAuthContext, + wire, + ); + const response = parseWithSchema( + BitbucketEndpointOutputSchemas[key], + raw, + 'output', + definition.path, + ); + await logEventFromContext(ctx, eventType, auditPayload, 'completed'); + return response; + } catch (error) { + await logEventFromContext( + ctx, + eventType, + { + ...auditPayload, + error: error instanceof Error ? error.message : String(error), + }, + 'failed', + ); + throw error; + } }; } diff --git a/packages/bitbucket/endpoints/operations.ts b/packages/bitbucket/endpoints/operations.ts index 55e0551f4..a1298f4d7 100644 --- a/packages/bitbucket/endpoints/operations.ts +++ b/packages/bitbucket/endpoints/operations.ts @@ -2892,16 +2892,17 @@ export const bitbucketOperationCatalog = [ queryFields: [], fixedPathValues: {}, projectFilterField: undefined, - acceptsBody: false, - bodyRequired: false, + acceptsBody: true, + bodyRequired: true, responseKind: 'json', - mediaType: undefined, + mediaType: 'application/json', scopes: ['issue:write'], deprecated: true, exampleInput: { issue_id: 1, repo_slug: 'repository', workspace: 'workspace', + body: { title: 'Updated issue title' }, }, exampleOutput: {}, }, diff --git a/packages/bitbucket/endpoints/types.ts b/packages/bitbucket/endpoints/types.ts index 5da2bc4ac..c85acc8dd 100644 --- a/packages/bitbucket/endpoints/types.ts +++ b/packages/bitbucket/endpoints/types.ts @@ -751,6 +751,7 @@ export const BitbucketEndpointInputSchemas = { issue_id: z.union([z.string(), z.number().int()]), repo_slug: z.string(), workspace: z.string(), + body: BitbucketRequestBodySchema, }) .strict(), updateRepositoriesCommitComments: z diff --git a/packages/bitbucket/index.ts b/packages/bitbucket/index.ts index e26b25adc..36f298b0b 100644 --- a/packages/bitbucket/index.ts +++ b/packages/bitbucket/index.ts @@ -11,7 +11,11 @@ import type { RequiredPluginEndpointSchemas, } from 'corsair/core'; import { AuthMissingError } from 'corsair/core'; -import { getValidBitbucketAccessToken } from './client'; +import { + BITBUCKET_AUTH_URL, + BITBUCKET_TOKEN_URL, + getValidBitbucketAccessToken, +} from './client'; import { BitbucketEndpoints } from './endpoints'; import { BitbucketEndpointInputSchemas, @@ -30,6 +34,12 @@ export type BitbucketPluginOptions = { hooks?: InternalBitbucketPlugin['hooks']; errorHandlers?: CorsairErrorHandler; permissions?: PluginPermissionsConfig; + /** + * OAuth scopes requested during authorization. Defaults to + * `defaultBitbucketScopes`, which covers every operation in the catalog; + * narrow it to only the scopes the operations you actually call need. + */ + scopes?: readonly string[]; }; export type BitbucketContext = CorsairPluginContext< typeof BitbucketSchema, @@ -1009,6 +1019,33 @@ const bitbucketEndpointMeta = { typeof bitbucketEndpointsNested >; const defaultAuthType = 'oauth_2' as const; +/** + * Scopes requested when the caller does not pass `options.scopes`. Every scope + * here is required by at least one catalog operation — `repository:admin` by + * `createRepository`, `repository:delete` by `deleteRepository`, + * `pipeline:variable` by the pipeline variable operations and `runner` by + * `getRepositoriesPipelinesConfigRunners` — so dropping one disables those + * endpoints for the connection. + */ +export const defaultBitbucketScopes = [ + 'account', + 'email', + 'repository', + 'repository:write', + 'repository:admin', + 'repository:delete', + 'pullrequest', + 'pullrequest:write', + 'issue', + 'issue:write', + 'snippet', + 'snippet:write', + 'project', + 'pipeline', + 'pipeline:write', + 'pipeline:variable', + 'runner', +] as const; export type BaseBitbucketPlugin = CorsairPlugin< 'bitbucket', @@ -1037,27 +1074,9 @@ export function bitbucket( authConfig: bitbucketAuthConfig, oauthConfig: { providerName: 'Bitbucket', - authUrl: 'https://bitbucket.org/site/oauth2/authorize', - tokenUrl: 'https://bitbucket.org/site/oauth2/access_token', - scopes: [ - 'account', - 'email', - 'repository', - 'repository:write', - 'repository:admin', - 'repository:delete', - 'pullrequest', - 'pullrequest:write', - 'issue', - 'issue:write', - 'snippet', - 'snippet:write', - 'project', - 'pipeline', - 'pipeline:write', - 'pipeline:variable', - 'runner', - ], + authUrl: BITBUCKET_AUTH_URL, + tokenUrl: BITBUCKET_TOKEN_URL, + scopes: [...(options.scopes ?? defaultBitbucketScopes)], }, hooks: options.hooks, endpoints: bitbucketEndpointsNested, @@ -1095,8 +1114,12 @@ export function bitbucket( ( ctx as unknown as { _refreshAuth?: () => Promise } )._refreshAuth = async () => { + // Read the persisted token on every call: Bitbucket rotates refresh + // tokens, so a token captured when the key was built goes stale after + // the first refresh. + const storedRefreshToken = await ctx.keys.get_refresh_token(); const fresh = await getValidBitbucketAccessToken({ - refreshToken: result.refreshToken ?? refreshToken, + refreshToken: storedRefreshToken ?? refreshToken, clientId: credentials.client_id, clientSecret: credentials.client_secret, forceRefresh: true, diff --git a/packages/bitbucket/routing.test.ts b/packages/bitbucket/routing.test.ts index 1b4e84a6e..5a9de20d6 100644 --- a/packages/bitbucket/routing.test.ts +++ b/packages/bitbucket/routing.test.ts @@ -79,15 +79,69 @@ describe('Bitbucket routing coverage', () => { expect(wire.query).toMatchObject({ q: 'project.key="PROJ"' }); }); it('marks every DELETE that permanently removes data as destructive and irreversible', () => { - const permanent = bitbucketOperationCatalog.filter( - (row) => row.riskLevel === 'destructive', + const byRisk = (level: string) => + bitbucketOperationCatalog + .filter((row) => row.riskLevel === level) + .map((row) => row.code) + .sort(); + expect(byRisk('destructive')).toEqual([ + 'BITBUCKET_DELETE_COMMIT_COMMENT', + 'BITBUCKET_DELETE_ISSUE', + 'BITBUCKET_DELETE_REPOSITORIES_COMMIT_REPORTS_ANNOTATIONS', + 'BITBUCKET_DELETE_REPOSITORY', + 'BITBUCKET_DELETE_USER_PIPELINE_VARIABLE', + ]); + // Every destructive operation is a DELETE, and the only DELETE that is not + // destructive is unwatching a snippet, which removes no data. + expect( + bitbucketOperationCatalog + .filter((row) => row.riskLevel === 'destructive') + .every((row) => row.httpMethod === 'DELETE'), + ).toBe(true); + expect( + bitbucketOperationCatalog + .filter( + (row) => + row.httpMethod === 'DELETE' && row.riskLevel !== 'destructive', + ) + .map((row) => row.code), + ).toEqual(['BITBUCKET_DELETE_SNIPPETS_WATCH']); + }); + it('forwards the request body for operations that accept one', () => { + const operation = bitbucketOperationCatalog.find( + (row) => row.code === 'BITBUCKET_UPDATE_ISSUE', + ); + expect(operation).toBeDefined(); + const parsed = BitbucketEndpointInputSchemas.updateIssue.parse({ + workspace: 'team', + repo_slug: 'repository', + issue_id: 1, + body: { title: 'Updated issue title' }, + }); + const wire = buildBitbucketWireRequest(operation!, parsed); + expect(wire.body).toEqual({ title: 'Updated issue title' }); + }); + it('rejects dot segments in path parameters and only spans segments for refs and file paths', () => { + const browse = bitbucketOperationCatalog.find( + (row) => row.code === 'BITBUCKET_BROWSE_REPOSITORY_PATH', ); - expect(permanent.map((row) => row.code)).toEqual( - expect.arrayContaining([ - 'BITBUCKET_DELETE_REPOSITORY', - 'BITBUCKET_DELETE_ISSUE', - 'BITBUCKET_DELETE_COMMIT_COMMENT', - ]), + expect(browse).toBeDefined(); + expect(() => + buildBitbucketWireRequest(browse!, { + workspace: 'team', + repo_slug: 'repository', + commit: 'main', + path: '../../../user', + }), + ).toThrow(/must not contain/); + const wire = buildBitbucketWireRequest(browse!, { + workspace: 'team/other', + repo_slug: 'repository', + commit: 'feature/login', + path: 'src/index.ts', + }); + expect(wire.url).toBe( + '/repositories/team%2Fother/repository/src/feature/login/src/index.ts', ); }); }); From 9882431df463e9f5ea17bcff8efe8ee36663227c Mon Sep 17 00:00:00 2001 From: abhishek-2k23 Date: Mon, 17 Aug 2026 01:52:45 +0530 Subject: [PATCH 3/5] feat(bitbucket): enhance issue update handling and validation --- packages/bitbucket/endpoints/operations.ts | 2 +- packages/bitbucket/endpoints/types.ts | 58 +++++++++++- packages/bitbucket/index.ts | 61 ++++++++---- packages/bitbucket/integration.test.ts | 104 +++++++++++++++++++++ packages/bitbucket/routing.test.ts | 55 +++++++++++ 5 files changed, 259 insertions(+), 21 deletions(-) diff --git a/packages/bitbucket/endpoints/operations.ts b/packages/bitbucket/endpoints/operations.ts index a1298f4d7..e7faeb5e5 100644 --- a/packages/bitbucket/endpoints/operations.ts +++ b/packages/bitbucket/endpoints/operations.ts @@ -2879,7 +2879,7 @@ export const bitbucketOperationCatalog = [ code: 'BITBUCKET_UPDATE_ISSUE', title: 'Update an issue', description: - 'Updates an existing issue in a Bitbucket repository by modifying specified attributes; requires `workspace`, `repo_slug`, `issue_id`, and at least one attribute to update.', + 'Updates an existing issue in a Bitbucket repository by modifying specified attributes; requires `workspace`, `repo_slug`, `issue_id`, and a `body` setting at least one of `title`, `content`, `state`, `kind`, `priority`, `assignee`, `milestone`, `component` or `version`. An empty body is rejected because it would leave the issue unchanged.', providerOperationId: 'PUT /repositories/{workspace}/{repo_slug}/issues/{issue_id}', key: 'updateIssue', diff --git a/packages/bitbucket/endpoints/types.ts b/packages/bitbucket/endpoints/types.ts index c85acc8dd..16ac193dc 100644 --- a/packages/bitbucket/endpoints/types.ts +++ b/packages/bitbucket/endpoints/types.ts @@ -4,6 +4,62 @@ export const BitbucketRequestBodySchema = z.union([ z.object({}).loose(), z.array(z.object({}).loose()), ]); +/** + * Attributes `PUT /repositories/{workspace}/{repo_slug}/issues/{issue_id}` + * updates. Bitbucket requires at least one of them — a payload without any is + * either rejected or silently leaves the issue unchanged — so the body schema + * below refuses `{}` and bodies that carry only unrecognized keys. + */ +export const bitbucketIssueUpdateAttributes = [ + 'title', + 'content', + 'state', + 'kind', + 'priority', + 'assignee', + 'milestone', + 'component', + 'version', +] as const; +export const BitbucketIssueUpdateBodySchema = z + .object({ + title: z.string().min(1).optional(), + content: z.object({ raw: z.string() }).loose().optional(), + state: z + .enum([ + 'new', + 'open', + 'resolved', + 'on hold', + 'invalid', + 'duplicate', + 'wontfix', + 'closed', + ]) + .optional(), + kind: z.enum(['bug', 'enhancement', 'proposal', 'task']).optional(), + priority: z + .enum(['trivial', 'minor', 'major', 'critical', 'blocker']) + .optional(), + // `null` clears the field; an object selects one (by uuid, account_id or name). + assignee: z.object({}).loose().nullable().optional(), + milestone: z.object({}).loose().nullable().optional(), + component: z.object({}).loose().nullable().optional(), + version: z.object({}).loose().nullable().optional(), + }) + .loose() + .refine( + (body) => + bitbucketIssueUpdateAttributes.some( + (attribute) => body[attribute] !== undefined, + ), + { + message: + 'must set at least one issue attribute to update (' + + bitbucketIssueUpdateAttributes.join(', ') + + ')', + }, + ); export const BitbucketResponseSchema = z.union([ z.object({}).loose(), z.array(z.unknown()), @@ -751,7 +807,7 @@ export const BitbucketEndpointInputSchemas = { issue_id: z.union([z.string(), z.number().int()]), repo_slug: z.string(), workspace: z.string(), - body: BitbucketRequestBodySchema, + body: BitbucketIssueUpdateBodySchema, }) .strict(), updateRepositoriesCommitComments: z diff --git a/packages/bitbucket/index.ts b/packages/bitbucket/index.ts index 36f298b0b..ac049cad8 100644 --- a/packages/bitbucket/index.ts +++ b/packages/bitbucket/index.ts @@ -1019,6 +1019,28 @@ const bitbucketEndpointMeta = { typeof bitbucketEndpointsNested >; const defaultAuthType = 'oauth_2' as const; +/** + * In-flight forced refresh per connection. Bitbucket rotates refresh tokens, so + * two concurrent refreshes would present the same token twice and the loser + * fails with `invalid_grant`. Endpoint calls of one binding share a single keys + * manager instance, which makes it a stable per-connection scope: the first + * caller runs the refresh and persists the rotated token, and everyone who + * arrives while it is running awaits that same result. + */ +const inFlightBitbucketRefresh = new WeakMap>(); +function refreshBitbucketTokenOnce( + scope: object, + run: () => Promise, +): Promise { + const existing = inFlightBitbucketRefresh.get(scope); + if (existing) return existing; + const pending = run().finally(() => { + if (inFlightBitbucketRefresh.get(scope) === pending) + inFlightBitbucketRefresh.delete(scope); + }); + inFlightBitbucketRefresh.set(scope, pending); + return pending; +} /** * Scopes requested when the caller does not pass `options.scopes`. Every scope * here is required by at least one catalog operation — `repository:admin` by @@ -1113,26 +1135,27 @@ export function bitbucket( ]); ( ctx as unknown as { _refreshAuth?: () => Promise } - )._refreshAuth = async () => { - // Read the persisted token on every call: Bitbucket rotates refresh - // tokens, so a token captured when the key was built goes stale after - // the first refresh. - const storedRefreshToken = await ctx.keys.get_refresh_token(); - const fresh = await getValidBitbucketAccessToken({ - refreshToken: storedRefreshToken ?? refreshToken, - clientId: credentials.client_id, - clientSecret: credentials.client_secret, - forceRefresh: true, + )._refreshAuth = () => + refreshBitbucketTokenOnce(ctx.keys, async () => { + // Read the persisted token on every call: Bitbucket rotates refresh + // tokens, so a token captured when the key was built goes stale after + // the first refresh. + const storedRefreshToken = await ctx.keys.get_refresh_token(); + const fresh = await getValidBitbucketAccessToken({ + refreshToken: storedRefreshToken ?? refreshToken, + clientId: credentials.client_id, + clientSecret: credentials.client_secret, + forceRefresh: true, + }); + await Promise.all([ + ctx.keys.set_access_token(fresh.accessToken), + ctx.keys.set_expires_at(String(fresh.expiresAt)), + fresh.refreshToken + ? ctx.keys.set_refresh_token(fresh.refreshToken) + : Promise.resolve(), + ]); + return fresh.accessToken; }); - await Promise.all([ - ctx.keys.set_access_token(fresh.accessToken), - ctx.keys.set_expires_at(String(fresh.expiresAt)), - fresh.refreshToken - ? ctx.keys.set_refresh_token(fresh.refreshToken) - : Promise.resolve(), - ]); - return fresh.accessToken; - }; return result.accessToken; }, } satisfies InternalBitbucketPlugin; diff --git a/packages/bitbucket/integration.test.ts b/packages/bitbucket/integration.test.ts index bd1dc9d10..c5560e58f 100644 --- a/packages/bitbucket/integration.test.ts +++ b/packages/bitbucket/integration.test.ts @@ -1,6 +1,44 @@ +import { request } from 'corsair/http'; import { bitbucket } from './index'; +jest.mock('corsair/http', () => { + const actual = jest.requireActual('corsair/http'); + return { ...actual, request: jest.fn() }; +}); +const mockRequest = request as jest.MockedFunction; +type RefreshableContext = { _refreshAuth?: () => Promise }; +type KeyBuilderFn = (ctx: unknown, source: 'endpoint') => Promise; +function buildKey(plugin: { keyBuilder?: unknown }, ctx: unknown) { + return (plugin.keyBuilder as unknown as KeyBuilderFn)(ctx, 'endpoint'); +} +function keyBuilderContext() { + const stored = { + access_token: 'stored-access', + expires_at: String(Math.floor(Date.now() / 1000) + 1800), + refresh_token: 'stored-refresh', + }; + const keys = { + get_access_token: jest.fn(async () => stored.access_token), + get_expires_at: jest.fn(async () => stored.expires_at), + get_refresh_token: jest.fn(async () => stored.refresh_token), + get_integration_credentials: jest.fn(async () => ({ + client_id: 'client', + client_secret: 'secret', + })), + set_access_token: jest.fn(async (value: string) => { + stored.access_token = value; + }), + set_expires_at: jest.fn(async (value: string) => { + stored.expires_at = value; + }), + set_refresh_token: jest.fn(async (value: string) => { + stored.refresh_token = value; + }), + }; + return { ctx: { authType: 'oauth_2', keys }, keys, stored }; +} describe('Bitbucket integration', () => { + beforeEach(() => mockRequest.mockReset()); it('uses OAuth 2.0 and intentionally exposes no webhooks', () => { const plugin = bitbucket({ key: 'test-token', authType: 'oauth_2' }); expect(plugin.options?.authType).toBe('oauth_2'); @@ -10,4 +48,70 @@ describe('Bitbucket integration', () => { expect(plugin.webhooks).toEqual({}); expect(plugin.pluginWebhookMatcher).toBeUndefined(); }); + it('requests defaultBitbucketScopes unless the caller narrows them', () => { + expect(bitbucket({}).oauthConfig?.scopes).toContain('repository:delete'); + expect( + bitbucket({ scopes: ['account', 'repository'] }).oauthConfig?.scopes, + ).toEqual(['account', 'repository']); + }); + it('collapses concurrent refreshes into a single token rotation', async () => { + const plugin = bitbucket({}); + const { ctx, keys, stored } = keyBuilderContext(); + await buildKey(plugin, ctx); + const refreshAuth = (ctx as RefreshableContext)._refreshAuth; + expect(typeof refreshAuth).toBe('function'); + expect(mockRequest).not.toHaveBeenCalled(); + let resolveRefresh: ((value: unknown) => void) | undefined; + mockRequest.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveRefresh = resolve; + }) as ReturnType, + ); + const inFlight = [refreshAuth?.(), refreshAuth?.(), refreshAuth?.()]; + await Promise.resolve(); + resolveRefresh?.({ + access_token: 'rotated-access', + refresh_token: 'rotated-refresh', + expires_in: 3600, + }); + const tokens = await Promise.all(inFlight); + expect(tokens).toEqual([ + 'rotated-access', + 'rotated-access', + 'rotated-access', + ]); + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(keys.set_refresh_token).toHaveBeenCalledTimes(1); + expect(stored.refresh_token).toBe('rotated-refresh'); + }); + it('refreshes again after an in-flight refresh settles', async () => { + const plugin = bitbucket({}); + const { ctx } = keyBuilderContext(); + await buildKey(plugin, ctx); + const refreshAuth = (ctx as RefreshableContext)._refreshAuth; + mockRequest + .mockResolvedValueOnce({ access_token: 'first', expires_in: 3600 }) + .mockResolvedValueOnce({ access_token: 'second', expires_in: 3600 }); + await expect(refreshAuth?.()).resolves.toBe('first'); + await expect(refreshAuth?.()).resolves.toBe('second'); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); + it('propagates a failed refresh to every concurrent caller and recovers', async () => { + const plugin = bitbucket({}); + const { ctx } = keyBuilderContext(); + await buildKey(plugin, ctx); + const refreshAuth = (ctx as RefreshableContext)._refreshAuth; + mockRequest.mockRejectedValueOnce(new Error('invalid_grant')); + const failures = [refreshAuth?.(), refreshAuth?.()]; + await expect(Promise.all(failures)).rejects.toThrow('invalid_grant'); + await Promise.allSettled(failures); + expect(mockRequest).toHaveBeenCalledTimes(1); + mockRequest.mockResolvedValueOnce({ + access_token: 'recovered', + expires_in: 3600, + }); + await expect(refreshAuth?.()).resolves.toBe('recovered'); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/bitbucket/routing.test.ts b/packages/bitbucket/routing.test.ts index 5a9de20d6..2f4af25ef 100644 --- a/packages/bitbucket/routing.test.ts +++ b/packages/bitbucket/routing.test.ts @@ -121,6 +121,61 @@ describe('Bitbucket routing coverage', () => { const wire = buildBitbucketWireRequest(operation!, parsed); expect(wire.body).toEqual({ title: 'Updated issue title' }); }); + describe('updateIssue body contract', () => { + const parse = (body: unknown) => + BitbucketEndpointInputSchemas.updateIssue.safeParse({ + workspace: 'team', + repo_slug: 'repository', + issue_id: 1, + body, + }); + it('rejects a body that updates nothing', () => { + for (const body of [ + {}, + { unknown_attribute: 'value' }, + { title: undefined }, + ]) { + const result = parse(body); + expect(result.success).toBe(false); + expect(result.error?.issues[0]?.message).toContain( + 'at least one issue attribute', + ); + } + }); + it('rejects a missing body outright', () => { + expect(parse(undefined).success).toBe(false); + }); + it('accepts every documented attribute on its own', () => { + const bodies: Record[] = [ + { title: 'New title' }, + { content: { raw: 'Updated description' } }, + { state: 'resolved' }, + { kind: 'bug' }, + { priority: 'critical' }, + { assignee: { uuid: '{some-uuid}' } }, + { assignee: null }, + { milestone: { name: '1.0' } }, + { component: { name: 'api' } }, + { version: { name: '2.1' } }, + ]; + for (const body of bodies) expect(parse(body).success).toBe(true); + }); + it('keeps unrecognized keys once a known attribute is present', () => { + const result = parse({ state: 'closed', future_field: 'kept' }); + expect(result.success).toBe(true); + expect(result.data?.body).toEqual({ + state: 'closed', + future_field: 'kept', + }); + }); + it('rejects attribute values Bitbucket does not accept', () => { + expect(parse({ title: '' }).success).toBe(false); + expect(parse({ state: 'Resolved' }).success).toBe(false); + expect(parse({ kind: 'defect' }).success).toBe(false); + expect(parse({ priority: 'urgent' }).success).toBe(false); + expect(parse({ content: 'raw text' }).success).toBe(false); + }); + }); it('rejects dot segments in path parameters and only spans segments for refs and file paths', () => { const browse = bitbucketOperationCatalog.find( (row) => row.code === 'BITBUCKET_BROWSE_REPOSITORY_PATH', From 2b5c2b2d20ffdbe459603fa83937f66a76c167b8 Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 18 Aug 2026 04:26:08 +0530 Subject: [PATCH 4/5] fix(bitbucket): match rest 2.0 schema and pagination --- packages/bitbucket/endpoints/operations.ts | 118 ++++---- packages/bitbucket/endpoints/types.ts | 125 +++++++- packages/bitbucket/index.ts | 33 ++- packages/bitbucket/integration.test.ts | 24 ++ packages/bitbucket/routing.test.ts | 69 +++++ packages/bitbucket/schema.test.ts | 294 +++++++++++++++++++ packages/bitbucket/schema/database.ts | 314 ++++++++++++++++++++- packages/bitbucket/schema/index.ts | 34 ++- packages/bitbucket/schema/primitives.ts | 11 + 9 files changed, 949 insertions(+), 73 deletions(-) create mode 100644 packages/bitbucket/schema.test.ts create mode 100644 packages/bitbucket/schema/primitives.ts diff --git a/packages/bitbucket/endpoints/operations.ts b/packages/bitbucket/endpoints/operations.ts index e7faeb5e5..46e0678e9 100644 --- a/packages/bitbucket/endpoints/operations.ts +++ b/packages/bitbucket/endpoints/operations.ts @@ -44,7 +44,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', riskLevel: 'read', pathFields: ['commit', 'path', 'repo_slug', 'workspace'], - queryFields: ['format', 'q', 'sort', 'max_depth'], + queryFields: ['format', 'q', 'sort', 'max_depth', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -117,7 +117,10 @@ export const bitbucketOperationCatalog = [ exampleInput: { repo_slug: 'repository', workspace: 'workspace', - body: {}, + body: { + name: 'feature', + target: { hash: 'a'.repeat(40) }, + }, }, exampleOutput: {}, }, @@ -139,7 +142,7 @@ export const bitbucketOperationCatalog = [ fixedPathValues: {}, projectFilterField: undefined, acceptsBody: true, - bodyRequired: false, + bodyRequired: true, responseKind: 'json', mediaType: 'application/json', scopes: ['pullrequest:write'], @@ -147,7 +150,10 @@ export const bitbucketOperationCatalog = [ exampleInput: { repo_slug: 'repository', workspace: 'workspace', - body: {}, + body: { + title: 'Fix pagination', + source: { branch: { name: 'feature' } }, + }, }, exampleOutput: {}, }, @@ -176,7 +182,7 @@ export const bitbucketOperationCatalog = [ exampleInput: { repo_slug: 'repository', workspace: 'workspace', - body: {}, + body: { title: 'Cannot clone over HTTPS' }, }, exampleOutput: {}, }, @@ -620,7 +626,15 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/diffstat/{spec}', riskLevel: 'read', pathFields: ['repo_slug', 'spec', 'workspace'], - queryFields: ['ignore_whitespace', 'merge', 'path', 'renames', 'topic'], + queryFields: [ + 'ignore_whitespace', + 'merge', + 'path', + 'renames', + 'topic', + 'page', + 'pagelen', + ], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -687,7 +701,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}/reports', riskLevel: 'read', pathFields: ['workspace', 'repo_slug', 'commit'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -776,7 +790,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/commits', riskLevel: 'read', pathFields: ['pull_request_id', 'repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -838,7 +852,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat', riskLevel: 'read', pathFields: ['pull_request_id', 'repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1016,7 +1030,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/users/{selected_user}/ssh-keys', riskLevel: 'read', pathFields: ['selected_user'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1044,7 +1058,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/workspaces/{workspace}/pullrequests/{selected_user}', riskLevel: 'read', pathFields: ['selected_user', 'workspace'], - queryFields: ['state'], + queryFields: ['state', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1135,7 +1149,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}/comments', riskLevel: 'read', pathFields: ['commit', 'repo_slug', 'workspace'], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1234,7 +1248,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses', riskLevel: 'read', pathFields: ['commit', 'repo_slug', 'workspace'], - queryFields: ['refname', 'q', 'sort'], + queryFields: ['refname', 'q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1289,7 +1303,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/deployments_config/environments/{environment_uuid}/variables', riskLevel: 'read', pathFields: ['workspace', 'repo_slug', 'environment_uuid'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1349,7 +1363,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/filehistory/{commit}/{path}', riskLevel: 'read', pathFields: ['commit', 'path', 'repo_slug', 'workspace'], - queryFields: ['renames', 'q', 'sort'], + queryFields: ['renames', 'q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1380,7 +1394,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', riskLevel: 'read', pathFields: ['commit', 'path', 'repo_slug', 'workspace'], - queryFields: ['format', 'q', 'sort', 'max_depth'], + queryFields: ['format', 'q', 'sort', 'max_depth', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1410,7 +1424,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/hook_events/{subject_type}', riskLevel: 'read', pathFields: ['subject_type'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1438,7 +1452,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps', riskLevel: 'read', pathFields: ['workspace', 'repo_slug', 'pipeline_uuid'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1467,7 +1481,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}', riskLevel: 'read', pathFields: ['workspace'], - queryFields: ['role', 'q', 'sort'], + queryFields: ['role', 'q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: 'project_key', acceptsBody: false, @@ -1529,7 +1543,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments', riskLevel: 'read', pathFields: ['pull_request_id', 'repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1560,7 +1574,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/statuses', riskLevel: 'read', pathFields: ['pull_request_id', 'repo_slug', 'workspace'], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1590,7 +1604,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/pullrequests/activity', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1649,7 +1663,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/src', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: ['format'], + queryFields: ['format', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1706,7 +1720,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pipelines_config/ssh/known_hosts', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1734,7 +1748,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/pipelines-config/runners', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1762,7 +1776,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/pipelines_config/schedules', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1790,7 +1804,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/pipelines_config/variables', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1818,7 +1832,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/pipelines-config/caches', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1846,7 +1860,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/refs', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -1874,7 +1888,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/watchers', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2096,7 +2110,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/user/permissions/repositories', riskLevel: 'read', pathFields: [], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2121,7 +2135,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/user/permissions/workspaces', riskLevel: 'read', pathFields: [], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2146,7 +2160,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/user/workspaces', riskLevel: 'read', pathFields: [], - queryFields: ['sort', 'administrator'], + queryFields: ['sort', 'administrator', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2198,7 +2212,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories', riskLevel: 'read', pathFields: [], - queryFields: ['after', 'role', 'q', 'sort'], + queryFields: ['after', 'role', 'q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2224,7 +2238,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/refs/branches', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2252,7 +2266,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commits', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2281,7 +2295,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commits/{revision}', riskLevel: 'read', pathFields: ['repo_slug', 'revision', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2311,7 +2325,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commits/{revision}', riskLevel: 'write', pathFields: ['repo_slug', 'revision', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: true, @@ -2342,7 +2356,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/commits/{revision}', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: { revision: 'master', }, @@ -2372,7 +2386,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/deployments', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2400,7 +2414,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/issues', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2472,7 +2486,7 @@ export const bitbucketOperationCatalog = [ '/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/tasks', riskLevel: 'read', pathFields: ['pull_request_id', 'repo_slug', 'workspace'], - queryFields: ['q', 'sort', 'pagelen'], + queryFields: ['q', 'sort', 'pagelen', 'page'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2502,7 +2516,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/pullrequests', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: ['state'], + queryFields: ['state', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2530,7 +2544,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}', riskLevel: 'read', pathFields: ['workspace'], - queryFields: ['role', 'q', 'sort'], + queryFields: ['role', 'q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2557,7 +2571,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/environments', riskLevel: 'read', pathFields: ['workspace', 'repo_slug'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2586,7 +2600,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/src/{commit}/{path}', riskLevel: 'read', pathFields: ['commit', 'path', 'repo_slug', 'workspace'], - queryFields: ['format', 'q', 'sort', 'max_depth'], + queryFields: ['format', 'q', 'sort', 'max_depth', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2616,7 +2630,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/snippets', riskLevel: 'read', pathFields: [], - queryFields: ['role'], + queryFields: ['role', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2641,7 +2655,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/refs/tags', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: ['q', 'sort'], + queryFields: ['q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2669,7 +2683,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/repositories/{workspace}/{repo_slug}/versions', riskLevel: 'read', pathFields: ['repo_slug', 'workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2697,7 +2711,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/workspaces/{workspace}/members', riskLevel: 'read', pathFields: ['workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2724,7 +2738,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/workspaces/{workspace}/projects', riskLevel: 'read', pathFields: ['workspace'], - queryFields: [], + queryFields: ['page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, @@ -2751,7 +2765,7 @@ export const bitbucketOperationCatalog = [ apiPath: '/workspaces', riskLevel: 'read', pathFields: [], - queryFields: ['role', 'q', 'sort'], + queryFields: ['role', 'q', 'sort', 'page', 'pagelen'], fixedPathValues: {}, projectFilterField: undefined, acceptsBody: false, diff --git a/packages/bitbucket/endpoints/types.ts b/packages/bitbucket/endpoints/types.ts index 16ac193dc..75834247f 100644 --- a/packages/bitbucket/endpoints/types.ts +++ b/packages/bitbucket/endpoints/types.ts @@ -4,6 +4,30 @@ export const BitbucketRequestBodySchema = z.union([ z.object({}).loose(), z.array(z.object({}).loose()), ]); +/** POST /repositories/{workspace}/{repo_slug}/refs/branches */ +export const BitbucketCreateBranchBodySchema = z + .object({ + name: z.string().min(1), + target: z.object({ hash: z.string().min(1) }).loose(), + }) + .loose(); +/** POST /repositories/{workspace}/{repo_slug}/pullrequests */ +export const BitbucketCreatePullRequestBodySchema = z + .object({ + title: z.string().min(1), + source: z + .object({ + branch: z.object({ name: z.string().min(1) }).loose(), + }) + .loose(), + }) + .loose(); +/** POST /repositories/{workspace}/{repo_slug}/issues — title is the only required element. */ +export const BitbucketCreateIssueBodySchema = z + .object({ + title: z.string().min(1), + }) + .loose(); /** * Attributes `PUT /repositories/{workspace}/{repo_slug}/issues/{issue_id}` * updates. Bitbucket requires at least one of them — a payload without any is @@ -87,6 +111,8 @@ export const BitbucketEndpointInputSchemas = { q: z.string().optional(), sort: z.string().optional(), max_depth: z.number().int().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesIssuesVote: z @@ -100,21 +126,21 @@ export const BitbucketEndpointInputSchemas = { .object({ repo_slug: z.string(), workspace: z.string(), - body: BitbucketRequestBodySchema, + body: BitbucketCreateBranchBodySchema, }) .strict(), createPullRequest: z .object({ repo_slug: z.string(), workspace: z.string(), - body: BitbucketRequestBodySchema.optional(), + body: BitbucketCreatePullRequestBodySchema, }) .strict(), createIssue: z .object({ repo_slug: z.string(), workspace: z.string(), - body: BitbucketRequestBodySchema, + body: BitbucketCreateIssueBodySchema, }) .strict(), createIssueComment: z @@ -229,6 +255,8 @@ export const BitbucketEndpointInputSchemas = { path: z.string().optional(), renames: z.boolean().optional(), topic: z.boolean().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getCommitDiff: z @@ -250,6 +278,8 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), repo_slug: z.string(), commit: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getOpenidConfiguration: z @@ -269,6 +299,8 @@ export const BitbucketEndpointInputSchemas = { pull_request_id: z.union([z.string(), z.number().int()]), repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getPullRequestDiff: z @@ -283,6 +315,8 @@ export const BitbucketEndpointInputSchemas = { pull_request_id: z.union([z.string(), z.number().int()]), repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesMergeBase: z @@ -322,6 +356,8 @@ export const BitbucketEndpointInputSchemas = { getSshLatestKeys: z .object({ selected_user: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getWorkspacesPullrequests: z @@ -329,6 +365,8 @@ export const BitbucketEndpointInputSchemas = { selected_user: z.string(), workspace: z.string(), state: z.enum(['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED']).optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getBranch: z @@ -353,6 +391,8 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesCommitReport: z @@ -380,6 +420,8 @@ export const BitbucketEndpointInputSchemas = { refname: z.string().optional(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getCurrentUser2: z.object({}).strict(), @@ -388,6 +430,8 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), repo_slug: z.string(), environment_uuid: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesEffectiveBranchingModel: z @@ -405,6 +449,8 @@ export const BitbucketEndpointInputSchemas = { renames: z.string().optional(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getFileFromRepository: z @@ -417,11 +463,15 @@ export const BitbucketEndpointInputSchemas = { q: z.string().optional(), sort: z.string().optional(), max_depth: z.number().int().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getHookEvents: z .object({ subject_type: z.enum(['repository', 'workspace']), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPipelinesSteps: z @@ -429,6 +479,8 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), repo_slug: z.string(), pipeline_uuid: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getProjectsRepos: z @@ -438,6 +490,8 @@ export const BitbucketEndpointInputSchemas = { q: z.string().optional(), sort: z.string().optional(), project_key: z.string().min(1), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getPullRequestComment: z @@ -453,6 +507,8 @@ export const BitbucketEndpointInputSchemas = { pull_request_id: z.union([z.string(), z.number().int()]), repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPullrequestsStatuses: z @@ -462,12 +518,16 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPullrequestsActivity: z .object({ repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRawFileContent: z @@ -487,6 +547,8 @@ export const BitbucketEndpointInputSchemas = { repo_slug: z.string(), workspace: z.string(), format: z.enum(['meta']).optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepository: z @@ -499,30 +561,40 @@ export const BitbucketEndpointInputSchemas = { .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPipelinesConfigRunners: z .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPipelinesConfigSchedules: z .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPipelinesConfigVariables: z .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesPipelinesConfigCaches: z .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesRefs: z @@ -531,12 +603,16 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getRepositoriesWatchers: z .object({ repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getSnippet: z @@ -580,18 +656,24 @@ export const BitbucketEndpointInputSchemas = { .object({ q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getUserPermissionsWorkspaces: z .object({ q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getUserWorkspaces: z .object({ sort: z.string().optional(), administrator: z.boolean().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), getWorkspace: z @@ -605,6 +687,8 @@ export const BitbucketEndpointInputSchemas = { role: z.enum(['admin', 'contributor', 'member', 'owner']).optional(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listBranches: z @@ -613,12 +697,16 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listCommits: z .object({ repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listCommitsFromRevision: z @@ -626,6 +714,8 @@ export const BitbucketEndpointInputSchemas = { repo_slug: z.string(), revision: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), createRepositoriesCommits2: z @@ -634,24 +724,32 @@ export const BitbucketEndpointInputSchemas = { revision: z.string(), workspace: z.string(), body: BitbucketRequestBodySchema.optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listCommitsOnMaster: z .object({ repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listDeployments: z .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listIssues: z .object({ repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listPipelines: z @@ -700,6 +798,7 @@ export const BitbucketEndpointInputSchemas = { q: z.string().optional(), sort: z.string().optional(), pagelen: z.number().int().optional(), + page: z.number().int().positive().optional(), }) .strict(), listPullRequests: z @@ -707,6 +806,8 @@ export const BitbucketEndpointInputSchemas = { repo_slug: z.string(), workspace: z.string(), state: z.enum(['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED']).optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listRepositoriesInWorkspace: z @@ -715,12 +816,16 @@ export const BitbucketEndpointInputSchemas = { role: z.enum(['admin', 'contributor', 'member', 'owner']).optional(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listRepositoriesEnvironments: z .object({ workspace: z.string(), repo_slug: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listRepositoryPaths: z @@ -733,11 +838,15 @@ export const BitbucketEndpointInputSchemas = { q: z.string().optional(), sort: z.string().optional(), max_depth: z.number().int().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listSnippets: z .object({ role: z.enum(['owner', 'contributor', 'member']).optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listTags: z @@ -746,22 +855,30 @@ export const BitbucketEndpointInputSchemas = { workspace: z.string(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listVersions: z .object({ repo_slug: z.string(), workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listWorkspaceMembers: z .object({ workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listWorkspaceProjects: z .object({ workspace: z.string(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), listWorkspaces: z @@ -769,6 +886,8 @@ export const BitbucketEndpointInputSchemas = { role: z.enum(['owner', 'collaborator', 'member']).optional(), q: z.string().optional(), sort: z.string().optional(), + page: z.number().int().positive().optional(), + pagelen: z.number().int().positive().optional(), }) .strict(), requestPullRequestChanges: z diff --git a/packages/bitbucket/index.ts b/packages/bitbucket/index.ts index ac049cad8..a84ce0a5a 100644 --- a/packages/bitbucket/index.ts +++ b/packages/bitbucket/index.ts @@ -1118,21 +1118,24 @@ export function bitbucket( ctx.keys.get_refresh_token(), ctx.keys.get_integration_credentials(), ]); - const result = await getValidBitbucketAccessToken({ - accessToken, - expiresAt, - refreshToken, - clientId: credentials.client_id, - clientSecret: credentials.client_secret, + const result = await refreshBitbucketTokenOnce(ctx.keys, async () => { + const token = await getValidBitbucketAccessToken({ + accessToken, + expiresAt, + refreshToken, + clientId: credentials.client_id, + clientSecret: credentials.client_secret, + }); + if (token.refreshed) + await Promise.all([ + ctx.keys.set_access_token(token.accessToken), + ctx.keys.set_expires_at(String(token.expiresAt)), + token.refreshToken + ? ctx.keys.set_refresh_token(token.refreshToken) + : Promise.resolve(), + ]); + return token.accessToken; }); - if (result.refreshed) - await Promise.all([ - ctx.keys.set_access_token(result.accessToken), - ctx.keys.set_expires_at(String(result.expiresAt)), - result.refreshToken - ? ctx.keys.set_refresh_token(result.refreshToken) - : Promise.resolve(), - ]); ( ctx as unknown as { _refreshAuth?: () => Promise } )._refreshAuth = () => @@ -1156,7 +1159,7 @@ export function bitbucket( ]); return fresh.accessToken; }); - return result.accessToken; + return result; }, } satisfies InternalBitbucketPlugin; } diff --git a/packages/bitbucket/integration.test.ts b/packages/bitbucket/integration.test.ts index c5560e58f..2fddf552e 100644 --- a/packages/bitbucket/integration.test.ts +++ b/packages/bitbucket/integration.test.ts @@ -54,6 +54,30 @@ describe('Bitbucket integration', () => { bitbucket({ scopes: ['account', 'repository'] }).oauthConfig?.scopes, ).toEqual(['account', 'repository']); }); + it('collapses concurrent expired-token builds into one refresh', async () => { + const plugin = bitbucket({}); + const { ctx, keys } = keyBuilderContext(); + ctx.keys.get_expires_at = jest.fn(async () => + String(Math.floor(Date.now() / 1000) - 10), + ); + mockRequest.mockResolvedValueOnce({ + access_token: 'rotated-access', + refresh_token: 'rotated-refresh', + expires_in: 3600, + }); + const tokens = await Promise.all([ + buildKey(plugin, ctx), + buildKey(plugin, ctx), + buildKey(plugin, ctx), + ]); + expect(tokens).toEqual([ + 'rotated-access', + 'rotated-access', + 'rotated-access', + ]); + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(keys.set_refresh_token).toHaveBeenCalledTimes(1); + }); it('collapses concurrent refreshes into a single token rotation', async () => { const plugin = bitbucket({}); const { ctx, keys, stored } = keyBuilderContext(); diff --git a/packages/bitbucket/routing.test.ts b/packages/bitbucket/routing.test.ts index 2f4af25ef..6ef431148 100644 --- a/packages/bitbucket/routing.test.ts +++ b/packages/bitbucket/routing.test.ts @@ -107,6 +107,75 @@ describe('Bitbucket routing coverage', () => { .map((row) => row.code), ).toEqual(['BITBUCKET_DELETE_SNIPPETS_WATCH']); }); + it('forwards page and pagelen on list endpoints', () => { + const operation = bitbucketOperationCatalog.find( + (row) => row.code === 'BITBUCKET_LIST_PULL_REQUESTS', + ); + expect(operation).toBeDefined(); + const wire = buildBitbucketWireRequest(operation!, { + workspace: 'team', + repo_slug: 'repository', + page: 2, + pagelen: 50, + }); + expect(wire.query).toMatchObject({ page: 2, pagelen: 50 }); + }); + it('requires title and source.branch.name to create a pull request', () => { + const parse = BitbucketEndpointInputSchemas.createPullRequest.safeParse; + expect( + parse({ + workspace: 'team', + repo_slug: 'repository', + }).success, + ).toBe(false); + expect( + parse({ + workspace: 'team', + repo_slug: 'repository', + body: { title: 'Fix' }, + }).success, + ).toBe(false); + expect( + parse({ + workspace: 'team', + repo_slug: 'repository', + body: { + title: 'Fix pagination', + source: { branch: { name: 'feature' } }, + }, + }).success, + ).toBe(true); + }); + it('requires title to create an issue and name plus target.hash to create a branch', () => { + expect( + BitbucketEndpointInputSchemas.createIssue.safeParse({ + workspace: 'team', + repo_slug: 'repository', + body: {}, + }).success, + ).toBe(false); + expect( + BitbucketEndpointInputSchemas.createIssue.safeParse({ + workspace: 'team', + repo_slug: 'repository', + body: { title: 'Cannot clone' }, + }).success, + ).toBe(true); + expect( + BitbucketEndpointInputSchemas.createBranch.safeParse({ + workspace: 'team', + repo_slug: 'repository', + body: { name: 'feature' }, + }).success, + ).toBe(false); + expect( + BitbucketEndpointInputSchemas.createBranch.safeParse({ + workspace: 'team', + repo_slug: 'repository', + body: { name: 'feature', target: { hash: 'abc123' } }, + }).success, + ).toBe(true); + }); it('forwards the request body for operations that accept one', () => { const operation = bitbucketOperationCatalog.find( (row) => row.code === 'BITBUCKET_UPDATE_ISSUE', diff --git a/packages/bitbucket/schema.test.ts b/packages/bitbucket/schema.test.ts new file mode 100644 index 000000000..f4be7c54c --- /dev/null +++ b/packages/bitbucket/schema.test.ts @@ -0,0 +1,294 @@ +/** + * Asserts every official / live-captured key is declared in `schema/database.ts`. + * + * Entities are `.loose()`, so an extra key still parses — `safeParse` alone + * would never notice a field the schema forgot. + * + * Official keys: https://developer.atlassian.com/cloud/bitbucket/swagger.v3.json + * Live extras: GET /2.0/workspaces/bitbucket and GET /2.0/repositories?pagelen=1 + */ + +import { BitbucketSchema } from './schema'; +import { + BitbucketAccount, + BitbucketBranchEntity, + BitbucketCommitEntity, + BitbucketIssueEntity, + BitbucketPipelineEntity, + BitbucketProjectEntity, + BitbucketPullRequestEntity, + BitbucketRepositoryEntity, + BitbucketSnippetEntity, + BitbucketTagEntity, + BitbucketUserEntity, + BitbucketWorkspaceEntity, +} from './schema/database'; + +const WORKSPACE_KEYS = [ + 'uuid', + 'type', + 'name', + 'slug', + 'is_private', + 'is_personal', + 'is_privacy_enforced', + 'forking_mode', + 'sign_system_commits', + 'created_on', + 'updated_on', + 'links', +]; +const USER_KEYS = [ + 'uuid', + 'type', + 'display_name', + 'nickname', + 'username', + 'account_id', + 'account_status', + 'created_on', + 'has_2fa_enabled', + 'is_staff', + 'links', +]; +const PROJECT_KEYS = [ + 'uuid', + 'type', + 'key', + 'name', + 'description', + 'is_private', + 'created_on', + 'updated_on', + 'has_publicly_visible_repos', + 'owner', + 'links', +]; +const REPOSITORY_KEYS = [ + 'uuid', + 'type', + 'name', + 'full_name', + 'slug', + 'description', + 'scm', + 'website', + 'language', + 'size', + 'is_private', + 'has_issues', + 'has_wiki', + 'fork_policy', + 'enforced_signed_commits', + 'created_on', + 'updated_on', + 'owner', + 'workspace', + 'project', + 'parent', + 'mainbranch', + 'override_settings', + 'links', +]; +const PULL_REQUEST_KEYS = [ + 'id', + 'type', + 'title', + 'state', + 'draft', + 'queued', + 'reason', + 'comment_count', + 'task_count', + 'close_source_branch', + 'created_on', + 'updated_on', + 'author', + 'closed_by', + 'source', + 'destination', + 'merge_commit', + 'reviewers', + 'participants', + 'summary', + 'rendered', + 'links', +]; +const ISSUE_KEYS = [ + 'id', + 'type', + 'title', + 'state', + 'kind', + 'priority', + 'votes', + 'created_on', + 'updated_on', + 'edited_on', + 'reporter', + 'assignee', + 'repository', + 'milestone', + 'version', + 'component', + 'content', + 'links', +]; +const SNIPPET_KEYS = [ + 'id', + 'type', + 'title', + 'scm', + 'is_private', + 'created_on', + 'updated_on', + 'owner', + 'creator', +]; +const PIPELINE_KEYS = [ + 'uuid', + 'type', + 'build_number', + 'created_on', + 'completed_on', + 'build_seconds_used', + 'creator', + 'repository', + 'target', + 'trigger', + 'state', + 'configuration_sources', + 'links', +]; +const COMMIT_KEYS = [ + 'hash', + 'type', + 'date', + 'message', + 'author', + 'committer', + 'summary', + 'rendered', + 'parents', + 'repository', + 'participants', + 'links', +]; +const BRANCH_KEYS = [ + 'name', + 'type', + 'target', + 'merge_strategies', + 'sync_strategies', + 'default_merge_strategy', + 'links', +]; +const TAG_KEYS = [ + 'name', + 'type', + 'message', + 'date', + 'target', + 'tagger', + 'links', +]; + +function declaredKeys(entity: { shape: Record }) { + return Object.keys(entity.shape); +} + +describe('Bitbucket schema', () => { + it('declares a semver version', () => { + expect(BitbucketSchema.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('mirrors the entities this plugin caches', () => { + expect(Object.keys(BitbucketSchema.entities).sort()).toEqual( + [ + 'branches', + 'commits', + 'issues', + 'pipelines', + 'projects', + 'pullRequests', + 'repositories', + 'snippets', + 'tags', + 'users', + 'workspaces', + ].sort(), + ); + }); + + describe('every official key is declared', () => { + const cases: [string, { shape: Record }, string[]][] = [ + ['workspace', BitbucketWorkspaceEntity, WORKSPACE_KEYS], + ['user', BitbucketUserEntity, USER_KEYS], + ['project', BitbucketProjectEntity, PROJECT_KEYS], + ['repository', BitbucketRepositoryEntity, REPOSITORY_KEYS], + ['pull request', BitbucketPullRequestEntity, PULL_REQUEST_KEYS], + ['issue', BitbucketIssueEntity, ISSUE_KEYS], + ['snippet', BitbucketSnippetEntity, SNIPPET_KEYS], + ['pipeline', BitbucketPipelineEntity, PIPELINE_KEYS], + ['commit', BitbucketCommitEntity, COMMIT_KEYS], + ['branch', BitbucketBranchEntity, BRANCH_KEYS], + ['tag', BitbucketTagEntity, TAG_KEYS], + ]; + for (const [label, entity, keys] of cases) { + it(`declares every ${label} key`, () => { + const declared = declaredKeys(entity); + expect(keys.filter((k) => !declared.includes(k))).toEqual([]); + }); + } + }); + + it('requires only the primary key', () => { + expect(BitbucketWorkspaceEntity.safeParse({ uuid: '{w}' }).success).toBe( + true, + ); + expect(BitbucketUserEntity.safeParse({ uuid: '{u}' }).success).toBe(true); + expect(BitbucketProjectEntity.safeParse({ uuid: '{p}' }).success).toBe( + true, + ); + expect(BitbucketRepositoryEntity.safeParse({ uuid: '{r}' }).success).toBe( + true, + ); + expect(BitbucketPullRequestEntity.safeParse({ id: 1 }).success).toBe(true); + expect(BitbucketIssueEntity.safeParse({ id: 1 }).success).toBe(true); + expect(BitbucketSnippetEntity.safeParse({ id: 1 }).success).toBe(true); + expect(BitbucketPipelineEntity.safeParse({ uuid: '{pipe}' }).success).toBe( + true, + ); + expect(BitbucketCommitEntity.safeParse({ hash: 'abc' }).success).toBe(true); + expect(BitbucketBranchEntity.safeParse({ name: 'main' }).success).toBe( + true, + ); + expect(BitbucketTagEntity.safeParse({ name: 'v1' }).success).toBe(true); + }); + + it('rejects a record with no primary key', () => { + expect(BitbucketRepositoryEntity.safeParse({ name: 'repo' }).success).toBe( + false, + ); + expect(BitbucketPullRequestEntity.safeParse({ title: 'pr' }).success).toBe( + false, + ); + }); + + it('keeps undeclared live extras because entities are loose', () => { + const parsed = BitbucketRepositoryEntity.safeParse({ + uuid: '{r}', + aKeyNobodyDeclared: 1, + }); + expect(parsed.success).toBe(true); + expect(declaredKeys(BitbucketRepositoryEntity)).not.toContain( + 'aKeyNobodyDeclared', + ); + }); + + it('does not model email or pipeline-variable values', () => { + expect(declaredKeys(BitbucketUserEntity)).not.toContain('email'); + expect(declaredKeys(BitbucketAccount)).not.toContain('email'); + expect(declaredKeys(BitbucketPipelineEntity)).not.toContain('variables'); + }); +}); diff --git a/packages/bitbucket/schema/database.ts b/packages/bitbucket/schema/database.ts index 04976508c..d6ad5aa3a 100644 --- a/packages/bitbucket/schema/database.ts +++ b/packages/bitbucket/schema/database.ts @@ -1,2 +1,312 @@ -// Bitbucket source, comment, email, and secret-variable content is deliberately not persisted. -export {}; +import { z } from 'zod'; +import { B, Id, N, NumId, Obj, S, UnknownArray } from './primitives'; + +/** + * Field names match official JSON keys. + * https://developer.atlassian.com/cloud/bitbucket/swagger.v3.json + * Live extras on repository/workspace confirmed against GET /2.0 public objects. + * + * Source blobs, comments, email addresses, and pipeline-variable values are + * not persisted. + */ + +export const BitbucketLinks = z + .record(z.string(), z.unknown()) + .nullable() + .optional(); + +export const BitbucketAccount = z + .object({ + type: S, + uuid: S, + display_name: S, + nickname: S, + username: S, + account_id: S, + account_status: S, + created_on: S, + has_2fa_enabled: B, + is_staff: B, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketAccount = z.infer; + +export const BitbucketRendered = z + .object({ + raw: S, + markup: S, + html: S, + }) + .loose(); +export type BitbucketRendered = z.infer; + +export const BitbucketBranchTarget = z + .object({ + hash: S, + type: S, + }) + .loose(); +export type BitbucketBranchTarget = z.infer; + +export const BitbucketWorkspaceEntity = z + .object({ + uuid: Id, + type: S, + name: S, + slug: S, + is_private: B, + is_personal: B, + is_privacy_enforced: B, + forking_mode: S, + sign_system_commits: B, + created_on: S, + updated_on: S, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketWorkspaceEntity = z.infer; + +export const BitbucketUserEntity = z + .object({ + uuid: Id, + type: S, + display_name: S, + nickname: S, + username: S, + account_id: S, + account_status: S, + created_on: S, + has_2fa_enabled: B, + is_staff: B, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketUserEntity = z.infer; + +export const BitbucketProjectEntity = z + .object({ + uuid: Id, + type: S, + key: S, + name: S, + description: S, + is_private: B, + created_on: S, + updated_on: S, + has_publicly_visible_repos: B, + owner: BitbucketAccount.nullable().optional(), + links: BitbucketLinks, + }) + .loose(); +export type BitbucketProjectEntity = z.infer; + +export const BitbucketRepositoryEntity = z + .object({ + uuid: Id, + type: S, + name: S, + full_name: S, + slug: S, + description: S, + scm: S, + website: S, + language: S, + size: N, + is_private: B, + has_issues: B, + has_wiki: B, + fork_policy: S, + enforced_signed_commits: B, + created_on: S, + updated_on: S, + owner: BitbucketAccount.nullable().optional(), + workspace: BitbucketWorkspaceEntity.nullable().optional(), + project: BitbucketProjectEntity.nullable().optional(), + parent: z + .object({ + uuid: S, + type: S, + name: S, + full_name: S, + slug: S, + }) + .loose() + .nullable() + .optional(), + mainbranch: z + .object({ + type: S, + name: S, + }) + .loose() + .nullable() + .optional(), + override_settings: Obj, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketRepositoryEntity = z.infer< + typeof BitbucketRepositoryEntity +>; + +export const BitbucketPullRequestEndpoint = z + .object({ + repository: BitbucketRepositoryEntity.nullable().optional(), + branch: z + .object({ + name: S, + }) + .loose() + .nullable() + .optional(), + commit: z + .object({ + hash: S, + }) + .loose() + .nullable() + .optional(), + }) + .loose(); +export type BitbucketPullRequestEndpoint = z.infer< + typeof BitbucketPullRequestEndpoint +>; + +export const BitbucketPullRequestEntity = z + .object({ + id: NumId, + type: S, + title: S, + state: S, + draft: B, + queued: B, + reason: S, + comment_count: N, + task_count: N, + close_source_branch: B, + created_on: S, + updated_on: S, + author: BitbucketAccount.nullable().optional(), + closed_by: BitbucketAccount.nullable().optional(), + source: BitbucketPullRequestEndpoint.nullable().optional(), + destination: BitbucketPullRequestEndpoint.nullable().optional(), + merge_commit: z + .object({ + hash: S, + }) + .loose() + .nullable() + .optional(), + reviewers: UnknownArray, + participants: UnknownArray, + summary: BitbucketRendered.nullable().optional(), + rendered: Obj, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketPullRequestEntity = z.infer< + typeof BitbucketPullRequestEntity +>; + +export const BitbucketIssueEntity = z + .object({ + id: NumId, + type: S, + title: S, + state: S, + kind: S, + priority: S, + votes: N, + created_on: S, + updated_on: S, + edited_on: S, + reporter: BitbucketAccount.nullable().optional(), + assignee: BitbucketAccount.nullable().optional(), + repository: BitbucketRepositoryEntity.nullable().optional(), + milestone: Obj, + version: Obj, + component: Obj, + content: BitbucketRendered.nullable().optional(), + links: BitbucketLinks, + }) + .loose(); +export type BitbucketIssueEntity = z.infer; + +export const BitbucketSnippetEntity = z + .object({ + id: NumId, + type: S, + title: S, + scm: S, + is_private: B, + created_on: S, + updated_on: S, + owner: BitbucketAccount.nullable().optional(), + creator: BitbucketAccount.nullable().optional(), + }) + .loose(); +export type BitbucketSnippetEntity = z.infer; + +export const BitbucketPipelineEntity = z + .object({ + uuid: Id, + type: S, + build_number: N, + created_on: S, + completed_on: S, + build_seconds_used: N, + creator: BitbucketAccount.nullable().optional(), + repository: BitbucketRepositoryEntity.nullable().optional(), + target: Obj, + trigger: Obj, + state: Obj, + configuration_sources: UnknownArray, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketPipelineEntity = z.infer; + +export const BitbucketCommitEntity = z + .object({ + hash: Id, + type: S, + date: S, + message: S, + author: Obj, + committer: Obj, + summary: BitbucketRendered.nullable().optional(), + rendered: Obj, + parents: UnknownArray, + repository: BitbucketRepositoryEntity.nullable().optional(), + participants: UnknownArray, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketCommitEntity = z.infer; + +export const BitbucketBranchEntity = z + .object({ + name: Id, + type: S, + target: BitbucketCommitEntity.nullable().optional(), + merge_strategies: z.array(z.string()).nullable().optional(), + sync_strategies: UnknownArray, + default_merge_strategy: S, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketBranchEntity = z.infer; + +export const BitbucketTagEntity = z + .object({ + name: Id, + type: S, + message: S, + date: S, + target: BitbucketCommitEntity.nullable().optional(), + tagger: Obj, + links: BitbucketLinks, + }) + .loose(); +export type BitbucketTagEntity = z.infer; diff --git a/packages/bitbucket/schema/index.ts b/packages/bitbucket/schema/index.ts index b600d6629..a00fa9b55 100644 --- a/packages/bitbucket/schema/index.ts +++ b/packages/bitbucket/schema/index.ts @@ -1 +1,33 @@ -export const BitbucketSchema = { version: '1.0.0', entities: {} } as const; +import { + BitbucketBranchEntity, + BitbucketCommitEntity, + BitbucketIssueEntity, + BitbucketPipelineEntity, + BitbucketProjectEntity, + BitbucketPullRequestEntity, + BitbucketRepositoryEntity, + BitbucketSnippetEntity, + BitbucketTagEntity, + BitbucketUserEntity, + BitbucketWorkspaceEntity, +} from './database'; + +export const BitbucketSchema = { + version: '1.0.0', + entities: { + workspaces: BitbucketWorkspaceEntity, + repositories: BitbucketRepositoryEntity, + projects: BitbucketProjectEntity, + pullRequests: BitbucketPullRequestEntity, + issues: BitbucketIssueEntity, + users: BitbucketUserEntity, + snippets: BitbucketSnippetEntity, + pipelines: BitbucketPipelineEntity, + commits: BitbucketCommitEntity, + branches: BitbucketBranchEntity, + tags: BitbucketTagEntity, + }, +} as const; + +export * from './database'; +export * from './primitives'; diff --git a/packages/bitbucket/schema/primitives.ts b/packages/bitbucket/schema/primitives.ts new file mode 100644 index 000000000..2cc262b6b --- /dev/null +++ b/packages/bitbucket/schema/primitives.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +/** Official JSON keys. https://developer.atlassian.com/cloud/bitbucket/swagger.v3.json */ + +export const S = z.string().nullable().optional(); +export const N = z.number().nullable().optional(); +export const B = z.boolean().nullable().optional(); +export const Id = z.string(); +export const NumId = z.number(); +export const Obj = z.record(z.string(), z.unknown()).nullable().optional(); +export const UnknownArray = z.array(z.unknown()).nullable().optional(); From debac4ec8da6e5c9dac14d9360aee9bedc2b9475 Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 18 Aug 2026 04:40:07 +0530 Subject: [PATCH 5/5] fix(bitbucket): strip emails from persisted schema --- packages/bitbucket/schema.test.ts | 41 ++- packages/bitbucket/schema/database.ts | 492 ++++++++++++-------------- 2 files changed, 269 insertions(+), 264 deletions(-) diff --git a/packages/bitbucket/schema.test.ts b/packages/bitbucket/schema.test.ts index f4be7c54c..3399d14e4 100644 --- a/packages/bitbucket/schema.test.ts +++ b/packages/bitbucket/schema.test.ts @@ -1,8 +1,8 @@ /** * Asserts every official / live-captured key is declared in `schema/database.ts`. * - * Entities are `.loose()`, so an extra key still parses — `safeParse` alone - * would never notice a field the schema forgot. + * Entities strip unknown keys. `safeParse` alone would never notice a field + * the schema forgot, so declared-key lists are the check. * * Official keys: https://developer.atlassian.com/cloud/bitbucket/swagger.v3.json * Live extras: GET /2.0/workspaces/bitbucket and GET /2.0/repositories?pagelen=1 @@ -275,20 +275,53 @@ describe('Bitbucket schema', () => { ); }); - it('keeps undeclared live extras because entities are loose', () => { + it('strips undeclared keys from persisted entities', () => { const parsed = BitbucketRepositoryEntity.safeParse({ uuid: '{r}', aKeyNobodyDeclared: 1, }); expect(parsed.success).toBe(true); + if (!parsed.success) return; + expect(parsed.data).not.toHaveProperty('aKeyNobodyDeclared'); expect(declaredKeys(BitbucketRepositoryEntity)).not.toContain( 'aKeyNobodyDeclared', ); }); - it('does not model email or pipeline-variable values', () => { + it('does not persist email, pipeline-variable, or author-raw values', () => { expect(declaredKeys(BitbucketUserEntity)).not.toContain('email'); expect(declaredKeys(BitbucketAccount)).not.toContain('email'); expect(declaredKeys(BitbucketPipelineEntity)).not.toContain('variables'); + + const user = BitbucketUserEntity.safeParse({ + uuid: '{u}', + email: 'hidden@example.com', + }); + expect(user.success).toBe(true); + if (user.success) expect(user.data).not.toHaveProperty('email'); + + const pipeline = BitbucketPipelineEntity.safeParse({ + uuid: '{pipe}', + variables: [{ key: 'TOKEN', value: 'secret' }], + }); + expect(pipeline.success).toBe(true); + if (pipeline.success) expect(pipeline.data).not.toHaveProperty('variables'); + + const commit = BitbucketCommitEntity.safeParse({ + hash: 'abc', + author: { + type: 'author', + raw: 'Ada ', + user: { uuid: '{u}', email: 'ada@example.com' }, + }, + committer: { + raw: 'Ada ', + }, + }); + expect(commit.success).toBe(true); + if (!commit.success) return; + expect(commit.data.author).not.toHaveProperty('raw'); + expect(commit.data.committer).not.toHaveProperty('raw'); + expect(commit.data.author?.user).not.toHaveProperty('email'); }); }); diff --git a/packages/bitbucket/schema/database.ts b/packages/bitbucket/schema/database.ts index d6ad5aa3a..385e9110e 100644 --- a/packages/bitbucket/schema/database.ts +++ b/packages/bitbucket/schema/database.ts @@ -15,298 +15,270 @@ export const BitbucketLinks = z .nullable() .optional(); -export const BitbucketAccount = z - .object({ - type: S, - uuid: S, - display_name: S, - nickname: S, - username: S, - account_id: S, - account_status: S, - created_on: S, - has_2fa_enabled: B, - is_staff: B, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketAccount = z.object({ + type: S, + uuid: S, + display_name: S, + nickname: S, + username: S, + account_id: S, + account_status: S, + created_on: S, + has_2fa_enabled: B, + is_staff: B, + links: BitbucketLinks, +}); export type BitbucketAccount = z.infer; -export const BitbucketRendered = z - .object({ - raw: S, - markup: S, - html: S, - }) - .loose(); +export const BitbucketRendered = z.object({ + raw: S, + markup: S, + html: S, +}); export type BitbucketRendered = z.infer; -export const BitbucketBranchTarget = z - .object({ - hash: S, - type: S, - }) - .loose(); +export const BitbucketBranchTarget = z.object({ + hash: S, + type: S, +}); export type BitbucketBranchTarget = z.infer; -export const BitbucketWorkspaceEntity = z - .object({ - uuid: Id, - type: S, - name: S, - slug: S, - is_private: B, - is_personal: B, - is_privacy_enforced: B, - forking_mode: S, - sign_system_commits: B, - created_on: S, - updated_on: S, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketWorkspaceEntity = z.object({ + uuid: Id, + type: S, + name: S, + slug: S, + is_private: B, + is_personal: B, + is_privacy_enforced: B, + forking_mode: S, + sign_system_commits: B, + created_on: S, + updated_on: S, + links: BitbucketLinks, +}); export type BitbucketWorkspaceEntity = z.infer; -export const BitbucketUserEntity = z - .object({ - uuid: Id, - type: S, - display_name: S, - nickname: S, - username: S, - account_id: S, - account_status: S, - created_on: S, - has_2fa_enabled: B, - is_staff: B, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketUserEntity = z.object({ + uuid: Id, + type: S, + display_name: S, + nickname: S, + username: S, + account_id: S, + account_status: S, + created_on: S, + has_2fa_enabled: B, + is_staff: B, + links: BitbucketLinks, +}); export type BitbucketUserEntity = z.infer; -export const BitbucketProjectEntity = z - .object({ - uuid: Id, - type: S, - key: S, - name: S, - description: S, - is_private: B, - created_on: S, - updated_on: S, - has_publicly_visible_repos: B, - owner: BitbucketAccount.nullable().optional(), - links: BitbucketLinks, - }) - .loose(); +export const BitbucketProjectEntity = z.object({ + uuid: Id, + type: S, + key: S, + name: S, + description: S, + is_private: B, + created_on: S, + updated_on: S, + has_publicly_visible_repos: B, + owner: BitbucketAccount.nullable().optional(), + links: BitbucketLinks, +}); export type BitbucketProjectEntity = z.infer; -export const BitbucketRepositoryEntity = z - .object({ - uuid: Id, - type: S, - name: S, - full_name: S, - slug: S, - description: S, - scm: S, - website: S, - language: S, - size: N, - is_private: B, - has_issues: B, - has_wiki: B, - fork_policy: S, - enforced_signed_commits: B, - created_on: S, - updated_on: S, - owner: BitbucketAccount.nullable().optional(), - workspace: BitbucketWorkspaceEntity.nullable().optional(), - project: BitbucketProjectEntity.nullable().optional(), - parent: z - .object({ - uuid: S, - type: S, - name: S, - full_name: S, - slug: S, - }) - .loose() - .nullable() - .optional(), - mainbranch: z - .object({ - type: S, - name: S, - }) - .loose() - .nullable() - .optional(), - override_settings: Obj, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketRepositoryEntity = z.object({ + uuid: Id, + type: S, + name: S, + full_name: S, + slug: S, + description: S, + scm: S, + website: S, + language: S, + size: N, + is_private: B, + has_issues: B, + has_wiki: B, + fork_policy: S, + enforced_signed_commits: B, + created_on: S, + updated_on: S, + owner: BitbucketAccount.nullable().optional(), + workspace: BitbucketWorkspaceEntity.nullable().optional(), + project: BitbucketProjectEntity.nullable().optional(), + parent: z + .object({ + uuid: S, + type: S, + name: S, + full_name: S, + slug: S, + }) + .nullable() + .optional(), + mainbranch: z + .object({ + type: S, + name: S, + }) + .nullable() + .optional(), + override_settings: Obj, + links: BitbucketLinks, +}); export type BitbucketRepositoryEntity = z.infer< typeof BitbucketRepositoryEntity >; -export const BitbucketPullRequestEndpoint = z - .object({ - repository: BitbucketRepositoryEntity.nullable().optional(), - branch: z - .object({ - name: S, - }) - .loose() - .nullable() - .optional(), - commit: z - .object({ - hash: S, - }) - .loose() - .nullable() - .optional(), - }) - .loose(); +export const BitbucketPullRequestEndpoint = z.object({ + repository: BitbucketRepositoryEntity.nullable().optional(), + branch: z + .object({ + name: S, + }) + .nullable() + .optional(), + commit: z + .object({ + hash: S, + }) + .nullable() + .optional(), +}); export type BitbucketPullRequestEndpoint = z.infer< typeof BitbucketPullRequestEndpoint >; -export const BitbucketPullRequestEntity = z - .object({ - id: NumId, - type: S, - title: S, - state: S, - draft: B, - queued: B, - reason: S, - comment_count: N, - task_count: N, - close_source_branch: B, - created_on: S, - updated_on: S, - author: BitbucketAccount.nullable().optional(), - closed_by: BitbucketAccount.nullable().optional(), - source: BitbucketPullRequestEndpoint.nullable().optional(), - destination: BitbucketPullRequestEndpoint.nullable().optional(), - merge_commit: z - .object({ - hash: S, - }) - .loose() - .nullable() - .optional(), - reviewers: UnknownArray, - participants: UnknownArray, - summary: BitbucketRendered.nullable().optional(), - rendered: Obj, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketPullRequestEntity = z.object({ + id: NumId, + type: S, + title: S, + state: S, + draft: B, + queued: B, + reason: S, + comment_count: N, + task_count: N, + close_source_branch: B, + created_on: S, + updated_on: S, + author: BitbucketAccount.nullable().optional(), + closed_by: BitbucketAccount.nullable().optional(), + source: BitbucketPullRequestEndpoint.nullable().optional(), + destination: BitbucketPullRequestEndpoint.nullable().optional(), + merge_commit: z + .object({ + hash: S, + }) + .nullable() + .optional(), + reviewers: UnknownArray, + participants: UnknownArray, + summary: BitbucketRendered.nullable().optional(), + rendered: Obj, + links: BitbucketLinks, +}); export type BitbucketPullRequestEntity = z.infer< typeof BitbucketPullRequestEntity >; -export const BitbucketIssueEntity = z - .object({ - id: NumId, - type: S, - title: S, - state: S, - kind: S, - priority: S, - votes: N, - created_on: S, - updated_on: S, - edited_on: S, - reporter: BitbucketAccount.nullable().optional(), - assignee: BitbucketAccount.nullable().optional(), - repository: BitbucketRepositoryEntity.nullable().optional(), - milestone: Obj, - version: Obj, - component: Obj, - content: BitbucketRendered.nullable().optional(), - links: BitbucketLinks, - }) - .loose(); +export const BitbucketIssueEntity = z.object({ + id: NumId, + type: S, + title: S, + state: S, + kind: S, + priority: S, + votes: N, + created_on: S, + updated_on: S, + edited_on: S, + reporter: BitbucketAccount.nullable().optional(), + assignee: BitbucketAccount.nullable().optional(), + repository: BitbucketRepositoryEntity.nullable().optional(), + milestone: Obj, + version: Obj, + component: Obj, + content: BitbucketRendered.nullable().optional(), + links: BitbucketLinks, +}); export type BitbucketIssueEntity = z.infer; -export const BitbucketSnippetEntity = z - .object({ - id: NumId, - type: S, - title: S, - scm: S, - is_private: B, - created_on: S, - updated_on: S, - owner: BitbucketAccount.nullable().optional(), - creator: BitbucketAccount.nullable().optional(), - }) - .loose(); +export const BitbucketSnippetEntity = z.object({ + id: NumId, + type: S, + title: S, + scm: S, + is_private: B, + created_on: S, + updated_on: S, + owner: BitbucketAccount.nullable().optional(), + creator: BitbucketAccount.nullable().optional(), +}); export type BitbucketSnippetEntity = z.infer; -export const BitbucketPipelineEntity = z - .object({ - uuid: Id, - type: S, - build_number: N, - created_on: S, - completed_on: S, - build_seconds_used: N, - creator: BitbucketAccount.nullable().optional(), - repository: BitbucketRepositoryEntity.nullable().optional(), - target: Obj, - trigger: Obj, - state: Obj, - configuration_sources: UnknownArray, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketPipelineEntity = z.object({ + uuid: Id, + type: S, + build_number: N, + created_on: S, + completed_on: S, + build_seconds_used: N, + creator: BitbucketAccount.nullable().optional(), + repository: BitbucketRepositoryEntity.nullable().optional(), + target: Obj, + trigger: Obj, + state: Obj, + configuration_sources: UnknownArray, + links: BitbucketLinks, +}); export type BitbucketPipelineEntity = z.infer; -export const BitbucketCommitEntity = z - .object({ - hash: Id, - type: S, - date: S, - message: S, - author: Obj, - committer: Obj, - summary: BitbucketRendered.nullable().optional(), - rendered: Obj, - parents: UnknownArray, - repository: BitbucketRepositoryEntity.nullable().optional(), - participants: UnknownArray, - links: BitbucketLinks, - }) - .loose(); +/** Commit author/committer/tagger without `raw` (that field embeds email). */ +export const BitbucketCommitAuthor = z.object({ + type: S, + user: BitbucketAccount.nullable().optional(), +}); +export type BitbucketCommitAuthor = z.infer; + +export const BitbucketCommitEntity = z.object({ + hash: Id, + type: S, + date: S, + message: S, + author: BitbucketCommitAuthor.nullable().optional(), + committer: BitbucketCommitAuthor.nullable().optional(), + summary: BitbucketRendered.nullable().optional(), + rendered: Obj, + parents: UnknownArray, + repository: BitbucketRepositoryEntity.nullable().optional(), + participants: UnknownArray, + links: BitbucketLinks, +}); export type BitbucketCommitEntity = z.infer; -export const BitbucketBranchEntity = z - .object({ - name: Id, - type: S, - target: BitbucketCommitEntity.nullable().optional(), - merge_strategies: z.array(z.string()).nullable().optional(), - sync_strategies: UnknownArray, - default_merge_strategy: S, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketBranchEntity = z.object({ + name: Id, + type: S, + target: BitbucketCommitEntity.nullable().optional(), + merge_strategies: z.array(z.string()).nullable().optional(), + sync_strategies: UnknownArray, + default_merge_strategy: S, + links: BitbucketLinks, +}); export type BitbucketBranchEntity = z.infer; -export const BitbucketTagEntity = z - .object({ - name: Id, - type: S, - message: S, - date: S, - target: BitbucketCommitEntity.nullable().optional(), - tagger: Obj, - links: BitbucketLinks, - }) - .loose(); +export const BitbucketTagEntity = z.object({ + name: Id, + type: S, + message: S, + date: S, + target: BitbucketCommitEntity.nullable().optional(), + tagger: BitbucketCommitAuthor.nullable().optional(), + links: BitbucketLinks, +}); export type BitbucketTagEntity = z.infer;