From eeae47121ef062fc26fde7609354b7b24c1efc69 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 01:36:01 +0000 Subject: [PATCH 1/4] test(bitbucket): reproduce #31758, access token denied account scope reads as unverified Bitbucket answers GET /2.0/user with 403 for an access token it accepts but that has no account scope. probeAuth should report it as authenticated rather than unknown. AI-assisted; verified with the tests and tools listed above. --- .../src/server/BitbucketApi.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts index ddfe567273ed9..54380d4fac38c 100644 --- a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts @@ -737,6 +737,30 @@ it.effect("reports saved credentials as configured when Bitbucket cannot confirm }).pipe(Effect.provide(layer)); }); +it.effect( + "reports an access token Bitbucket accepts without account scope as authenticated", + () => { + // Bitbucket answers 401 for a credential it rejects; 403 means it accepted the credential but + // that credential cannot read the account, as with repository, project, or workspace tokens. + 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()); + }).pipe(Effect.provide(layer)); + }, +); + it.effect("keeps Bitbucket response bodies out of checkout diagnostics", () => { const responseBody = '{"error":{"message":"credential=secret-value"}}'; const { layer } = makeLayer({ From 176f1753ef79c7ccd5d7571c9d7818c96096b4d2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 01:40:57 +0000 Subject: [PATCH 2/4] fix(bitbucket): access tokens without account scope read as authenticated Bitbucket answers GET /2.0/user with 403 for a credential it accepts that cannot read the account, such as repository, project, or workspace access tokens. probeAuth treated that like any other failure and reported status "unknown". A 403 now reports "authenticated" (with the email for an API token); 401, network, timeout, 5xx, and rate limits keep the old behaviour. AI-assisted; verified with the tests and tools listed above. --- .../src/server/BitbucketApi.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.ts index fe68110cbfa55..efce380b438ee 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; 401 means it was rejected. + 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({ From 679a71155367f41afb410430be7d136472fd6107 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Nov 2026 01:47:35 +0000 Subject: [PATCH 3/4] test(bitbucket): cover an API token denied account scope AI-assisted; verified with the tests and tools listed above. --- .../src/server/BitbucketApi.test.ts | 48 +++++++++++-------- .../src/server/BitbucketApi.ts | 2 +- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts index 54380d4fac38c..77cbc100ab995 100644 --- a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts @@ -737,29 +737,35 @@ it.effect("reports saved credentials as configured when Bitbucket cannot confirm }).pipe(Effect.provide(layer)); }); -it.effect( - "reports an access token Bitbucket accepts without account scope as authenticated", - () => { - // Bitbucket answers 401 for a credential it rejects; 403 means it accepted the credential but - // that credential cannot read the account, as with repository, project, or workspace tokens. - const { layer } = makeLayer({ - response: () => new Response(null, { status: 403 }), - env: { T3CODE_BITBUCKET_API_BASE_URL: "https://api.test.local/2.0" }, - }); +it.effect("reports a token Bitbucket accepts without account scope as authenticated", () => { + // Bitbucket answers 401 for a credential it rejects; 403 means it accepted the credential but + // that credential cannot read the account, as with repository, project, or workspace tokens. + 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; + 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()); - }).pipe(Effect.provide(layer)); - }, -); + 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"}}'; diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.ts index efce380b438ee..7874a04d1aa3e 100644 --- a/packages/source-control-bitbucket/src/server/BitbucketApi.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.ts @@ -1049,7 +1049,7 @@ export const make = Effect.gen(function* () { detail: Option.none(), })), // 403 means Bitbucket accepted the credential but it cannot read the account, as - // with repository, project, or workspace access tokens; 401 means it was rejected. + // with repository, project, or workspace access tokens. Effect.catch((error) => (error._tag === "BitbucketResponseError" || error._tag === "BitbucketResponseBodyReadError") && From 7af03566d2844c1ed8a54c2c857379dae777fd4f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 21:26:36 +0000 Subject: [PATCH 4/4] test(bitbucket): drop comment duplicating the probeAuth explanation AI-assisted; verified with the tests and tools listed above. --- .../source-control-bitbucket/src/server/BitbucketApi.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts index 77cbc100ab995..70fb03916a4b2 100644 --- a/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.test.ts @@ -738,8 +738,6 @@ it.effect("reports saved credentials as configured when Bitbucket cannot confirm }); it.effect("reports a token Bitbucket accepts without account scope as authenticated", () => { - // Bitbucket answers 401 for a credential it rejects; 403 means it accepted the credential but - // that credential cannot read the account, as with repository, project, or workspace tokens. const { layer } = makeLayer({ response: () => new Response(null, { status: 403 }), env: { T3CODE_BITBUCKET_API_BASE_URL: "https://api.test.local/2.0" },