Ctrl K
ComunidadGuías…

test/auth-warning.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { config } from '../src/config.js';
import {
  addAccountByKey,
  configureBindHost,
  getAccountInternal,
  getAccountList,
  getAccountListStats,
  maskApiKey,
  removeAccount,
  reportError,
  reportSuccess,
  shouldEmitNoAuthWarning,
  validateApiKey,
} from '../src/auth.js';

const originalApiKey = config.apiKey;
const createdAccountIds = [];

afterEach(() => {
  config.apiKey = originalApiKey;
  configureBindHost('0.0.0.0');
  while (createdAccountIds.length) removeAccount(createdAccountIds.pop());
});

describe('shouldEmitNoAuthWarning', () => {
  it('warns when unauthenticated service binds all interfaces', () => {
    assert.equal(shouldEmitNoAuthWarning('0.0.0.0', false), true);
    assert.equal(shouldEmitNoAuthWarning('::', false), true);
  });

  it('does not warn for localhost or configured auth', () => {
    assert.equal(shouldEmitNoAuthWarning('127.0.0.1', false), false);
    assert.equal(shouldEmitNoAuthWarning('0.0.0.0', true), false);
  });

  it('missing API_KEY fails closed by default; opt-in allows it only on local binds (M1)', () => {
    config.apiKey = '';
    // Default (no opt-in): fail closed even on a local bind — "local bind" is
    // not "no proxy" (same-host reverse proxy makes every peer look loopback).
    delete process.env.WINDSURFAPI_ALLOW_UNAUTHENTICATED;
    configureBindHost('127.0.0.1');
    assert.equal(validateApiKey(''), false);

    // Opt-in: allowed, but ONLY on a local bind.
    process.env.WINDSURFAPI_ALLOW_UNAUTHENTICATED = '1';
    configureBindHost('127.0.0.1');
    assert.equal(validateApiKey(''), true);
    configureBindHost('::1');
    assert.equal(validateApiKey(''), true);
    configureBindHost('[::1]');
    assert.equal(validateApiKey(''), true);
    configureBindHost('::ffff:127.0.0.1');
    assert.equal(validateApiKey(''), true);
    // Empty bindHost is "didn't configure / Node defaults to all interfaces"
    // which is non-local. Must fail closed even with opt-in.
    configureBindHost('');
    assert.equal(validateApiKey(''), false);

    configureBindHost('0.0.0.0');
    assert.equal(validateApiKey(''), false);
    configureBindHost('192.168.1.10');
    assert.equal(validateApiKey('anything'), false);
    delete process.env.WINDSURFAPI_ALLOW_UNAUTHENTICATED;
  });

  it('compares configured API_KEY without default-allowing missing or wrong keys', () => {
    config.apiKey = 'server-secret';
    configureBindHost('0.0.0.0');

    assert.equal(validateApiKey('server-secret'), true);
    assert.equal(validateApiKey('wrong'), false);
    assert.equal(validateApiKey(''), false);
  });

  it('returns masked account keys without the raw upstream apiKey', () => {
    const key = `abcd1234efgh5678-${Date.now()}`;
    const account = addAccountByKey(key, 'masked-list');
    createdAccountIds.push(account.id);

    const listed = getAccountList().find(a => a.id === account.id);
    assert.equal(listed.apiKey, undefined);
    assert.equal(listed.apiKey_masked, `${key.slice(0, 8)}...${key.slice(-4)}`);
    assert.equal(listed.keyPrefix, 'abcd1234...');
  });

  it('counts flagged, rate-limited, and disabled accounts in one stats view', () => {
    const suffix = `${Date.now()}-${Math.random()}`;
    const active = addAccountByKey(`active-${suffix}`, 'active');
    const errored = addAccountByKey(`errored-${suffix}`, 'errored');
    const limited = addAccountByKey(`limited-${suffix}`, 'limited');
    const disabled = addAccountByKey(`disabled-${suffix}`, 'disabled');
    createdAccountIds.push(active.id, errored.id, limited.id, disabled.id);

    getAccountInternal(errored.id).errorCount = 1;
    getAccountInternal(limited.id).rateLimitedUntil = Date.now() + 60_000;
    getAccountInternal(disabled.id).status = 'disabled';

    const stats = getAccountListStats();
    assert.ok(stats.total >= 4);
    assert.ok(stats.flagged >= 2);
    assert.ok(stats.rateLimited >= 1);
    assert.ok(stats.disabled >= 1);
  });
});

describe('reportError — time-windowed auth-failure streak', () => {
  it('does not disable a key when 3 failures are spread beyond the window', () => {
    const key = `err-spread-${Date.now()}-${Math.random()}`;
    const account = addAccountByKey(key, 'err-spread');
    createdAccountIds.push(account.id);

    // Three failures, each older than the window relative to the next: the
    // streak resets every time, so the account stays active (transient blips
    // during separate Windsurf deploys must not kill a healthy key).
    reportError(key, { windowMs: 1 });
    const internal = getAccountInternal(account.id);
    internal._errorAt = Date.now() - 10; // age the last failure past windowMs
    reportError(key, { windowMs: 1 });
    internal._errorAt = Date.now() - 10;
    reportError(key, { windowMs: 1 });

    assert.equal(internal.errorCount, 1);
    assert.notEqual(internal.status, 'error');
  });

  it('disables a key after 3 failures inside the window, and success rehabilitates', () => {
    // A healthy peer so the key under test isn't the pool's last usable account
    // (the LB last-account exemption keeps a sole account active by design).
    const peer = addAccountByKey(`err-burst-peer-${Date.now()}-${Math.random()}`, 'err-burst-peer');
    createdAccountIds.push(peer.id);
    reportSuccess(peer.apiKey);
    const key = `err-burst-${Date.now()}-${Math.random()}`;
    const account = addAccountByKey(key, 'err-burst');
    createdAccountIds.push(account.id);

    reportError(key);
    reportError(key);
    reportError(key);
    const internal = getAccountInternal(account.id);
    assert.equal(internal.errorCount, 3);
    assert.equal(internal.status, 'error');

    // A later success clears the streak and reactivates the account.
    reportSuccess(key);
    assert.equal(internal.errorCount, 0);
    assert.equal(internal.status, 'active');
  });
});

describe('S6-F1: maskApiKey never exposes more than ~25% of a short key', () => {
  it('scales the visible prefix with key length', () => {
    assert.equal(maskApiKey('12345678'), '12...', 'the 8-char floor key used to leak half its entropy');
    assert.equal(maskApiKey('123456789'), '123...');
    assert.equal(maskApiKey('123456789012'), '123...');
    assert.equal(maskApiKey('1234567890123'), '12345678...0123');
    assert.equal(maskApiKey('sk-ant-fixture-long-key-abcdef'), 'sk-ant-f...cdef');
    assert.equal(maskApiKey(''), '');
  });

  it('keyPrefix on a listed account is bounded the same way', () => {
    const account = addAccountByKey('abcdefgh', 'short-prefix');
    createdAccountIds.push(account.id);
    const listed = getAccountList().find((a) => a.id === account.id);
    assert.equal(listed.keyPrefix, 'ab...', '8-char key must not render all 8 chars as its "prefix"');
  });
});