diff --git a/package-lock.json b/package-lock.json index c5749f14..830b3dc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "better-sqlite3": "^11.0.0", "hono": "^4.0.0", "nanoid": "^5.0.0", + "node-fetch": "^3.3.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "ws": "^8.18.0" @@ -1273,6 +1274,15 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1439,6 +1449,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1457,6 +1490,18 @@ "rollup": "^4.34.8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1703,6 +1748,44 @@ "node": ">=10" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3035,6 +3118,15 @@ "dev": true, "license": "MIT" }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index 07896ccd..9f14eef3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "better-sqlite3": "^11.0.0", "hono": "^4.0.0", "nanoid": "^5.0.0", + "node-fetch": "^3.3.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "ws": "^8.18.0" diff --git a/tests/antiDetection.test.ts b/tests/antiDetection.test.ts new file mode 100644 index 00000000..3206711f --- /dev/null +++ b/tests/antiDetection.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { Hono } from 'hono'; +import { serve } from '@hono/node-server'; +import { handleRequest } from '../src/provider/index.js'; +import type { InnerPlaintext } from '../src/types.js'; + +describe('Anti-Detection Features', () => { + let mockServer; + let mockPort; + + beforeAll(async () => { + const app = new Hono(); + let callCount = 0; + + app.post('/v1/messages', async (c) => { + callCount++; + const body = await c.req.json(); + if (body.model === 'retry-test' && callCount === 1) { + return c.json({ error: { message: 'rate_limit' } }, 429); + } + return c.json({ id: 'msg_test', type: 'message', role: 'assistant', content: [{ type: 'text', text: 'Hello from mock!' }], model: 'claude-sonnet-4-20250514', stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 5 } }); + }); + + mockPort = 18900 + Math.floor(Math.random() * 100); + mockServer = serve({ fetch: app.fetch, port: mockPort }); + }); + + afterAll(() => { + mockServer?.close(); + }); + + it('should randomize headers and delays', async () => { + // Add assertions for the headers and delays + }); +}); \ No newline at end of file diff --git a/tests/provider.test.js b/tests/provider.test.js new file mode 100644 index 00000000..4427c94a --- /dev/null +++ b/tests/provider.test.js @@ -0,0 +1,82 @@ +import { handleRequest } from './src/provider/index.js'; // Attempting corrected relative path + +// Mocks for testing +import { vi } from 'vitest'; +vi.mock('node-fetch'); +import fetch from 'node-fetch'; +const { Response } = vi.importActual('node-fetch'); + +describe('Provider Request Handling', () => { + + // Test for header fingerprints + test('should not have identical header fingerprints on consecutive requests', async () => { + fetch.mockResolvedValue(new Response(JSON.stringify({ content: [{ text: 'hello' }], usage: { input_tokens: 5, output_tokens: 3 }, finish_reason: 'stop' }))); + + const apiKey = 'test-api-key'; + const inner = { + model: 'test-model', + max_tokens: 100, + messages: [{ role: 'user', content: 'text' }], + temperature: 0.5, + stop_sequences: [], + }; + + // First request + const result1 = await handleRequest(inner, apiKey); + const headers1 = fetch.mock.calls[0][1].headers; + + // Second request + const result2 = await handleRequest(inner, apiKey); + const headers2 = fetch.mock.calls[1][1].headers; + + expect(headers1['anthropic-version']).not.toEqual(headers2['anthropic-version']); + }); + + // Test for random delays + test('generates random delays between requests', async () => { + const delays = []; + const inner = { + model: 'test-model', + max_tokens: 100, + messages: [{ role: 'user', content: 'text' }], + temperature: 0.5, + stop_sequences: [], + }; + + for (let i = 0; i < 10; i++) { + const start = Date.now(); + await handleRequest(inner, apiKey); + const end = Date.now(); + delays.push(end - start); + } + + // Check if delays are random and within bounds of 0 to 500ms + delays.forEach(delay => { + expect(delay).toBeGreaterThanOrEqual(0); + expect(delay).toBeLessThanOrEqual(500); + }); + }); + + // Test for max_tokens variability + test('should honor maximum tokens variability', async () => { + fetch.mockResolvedValue(new Response(JSON.stringify({ content: [{ text: 'hello' }], usage: { input_tokens: 5, output_tokens: 3 }, finish_reason: 'stop' }))); + const inner = { + model: 'test-model', + max_tokens: 100, + messages: [{ role: 'user', content: 'test content' }], + temperature: 0.5, + stop_sequences: [], + }; + + const result = await handleRequest(inner, apiKey); + expect(result).toBeDefined(); + expect(result.usage.output_tokens).toBeLessThanOrEqual(105); // Allowing ±5% + expect(result.usage.output_tokens).toBeGreaterThanOrEqual(95); + }); + + // Test for anti_fingerprint config respect + test('should respect anti_fingerprint config', async () => { + // Implement test logic here + }); + +}); \ No newline at end of file diff --git a/tests/provider.test.ts b/tests/provider.test.ts index 18627eeb..14c9475f 100644 --- a/tests/provider.test.ts +++ b/tests/provider.test.ts @@ -8,6 +8,9 @@ describe('provider', () => { let mockServer: ReturnType; let mockPort: number; + let mockKey = 'test-key'; // Example API Key for tests + + // Mock Anthropic API beforeAll(async () => { const app = new Hono(); @@ -117,7 +120,77 @@ describe('provider', () => { }; // The mock returns 200 for any model, so this will succeed - // For a real decrypt error test, we'd need the full provider pipeline + }); + const results = await Promise.all(Array.from({ length: 10 }, async () => { + const inner: InnerPlaintext = { + messages: [{ role: 'user', content: 'hello' }], + model: 'claude-sonnet-4-20250514', + max_tokens: 100, + temperature: 1, + top_p: 1, + stop_sequences: [], + stream: false, + }; + + return handleRequest(inner, 'test-key', undefined, `http://localhost:${mockPort}`); + })); + const versions = results.map(result => result.headers['anthropic-version']); + const uniqueVersions = new Set(versions); + expect(uniqueVersions.size).toBeGreaterThan(1); + }); + + it('should enforce random delays between 0-500ms', async () => { + const inner: InnerPlaintext = { + messages: [{ role: 'user', content: 'hello' }], + model: 'claude-sonnet-4-20250514', + max_tokens: 100, + temperature: 1, + top_p: 1, + stop_sequences: [], + stream: false, + }; + + const start = Date.now(); + await handleRequest(inner, 'test-key', undefined, `http://localhost:${mockPort}`); + const delay = Date.now() - start; + expect(delay).toBeGreaterThanOrEqual(0); + expect(delay).toBeLessThanOrEqual(500); + }); + + it('should randomize max_tokens within ±5%', async () => { + const originalMaxTokens = 100; + const inner: InnerPlaintext = { + messages: [{ role: 'user', content: 'hello' }], + model: 'claude-sonnet-4-20250514', + max_tokens: originalMaxTokens, + temperature: 1, + top_p: 1, + stop_sequences: [], + stream: false, + }; + + const result = await handleRequest(inner, 'test-key', undefined, `http://localhost:${mockPort}`); + const adjustedMaxTokens = Math.round(originalMaxTokens * (1 + (Math.random() * 0.1 - 0.05))); + expect(result.max_tokens).toBe(adjustedMaxTokens); + }); + + it('should rotate User-Agent strings', async () => { + const inner: InnerPlaintext = { + messages: [{ role: 'user', content: 'hello' }], + model: 'claude-sonnet-4-20250514', + max_tokens: 100, + temperature: 1, + top_p: 1, + stop_sequences: [], + stream: false, + }; + + const result = await handleRequest(inner, 'test-key', undefined, `http://localhost:${mockPort}`); + const userAgent = result.headers['User-Agent']; + expect(userAgent).toMatch(/some-random-user-agent/i); + }); + + }); const result = await handleRequest(inner, 'test-key', undefined, `http://localhost:${mockPort}`); expect(result.content).toBeDefined(); });