mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* fix(bitbucket): commit credentials atomically and stop blaming the token for network faults STA-3941 (P0): a credential edit could destroy the last working pair. `writeFileSync` replaces in place, so a failed or interrupted write truncated the previous secret, and the secret and metadata files were published independently — a crash between them left a new secret paired with the old email, unusable on restart. - Write credential files through a temp + fsync + rename, so a reader sees either the old bytes or the complete new ones, never a truncated file. - Carry authMode/email/baseUrl inside the encrypted envelope and treat the plaintext metadata as display-only. A torn write can now only leave a stale displayed account, never an unusable credential. Envelopes written before this change still resolve through the metadata fallback. STA-3944 (P2): timeouts, DNS failures, 5xx and unparseable bodies collapsed to the same miss as a 401, so Orca told users their credentials were invalid when the host was simply unreachable — sending them to regenerate a working token. `/user` now reports rejected vs unreachable, connect explains which happened, and an unreachable host no longer renders as "Auth failed". Adds failure-injection coverage for a partial secret write, an interrupt between publishing the secret and the metadata, legacy envelopes, and the transport-vs-auth split. * fix(bitbucket): loop short writes and keep eligibility out of the keychain Review of the atomic writer found it ignored writeSync's return value. write(2) may return a short count, so a partial buffer could be fsynced and renamed into place — the same truncation the change exists to prevent, moved one step later. Write now loops to completion, with a test that fails without it. Also stops isBitbucketReviewCreationAuthenticated force-decrypting the stored secret. Create-PR eligibility is evaluated proactively for the sidebar, so that popped an OS keychain prompt just from opening a worktree. Presence is enough; creation itself still fails closed on an unusable credential.
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
import { authHeaders, type BitbucketAuthConfig } from './bitbucket-auth-config'
|
|
import { cancelUnreadResponseBody } from '../lib/unread-response-body'
|
|
|
|
const USER_REQUEST_TIMEOUT_MS = 4000
|
|
|
|
export type RawBitbucketUser = {
|
|
username?: string | null
|
|
display_name?: string | null
|
|
account_id?: string | null
|
|
}
|
|
|
|
/**
|
|
* Why (STA-3944): a timeout, DNS failure, 5xx, or unparseable body says nothing
|
|
* about the credential. Collapsing those to the same miss as a 401 told users
|
|
* their token was invalid when the network was simply unreachable.
|
|
*/
|
|
export type BitbucketUserResult =
|
|
| { ok: true; user: RawBitbucketUser }
|
|
| { ok: false; reason: 'rejected' | 'unreachable' }
|
|
|
|
export function accountNameFromUser(user: RawBitbucketUser | null): string | null {
|
|
return user?.username ?? user?.display_name ?? user?.account_id ?? null
|
|
}
|
|
|
|
// Shared by live env-var status checks and by connect-time verification, so a
|
|
// credential is proven against `/user` before it is ever persisted.
|
|
export async function fetchBitbucketUserResult(
|
|
config: BitbucketAuthConfig,
|
|
timeoutMs: number = USER_REQUEST_TIMEOUT_MS
|
|
): Promise<BitbucketUserResult> {
|
|
let response: Response
|
|
try {
|
|
const base = config.baseUrl.replace(/\/+$/, '')
|
|
response = await fetch(`${base}/user`, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...authHeaders(config)
|
|
},
|
|
signal: AbortSignal.timeout(timeoutMs)
|
|
})
|
|
} catch {
|
|
return { ok: false, reason: 'unreachable' }
|
|
}
|
|
if (!response.ok) {
|
|
await cancelUnreadResponseBody(response)
|
|
// Only the credential-bearing statuses are the credential's fault; 5xx and
|
|
// the rest are the server or the path in between.
|
|
const rejected = response.status === 401 || response.status === 403
|
|
return { ok: false, reason: rejected ? 'rejected' : 'unreachable' }
|
|
}
|
|
try {
|
|
return { ok: true, user: (await response.json()) as RawBitbucketUser }
|
|
} catch {
|
|
return { ok: false, reason: 'unreachable' }
|
|
}
|
|
}
|
|
|
|
/** Convenience for callers that only need the account, not the failure reason. */
|
|
export async function fetchBitbucketUser(
|
|
config: BitbucketAuthConfig,
|
|
timeoutMs: number = USER_REQUEST_TIMEOUT_MS
|
|
): Promise<RawBitbucketUser | null> {
|
|
const result = await fetchBitbucketUserResult(config, timeoutMs)
|
|
return result.ok ? result.user : null
|
|
}
|