diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts index ddfe567273ed9..70fb03916a4b2 100644 --- a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts @@ -737,6 +737,34 @@ it.effect("reports saved credentials as configured when Bitbucket cannot confirm }).pipe(Effect.provide(layer)); }); +it.effect("reports a token Bitbucket accepts without account scope as authenticated", () => { + const { layer } = makeLayer({ + response: () => new Response(null, { status: 403 }), + env: { T3CODE_BITBUCKET_API_BASE_URL: "https://api.test.local/2.0" }, + }); + + return Effect.gen(function* () { + const bitbucket = yield* BitbucketApi.BitbucketApi; + const settings = yield* TestSourceControlHost.TestSourceControlHostSettings; + + yield* settings.update({ + sourceControlHosts: { bitbucket: { accessToken: "saved-access-token" } }, + }); + const auth = yield* bitbucket.probeAuth; + assert.strictEqual(auth.status, "authenticated"); + assert.deepStrictEqual(auth.account, Option.none()); + + yield* settings.update({ + sourceControlHosts: { + bitbucket: { accessToken: "", email: "saved@example.com", apiToken: "saved-api-token" }, + }, + }); + const apiTokenAuth = yield* bitbucket.probeAuth; + assert.strictEqual(apiTokenAuth.status, "authenticated"); + assert.deepStrictEqual(apiTokenAuth.account, Option.some("saved@example.com")); + }).pipe(Effect.provide(layer)); +}); + it.effect("keeps Bitbucket response bodies out of checkout diagnostics", () => { const responseBody = '{"error":{"message":"credential=secret-value"}}'; const { layer } = makeLayer({ diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.ts index fe68110cbfa55..7874a04d1aa3e 100644 --- a/packages/source-control-bitbucket/src/server/BitbucketApi.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.ts @@ -1048,7 +1048,19 @@ export const make = Effect.gen(function* () { host: Option.some("bitbucket.org"), detail: Option.none(), })), - Effect.catch(() => currentCredential.pipe(Effect.map(authFromCredential))), + // 403 means Bitbucket accepted the credential but it cannot read the account, as + // with repository, project, or workspace access tokens. + Effect.catch((error) => + (error._tag === "BitbucketResponseError" || + error._tag === "BitbucketResponseBodyReadError") && + error.status === 403 + ? Effect.succeed({ + ...authFromCredential(credential), + status: "authenticated", + detail: Option.none(), + }) + : currentCredential.pipe(Effect.map(authFromCredential)), + ), ), ), Effect.catchTags({