W
File size: 3,413 Bytes
2b64d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// v2.0.56 — ban-shaped error detection. windsurf-assistant-pub
// inspiration: when upstream returns "Account suspended" / "API key
// revoked" we promote the account to status='banned' after a 2-strike
// streak, so the pool stops handing out a known-dead key.

import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  addAccountByKey, removeAccount, getAccountInternal,
  looksLikeBanSignal, reportBanSignal, reportSuccess,
} from '../src/auth.js';

const created = [];
function mkAccount(label = 'ban-test') {
  const a = addAccountByKey('sk-ban-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8), label);
  created.push(a.id);
  return a;
}

afterEach(() => {
  while (created.length) removeAccount(created.pop());
});

describe('looksLikeBanSignal (audit windsurf-assistant-pub)', () => {
  const positives = [
    'Your account has been suspended for terms-of-service violation',
    'account_disabled',
    'Account banned by upstream',
    'User suspended due to abuse',
    'Subscription cancelled',
    'subscription expired',
    'Authentication failed: invalid credentials',
    'Invalid API key',
    'API key revoked',
    'API key disabled',
    'unauthorized: account does not exist',
    '账号已停用',
    '账号封禁',
    '用户已禁用',
    '订阅已过期',
  ];
  const negatives = [
    'rate limit exceeded',
    'too many requests',
    'context deadline exceeded',
    'internal error occurred (error ID: abc)',
    'cascade transport failure',
    'panel state not found',
    'temporary upstream stall',
    '',
    null,
    undefined,
    42,
  ];

  const labelOf = (msg) => {
    const s = (msg === null) ? 'null' : (msg === undefined) ? 'undefined' : String(msg);
    return s.slice(0, 60).replace(/\s+/g, ' ');
  };
  for (const msg of positives) {
    it(`detects ban signal: ${labelOf(msg)}`, () => {
      assert.equal(looksLikeBanSignal(msg), true);
    });
  }
  for (const msg of negatives) {
    it(`ignores non-ban: ${labelOf(msg) || '(empty)'}`, () => {
      assert.equal(looksLikeBanSignal(msg), false);
    });
  }
});

describe('reportBanSignal: 2-strike → status=banned', () => {
  it('first ban-shaped error does NOT flip status', () => {
    const a = mkAccount();
    reportBanSignal(a.apiKey, 'Account suspended');
    assert.equal(getAccountInternal(a.id).status, 'active', 'one strike must not ban');
  });

  it('two ban-shaped errors within window → status=banned', () => {
    const a = mkAccount();
    reportBanSignal(a.apiKey, 'Account suspended');
    reportBanSignal(a.apiKey, 'Invalid API key');
    const after = getAccountInternal(a.id);
    assert.equal(after.status, 'banned', 'two strikes must promote to banned');
    assert.ok(after.bannedAt > 0, 'bannedAt timestamp must be set');
    assert.ok(after.bannedReason, 'bannedReason must be set');
  });

  it('reportSuccess between two ban signals resets the streak', () => {
    const a = mkAccount();
    reportBanSignal(a.apiKey, 'Authentication failed');
    reportSuccess(a.apiKey);
    reportBanSignal(a.apiKey, 'Authentication failed');
    assert.equal(getAccountInternal(a.id).status, 'active', 'success must clear ban-streak');
  });

  it('signal on unknown apiKey is a no-op', () => {
    const result = reportBanSignal('sk-does-not-exist', 'Account suspended');
    assert.equal(result, false);
  });
});