Skip to content

Commit ba39fd6

Browse files
committed
Harden workspace-disabled detection
1 parent c564946 commit ba39fd6

3 files changed

Lines changed: 57 additions & 41 deletions

File tree

lib/request/fetch-helpers.ts

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -265,50 +265,52 @@ export function isEntitlementError(code: string, bodyText: string): boolean {
265265
* @param bodyText - The response body text to inspect for workspace-related phrases
266266
* @returns `true` if the error indicates a disabled/expired workspace
267267
*/
268-
export function isWorkspaceDisabledError(status: number, code: string, bodyText: string): boolean {
269-
const haystack = `${code} ${bodyText}`.toLowerCase();
270-
271-
// Check for HTTP 403 with workspace disabled messages
272-
if (status === 403) {
273-
// Common patterns for disabled/expired workspaces
274-
const disabledPatterns = [
275-
/workspace.*(?:disabled|expired|deactivated|terminated)/i,
276-
/account.*(?:disabled|expired|deactivated|terminated)/i,
277-
/(?:workspace|account).*no longer.*(?:active|available|valid)/i,
278-
/(?:workspace|account).*has been.*(?:disabled|expired|closed)/i,
279-
/workspace.*(?:access|subscription).*expired/i,
280-
/billing.*(?:failed|expired|disabled)/i,
281-
/payment.*(?:failed|expired|required)/i,
282-
/organization.*(?:disabled|expired|inactive)/i,
283-
/team.*(?:disabled|expired|inactive)/i,
284-
];
285-
286-
for (const pattern of disabledPatterns) {
287-
if (pattern.test(haystack)) {
288-
return true;
289-
}
290-
}
268+
export function isWorkspaceDisabledError(
269+
status: number,
270+
code: unknown,
271+
bodyText: string,
272+
): boolean {
273+
if (status !== 403) {
274+
return false;
291275
}
292-
293-
// Check for specific error codes that indicate workspace issues.
294-
if (status === 403) {
295-
const workspaceErrorCodes = [
296-
"workspace_disabled",
297-
"workspace_expired",
298-
"workspace_terminated",
299-
"account_disabled",
300-
"account_expired",
301-
"organization_disabled",
302-
"billing_failed",
303-
"payment_required",
304-
];
305-
306-
if (workspaceErrorCodes.some((c) => code.toLowerCase().includes(c))) {
276+
277+
const normalizedCode = typeof code === "string" ? code.trim().toLowerCase() : "";
278+
const haystack = `${normalizedCode} ${bodyText}`.toLowerCase();
279+
280+
const disabledPatterns = [
281+
/workspace.*(?:disabled|expired|deactivated|terminated)/i,
282+
/account.*(?:disabled|expired|deactivated|terminated)/i,
283+
/(?:workspace|account).*no longer.*(?:active|available|valid)/i,
284+
/(?:workspace|account).*has been.*(?:disabled|expired|closed)/i,
285+
/workspace.*(?:access|subscription).*expired/i,
286+
/organization.*(?:disabled|expired|inactive)/i,
287+
/team.*(?:disabled|expired|inactive)/i,
288+
];
289+
290+
for (const pattern of disabledPatterns) {
291+
if (pattern.test(haystack)) {
307292
return true;
308293
}
309294
}
310295

311-
return false;
296+
const workspaceErrorCodes = new Set([
297+
"workspace_disabled",
298+
"workspace_expired",
299+
"workspace_terminated",
300+
"account_disabled",
301+
"account_expired",
302+
"organization_disabled",
303+
]);
304+
if (workspaceErrorCodes.has(normalizedCode)) {
305+
return true;
306+
}
307+
308+
const normalizedTokens = normalizedCode
309+
.split(/[^a-z0-9_]+/i)
310+
.map((token) => token.trim())
311+
.filter((token) => token.length > 0);
312+
313+
return normalizedTokens.some((token) => workspaceErrorCodes.has(token));
312314
}
313315

314316
/**

test/fetch-helpers.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,8 @@ describe('isWorkspaceDisabledError', () => {
338338
expect(isWorkspaceDisabledError(403, 'account_disabled', '')).toBe(true);
339339
});
340340

341-
it('returns true for billing_failed error code', () => {
342-
expect(isWorkspaceDisabledError(403, 'billing_failed', '')).toBe(true);
341+
it('returns true for organization_disabled error code', () => {
342+
expect(isWorkspaceDisabledError(403, 'organization_disabled', '')).toBe(true);
343343
});
344344

345345
it('returns false for non-403 status even with disabled message', () => {
@@ -358,6 +358,19 @@ describe('isWorkspaceDisabledError', () => {
358358
it('returns false for entitlement errors', () => {
359359
expect(isWorkspaceDisabledError(403, 'usage_not_included', 'Not in your plan')).toBe(false);
360360
});
361+
362+
it('returns false for numeric error codes', () => {
363+
expect(isWorkspaceDisabledError(403, 402, 'Workspace disabled')).toBe(true);
364+
expect(isWorkspaceDisabledError(403, 402, 'Billing failed for your subscription')).toBe(false);
365+
expect(isWorkspaceDisabledError(403, 0, '')).toBe(false);
366+
});
367+
368+
it('returns false for billing-style 403 codes without workspace or account disable signals', () => {
369+
expect(isWorkspaceDisabledError(403, 'billing_failed', '')).toBe(false);
370+
expect(isWorkspaceDisabledError(403, 'payment_required', '')).toBe(false);
371+
expect(isWorkspaceDisabledError(403, '', 'Payment required to continue')).toBe(false);
372+
expect(isWorkspaceDisabledError(403, '', 'Billing failed for your plan')).toBe(false);
373+
});
361374
});
362375

363376
describe('createEntitlementErrorResponse', () => {

test/index-retry.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ describe("OpenAIAuthPlugin rate-limit retry", () => {
479479
enabled: false,
480480
});
481481
expect(accountManagerState.saveToDiskDebouncedCalls).toBeGreaterThanOrEqual(1);
482+
expect(exhaustedAccount.enabled).toBe(false);
482483

483484
const firstHeaders = fetchMock.mock.calls[0]?.[1]?.headers as Headers;
484485
const secondHeaders = fetchMock.mock.calls[1]?.[1]?.headers as Headers;

0 commit comments

Comments
 (0)