saros-proxy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/dist/cli/opencode-config.d.ts +28 -0
- package/dist/cli/opencode-config.js +126 -0
- package/dist/cli/opencode-config.js.map +1 -0
- package/dist/cli/setup.d.ts +29 -0
- package/dist/cli/setup.js +669 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/ui.d.ts +27 -0
- package/dist/cli/ui.js +116 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +341 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +162 -0
- package/dist/constants.js.map +1 -0
- package/dist/dashboard-parser.d.ts +47 -0
- package/dist/dashboard-parser.js +88 -0
- package/dist/dashboard-parser.js.map +1 -0
- package/dist/firefox-cookies.d.ts +42 -0
- package/dist/firefox-cookies.js +170 -0
- package/dist/firefox-cookies.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/key-encryption.d.ts +21 -0
- package/dist/key-encryption.js +59 -0
- package/dist/key-encryption.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +35 -0
- package/dist/logger.js.map +1 -0
- package/dist/proxy-logic.d.ts +108 -0
- package/dist/proxy-logic.js +353 -0
- package/dist/proxy-logic.js.map +1 -0
- package/dist/proxy.d.ts +35 -0
- package/dist/proxy.js +551 -0
- package/dist/proxy.js.map +1 -0
- package/dist/scraper.d.ts +70 -0
- package/dist/scraper.js +239 -0
- package/dist/scraper.js.map +1 -0
- package/dist/validation.d.ts +34 -0
- package/dist/validation.js +64 -0
- package/dist/validation.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* key-encryption.ts — AES-256-GCM encryption for API keys using scrypt KDF.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in crypto module (no external dependencies).
|
|
5
|
+
* Format: enc:scrypt:<salt>:<iv>:<ciphertext>:<authTag>
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Encrypt a plaintext API key using AES-256-GCM with scrypt KDF.
|
|
9
|
+
* Returns format: "enc:scrypt:<salt>:<iv>:<ciphertext>:<authTag>"
|
|
10
|
+
*/
|
|
11
|
+
export declare function encryptKey(plaintext: string, masterKey: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Decrypt an encrypted API key.
|
|
14
|
+
* Input format: "enc:scrypt:<salt>:<iv>:<ciphertext>:<authTag>"
|
|
15
|
+
* Throws clear error if decryption fails (wrong key or corrupted data).
|
|
16
|
+
*/
|
|
17
|
+
export declare function decryptKey(encrypted: string, masterKey: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Check if a key value is encrypted (starts with "enc:scrypt:").
|
|
20
|
+
*/
|
|
21
|
+
export declare function isEncryptedKey(value: string): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* key-encryption.ts — AES-256-GCM encryption for API keys using scrypt KDF.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in crypto module (no external dependencies).
|
|
5
|
+
* Format: enc:scrypt:<salt>:<iv>:<ciphertext>:<authTag>
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
8
|
+
const ALGO = 'aes-256-gcm';
|
|
9
|
+
const KEY_LEN = 32;
|
|
10
|
+
const SALT_LEN = 16;
|
|
11
|
+
const IV_LEN = 12;
|
|
12
|
+
const SCRYPT_OPTS = { N: 16384, r: 8, p: 1 };
|
|
13
|
+
/**
|
|
14
|
+
* Encrypt a plaintext API key using AES-256-GCM with scrypt KDF.
|
|
15
|
+
* Returns format: "enc:scrypt:<salt>:<iv>:<ciphertext>:<authTag>"
|
|
16
|
+
*/
|
|
17
|
+
export function encryptKey(plaintext, masterKey) {
|
|
18
|
+
const salt = randomBytes(SALT_LEN);
|
|
19
|
+
const iv = randomBytes(IV_LEN);
|
|
20
|
+
const key = scryptSync(masterKey, salt, KEY_LEN, SCRYPT_OPTS);
|
|
21
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
22
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
23
|
+
const authTag = cipher.getAuthTag();
|
|
24
|
+
return `enc:scrypt:${salt.toString('base64')}:${iv.toString('base64')}:${encrypted.toString('base64')}:${authTag.toString('base64')}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Decrypt an encrypted API key.
|
|
28
|
+
* Input format: "enc:scrypt:<salt>:<iv>:<ciphertext>:<authTag>"
|
|
29
|
+
* Throws clear error if decryption fails (wrong key or corrupted data).
|
|
30
|
+
*/
|
|
31
|
+
export function decryptKey(encrypted, masterKey) {
|
|
32
|
+
const parts = encrypted.split(':');
|
|
33
|
+
if (parts.length !== 6 || parts[0] !== 'enc' || parts[1] !== 'scrypt') {
|
|
34
|
+
throw new Error('Invalid encrypted key format');
|
|
35
|
+
}
|
|
36
|
+
const [, , saltB64, ivB64, ciphertextB64, authTagB64] = parts;
|
|
37
|
+
try {
|
|
38
|
+
const salt = Buffer.from(saltB64, 'base64');
|
|
39
|
+
const iv = Buffer.from(ivB64, 'base64');
|
|
40
|
+
const ciphertext = Buffer.from(ciphertextB64, 'base64');
|
|
41
|
+
const authTag = Buffer.from(authTagB64, 'base64');
|
|
42
|
+
const key = scryptSync(masterKey, salt, KEY_LEN, SCRYPT_OPTS);
|
|
43
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
44
|
+
decipher.setAuthTag(authTag);
|
|
45
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
46
|
+
return decrypted.toString('utf8');
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
throw new Error(`Failed to decrypt key: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a key value is encrypted (starts with "enc:scrypt:").
|
|
55
|
+
*/
|
|
56
|
+
export function isEncryptedKey(value) {
|
|
57
|
+
return value.startsWith('enc:scrypt:');
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=key-encryption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-encryption.js","sourceRoot":"","sources":["../src/key-encryption.ts"],"names":[],"mappings":"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"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.ts — Structured logging with Pino and automatic API key masking.
|
|
3
|
+
*/
|
|
4
|
+
import pino from 'pino';
|
|
5
|
+
/**
|
|
6
|
+
* Mask a secret key: show first 4 and last 4 characters.
|
|
7
|
+
* Masks keys matching common patterns (sk-, key-, api-, token-)
|
|
8
|
+
* or any string longer than 20 characters.
|
|
9
|
+
*/
|
|
10
|
+
export declare function maskKey(key: string): string;
|
|
11
|
+
export declare const logger: pino.Logger<never, boolean>;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.ts — Structured logging with Pino and automatic API key masking.
|
|
3
|
+
*/
|
|
4
|
+
import pino from 'pino';
|
|
5
|
+
/**
|
|
6
|
+
* Mask a secret key: show first 4 and last 4 characters.
|
|
7
|
+
* Masks keys matching common patterns (sk-, key-, api-, token-)
|
|
8
|
+
* or any string longer than 20 characters.
|
|
9
|
+
*/
|
|
10
|
+
export function maskKey(key) {
|
|
11
|
+
if (!key || key.length < 4)
|
|
12
|
+
return '****';
|
|
13
|
+
// Detect common secret patterns
|
|
14
|
+
const patterns = [/^sk-/, /^key-/, /^api-/, /^token-/];
|
|
15
|
+
const isSecret = patterns.some((p) => p.test(key)) || key.length > 20;
|
|
16
|
+
if (!isSecret)
|
|
17
|
+
return key; // Not a known secret pattern, show as-is
|
|
18
|
+
// Mask: show first 4 and last 4 chars
|
|
19
|
+
if (key.length <= 12)
|
|
20
|
+
return key.slice(0, 4) + '****';
|
|
21
|
+
return key.slice(0, 4) + '...' + key.slice(-4);
|
|
22
|
+
}
|
|
23
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
24
|
+
export const logger = pino({
|
|
25
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
26
|
+
...(isDev
|
|
27
|
+
? {
|
|
28
|
+
transport: {
|
|
29
|
+
target: 'pino-pretty',
|
|
30
|
+
options: { colorize: true },
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IAE1C,gCAAgC;IAChC,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;IAEtE,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,CAAC,yCAAyC;IAEpE,sCAAsC;IACtC,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC;IACtD,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;AAEpD,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;IACtC,GAAG,CAAC,KAAK;QACP,CAAC,CAAC;YACE,SAAS,EAAE;gBACT,MAAM,EAAE,aAAa;gBACrB,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aAC5B;SACF;QACH,CAAC,CAAC,EAAE,CAAC;CACR,CAAC,CAAC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proxy-logic.ts — Pure production logic. Saros picks the best key per request.
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no console.log, no HTTP — completely portable and testable.
|
|
5
|
+
*
|
|
6
|
+
* Critical fixes over prototype:
|
|
7
|
+
* C1. Circuit breaker cooldown (lazy auto-re-enable via disabledAt)
|
|
8
|
+
* C2. Error classification (KeyFault | RequestFault | ServerFault)
|
|
9
|
+
* C3. Read-only KeySnapshot returned instead of mutable ApiKey reference
|
|
10
|
+
* C4. Request-scoped key tracking (activeRequests map)
|
|
11
|
+
*/
|
|
12
|
+
interface ApiKey {
|
|
13
|
+
label: string;
|
|
14
|
+
key: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
consecutiveFailures: number;
|
|
17
|
+
lastUsed: number | null;
|
|
18
|
+
disabledAt: number | null;
|
|
19
|
+
}
|
|
20
|
+
/** Read-only projection returned to callers — cannot mutate internal state. */
|
|
21
|
+
export interface KeySnapshot {
|
|
22
|
+
label: string;
|
|
23
|
+
key: string;
|
|
24
|
+
}
|
|
25
|
+
/** Tracks a single HTTP request's key usage through its lifecycle. */
|
|
26
|
+
interface RequestContext {
|
|
27
|
+
requestId: string;
|
|
28
|
+
triedKeys: string[];
|
|
29
|
+
currentKey: KeySnapshot | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ProxyState {
|
|
32
|
+
keys: ApiKey[];
|
|
33
|
+
currentIndex: number;
|
|
34
|
+
circuitBreakerThreshold: number;
|
|
35
|
+
circuitBreakerCooldownMs: number;
|
|
36
|
+
activeRequests: Map<string, RequestContext>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Usage data for a single account, scraped from the OpenCode-Go workspace.
|
|
40
|
+
* All values are percentages (0-100). null means no data for that window.
|
|
41
|
+
*/
|
|
42
|
+
export interface UsageInfo {
|
|
43
|
+
rolling: number | null;
|
|
44
|
+
weekly: number | null;
|
|
45
|
+
monthly: number | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Optional parameters for usage-gated key selection.
|
|
49
|
+
*/
|
|
50
|
+
export interface KeySelectionOptions {
|
|
51
|
+
usageMap?: Map<string, UsageInfo>;
|
|
52
|
+
usageThreshold?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Error classification for HTTP responses.
|
|
56
|
+
* - KeyFault: the API key should be penalised (invalid, revoked, rate-limited, server fault)
|
|
57
|
+
* - RequestFault: the request itself is bad — no key penalisation
|
|
58
|
+
* - ServerFault: transient server issue — key penalised (normal circuit-breaker)
|
|
59
|
+
*/
|
|
60
|
+
export type ErrorType = 'KeyFault' | 'RequestFault' | 'ServerFault';
|
|
61
|
+
export declare function createProxyState(keys: Array<{
|
|
62
|
+
label: string;
|
|
63
|
+
key: string;
|
|
64
|
+
}>, options?: {
|
|
65
|
+
circuitBreakerThreshold?: number;
|
|
66
|
+
circuitBreakerCooldownMs?: number;
|
|
67
|
+
}): ProxyState;
|
|
68
|
+
/**
|
|
69
|
+
* Obtain a key for a new (or in-flight) request.
|
|
70
|
+
* Creates a RequestContext if one does not already exist for this requestId.
|
|
71
|
+
* Skips keys that have already been tried for this request AND keys booked
|
|
72
|
+
* by any other active request.
|
|
73
|
+
*/
|
|
74
|
+
export declare function selectKeyForRequest(state: ProxyState, requestId: string, options?: KeySelectionOptions): KeySnapshot | null;
|
|
75
|
+
/**
|
|
76
|
+
* Failover: record the current key of this request as failed and pick the next
|
|
77
|
+
* available key. The caller should also call markKeyFailed() with the
|
|
78
|
+
* appropriate ErrorType; this function only manages the request-context
|
|
79
|
+
* tracking (adds the current key to triedKeys).
|
|
80
|
+
*/
|
|
81
|
+
export declare function failoverRequest(state: ProxyState, requestId: string, options?: KeySelectionOptions): KeySnapshot | null;
|
|
82
|
+
/**
|
|
83
|
+
* Complete a request: if successful, mark the current key as succeeded;
|
|
84
|
+
* always clean up the request context.
|
|
85
|
+
*/
|
|
86
|
+
export declare function completeRequest(state: ProxyState, requestId: string, success: boolean): void;
|
|
87
|
+
/**
|
|
88
|
+
* Mark a key as failed according to the error type (C2).
|
|
89
|
+
* - KeyFault / ServerFault: penalise the key (KeyFault disables immediately)
|
|
90
|
+
* - RequestFault: no penalisation (bad request, not bad key)
|
|
91
|
+
*/
|
|
92
|
+
export declare function markKeyFailed(state: ProxyState, keyLabel: string, errorType: ErrorType): void;
|
|
93
|
+
/**
|
|
94
|
+
* Mark a key as succeeded — resets consecutive failures.
|
|
95
|
+
*/
|
|
96
|
+
export declare function markKeySucceeded(state: ProxyState, keyLabel: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* Classify an HTTP error status + optional response body into an ErrorType.
|
|
99
|
+
*
|
|
100
|
+
* Rules:
|
|
101
|
+
* 401 → KeyFault (invalid/revoked key — immediate disable)
|
|
102
|
+
* 429 → KeyFault (rate-limited)
|
|
103
|
+
* 500 / 502 / 503 → KeyFault if body mentions quota/balance, else ServerFault
|
|
104
|
+
* 400 / 404 / 422 → RequestFault (bad request — don't penalise the key)
|
|
105
|
+
* everything else → ServerFault
|
|
106
|
+
*/
|
|
107
|
+
export declare function classifyHttpError(status: number, _body?: string): ErrorType;
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proxy-logic.ts — Pure production logic. Saros picks the best key per request.
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no console.log, no HTTP — completely portable and testable.
|
|
5
|
+
*
|
|
6
|
+
* Critical fixes over prototype:
|
|
7
|
+
* C1. Circuit breaker cooldown (lazy auto-re-enable via disabledAt)
|
|
8
|
+
* C2. Error classification (KeyFault | RequestFault | ServerFault)
|
|
9
|
+
* C3. Read-only KeySnapshot returned instead of mutable ApiKey reference
|
|
10
|
+
* C4. Request-scoped key tracking (activeRequests map)
|
|
11
|
+
*/
|
|
12
|
+
import { DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS } from './constants.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// State Factory
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export function createProxyState(keys, options) {
|
|
17
|
+
return {
|
|
18
|
+
keys: keys.map((k) => ({
|
|
19
|
+
label: k.label,
|
|
20
|
+
key: k.key,
|
|
21
|
+
enabled: true,
|
|
22
|
+
consecutiveFailures: 0,
|
|
23
|
+
lastUsed: null,
|
|
24
|
+
disabledAt: null,
|
|
25
|
+
})),
|
|
26
|
+
currentIndex: 0,
|
|
27
|
+
circuitBreakerThreshold: options?.circuitBreakerThreshold ?? 3,
|
|
28
|
+
circuitBreakerCooldownMs: options?.circuitBreakerCooldownMs ?? DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS,
|
|
29
|
+
activeRequests: new Map(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Internal Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* Internal: return a read-only snapshot of the key already selected.
|
|
37
|
+
* (Keeps mutation paths through markKeyFailed / markKeySucceeded / reenableKey.)
|
|
38
|
+
*/
|
|
39
|
+
function toSnapshot(key) {
|
|
40
|
+
return { label: key.label, key: key.key };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Internal: check whether a key is available (enabled & under threshold).
|
|
44
|
+
* If the key is disabled but past its cooldown window, it is lazily re-enabled
|
|
45
|
+
* (C1 — fixes dead-code cooldown).
|
|
46
|
+
*/
|
|
47
|
+
function isKeyAvailable(state, key) {
|
|
48
|
+
if (key.enabled && key.consecutiveFailures < state.circuitBreakerThreshold) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// C1: lazy auto-re-enable after cooldown expires
|
|
52
|
+
if (!key.enabled && key.disabledAt !== null) {
|
|
53
|
+
const elapsed = Date.now() - key.disabledAt;
|
|
54
|
+
if (elapsed >= state.circuitBreakerCooldownMs) {
|
|
55
|
+
key.enabled = true;
|
|
56
|
+
key.consecutiveFailures = 0;
|
|
57
|
+
key.disabledAt = null;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if a key is available based on usage data.
|
|
65
|
+
* A key is unavailable if ANY of its usage windows >= threshold.
|
|
66
|
+
*
|
|
67
|
+
* @param usage - The usage data for this key's account (null = no data = available)
|
|
68
|
+
* @param threshold - The usage threshold (1-100)
|
|
69
|
+
* @returns true if the key is available (usage below threshold or no data)
|
|
70
|
+
*/
|
|
71
|
+
function isKeyAvailableByUsage(usage, threshold) {
|
|
72
|
+
if (usage === null)
|
|
73
|
+
return true; // no data = don't block
|
|
74
|
+
return ((usage.rolling === null || usage.rolling < threshold) &&
|
|
75
|
+
(usage.weekly === null || usage.weekly < threshold) &&
|
|
76
|
+
(usage.monthly === null || usage.monthly < threshold));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Internal: when all circuit-breaker-available keys are over the usage
|
|
80
|
+
* threshold, fall back to the key with the lowest maximum usage across
|
|
81
|
+
* all windows. If a key has no usage data, it is preferred immediately.
|
|
82
|
+
*/
|
|
83
|
+
function findFallbackKey(state, usageMap) {
|
|
84
|
+
let best = null;
|
|
85
|
+
let bestMaxUsage = Infinity;
|
|
86
|
+
for (const key of state.keys) {
|
|
87
|
+
if (!isKeyAvailable(state, key))
|
|
88
|
+
continue;
|
|
89
|
+
const usage = usageMap.get(key.label);
|
|
90
|
+
if (!usage) {
|
|
91
|
+
// No usage data = best possible – take it immediately
|
|
92
|
+
key.lastUsed = Date.now();
|
|
93
|
+
return toSnapshot(key);
|
|
94
|
+
}
|
|
95
|
+
const maxUsage = Math.max(usage.rolling ?? 0, usage.weekly ?? 0, usage.monthly ?? 0);
|
|
96
|
+
if (maxUsage < bestMaxUsage) {
|
|
97
|
+
bestMaxUsage = maxUsage;
|
|
98
|
+
best = key;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (best) {
|
|
102
|
+
best.lastUsed = Date.now();
|
|
103
|
+
return toSnapshot(best);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Internal: apply usage-gating to an exclusion set, then attempt to find
|
|
109
|
+
* the next available key. Falls back to findFallbackKey if all
|
|
110
|
+
* circuit-breaker-available keys are over the usage threshold.
|
|
111
|
+
*
|
|
112
|
+
* Shared by selectKeyForRequest and failoverRequest — extracts the ~90%
|
|
113
|
+
* identical usage-gating logic that was previously copy-pasted (PR-5).
|
|
114
|
+
*
|
|
115
|
+
* @param excludeLabels Mutable set; usage-over-threshold labels are added.
|
|
116
|
+
* @returns A key snapshot, or null if no key is available.
|
|
117
|
+
*/
|
|
118
|
+
function selectKeyWithUsageFallback(state, excludeLabels, options) {
|
|
119
|
+
// Exclude keys whose account usage exceeds the threshold
|
|
120
|
+
if (options?.usageMap && options?.usageThreshold !== undefined) {
|
|
121
|
+
const threshold = options.usageThreshold;
|
|
122
|
+
for (const key of state.keys) {
|
|
123
|
+
const usage = options.usageMap.get(key.label) ?? null;
|
|
124
|
+
if (!isKeyAvailableByUsage(usage, threshold)) {
|
|
125
|
+
excludeLabels.add(key.label);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let snapshot = findNextKey(state, excludeLabels);
|
|
130
|
+
// Fallback: all circuit-breaker-available keys are over threshold
|
|
131
|
+
if (!snapshot && options?.usageMap) {
|
|
132
|
+
const fallback = findFallbackKey(state, options.usageMap);
|
|
133
|
+
if (fallback) {
|
|
134
|
+
const idx = state.keys.findIndex((k) => k.label === fallback.label);
|
|
135
|
+
if (idx !== -1) {
|
|
136
|
+
state.currentIndex = (idx + 1) % state.keys.length;
|
|
137
|
+
}
|
|
138
|
+
snapshot = fallback;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return snapshot;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Internal: return the set of key labels that are currently *in use* by
|
|
145
|
+
* active requests (C4 double-booking avoidance).
|
|
146
|
+
*
|
|
147
|
+
* Previously this collected ALL ctx.triedKeys, which meant an abandoned key
|
|
148
|
+
* (one a request failed over from) stayed globally excluded even though
|
|
149
|
+
* nobody was using it. Now it only tracks the key each request is actively
|
|
150
|
+
* using (ctx.currentKey). Tried keys are still excluded per-request via
|
|
151
|
+
* the per-request excludeLabels in selectKeyForRequest/failoverRequest.
|
|
152
|
+
*/
|
|
153
|
+
function buildBookedLabels(state) {
|
|
154
|
+
const booked = new Set();
|
|
155
|
+
for (const [, ctx] of state.activeRequests) {
|
|
156
|
+
if (ctx.currentKey) {
|
|
157
|
+
booked.add(ctx.currentKey.label);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return booked;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Internal: find the next available key, skipping:
|
|
164
|
+
* - disabled / under-cooldown keys
|
|
165
|
+
* - labels in `excludeLabels`
|
|
166
|
+
* Advances the round-robin index and returns a read-only snapshot.
|
|
167
|
+
*/
|
|
168
|
+
function findNextKey(state, excludeLabels) {
|
|
169
|
+
const n = state.keys.length;
|
|
170
|
+
if (n === 0)
|
|
171
|
+
return null;
|
|
172
|
+
for (let i = 0; i < n; i++) {
|
|
173
|
+
const idx = (state.currentIndex + i) % n;
|
|
174
|
+
const key = state.keys[idx];
|
|
175
|
+
if (excludeLabels.has(key.label))
|
|
176
|
+
continue;
|
|
177
|
+
if (!isKeyAvailable(state, key))
|
|
178
|
+
continue;
|
|
179
|
+
// Advance past the selected key
|
|
180
|
+
state.currentIndex = (idx + 1) % n;
|
|
181
|
+
key.lastUsed = Date.now();
|
|
182
|
+
return toSnapshot(key);
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Request-Scoped Key Tracking (C4)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
/**
|
|
190
|
+
* Obtain a key for a new (or in-flight) request.
|
|
191
|
+
* Creates a RequestContext if one does not already exist for this requestId.
|
|
192
|
+
* Skips keys that have already been tried for this request AND keys booked
|
|
193
|
+
* by any other active request.
|
|
194
|
+
*/
|
|
195
|
+
export function selectKeyForRequest(state, requestId, options) {
|
|
196
|
+
// If context already exists, return the already-assigned current key
|
|
197
|
+
const existing = state.activeRequests.get(requestId);
|
|
198
|
+
if (existing) {
|
|
199
|
+
return existing.currentKey;
|
|
200
|
+
}
|
|
201
|
+
// Create new context and pick a fresh key
|
|
202
|
+
const ctx = { requestId, triedKeys: [], currentKey: null };
|
|
203
|
+
state.activeRequests.set(requestId, ctx);
|
|
204
|
+
const excludeLabels = new Set(ctx.triedKeys);
|
|
205
|
+
// Avoid double-booking: don't assign a key that another request already has
|
|
206
|
+
const booked = buildBookedLabels(state);
|
|
207
|
+
for (const label of booked) {
|
|
208
|
+
excludeLabels.add(label);
|
|
209
|
+
}
|
|
210
|
+
const snapshot = selectKeyWithUsageFallback(state, excludeLabels, options);
|
|
211
|
+
ctx.currentKey = snapshot;
|
|
212
|
+
return snapshot;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Failover: record the current key of this request as failed and pick the next
|
|
216
|
+
* available key. The caller should also call markKeyFailed() with the
|
|
217
|
+
* appropriate ErrorType; this function only manages the request-context
|
|
218
|
+
* tracking (adds the current key to triedKeys).
|
|
219
|
+
*/
|
|
220
|
+
export function failoverRequest(state, requestId, options) {
|
|
221
|
+
const ctx = state.activeRequests.get(requestId);
|
|
222
|
+
if (!ctx)
|
|
223
|
+
return null;
|
|
224
|
+
// Record current key as tried
|
|
225
|
+
if (ctx.currentKey) {
|
|
226
|
+
ctx.triedKeys.push(ctx.currentKey.label);
|
|
227
|
+
}
|
|
228
|
+
// Pick next excluding tried keys + double-booking guard
|
|
229
|
+
const excludeLabels = new Set(ctx.triedKeys);
|
|
230
|
+
const booked = buildBookedLabels(state);
|
|
231
|
+
for (const label of booked) {
|
|
232
|
+
excludeLabels.add(label);
|
|
233
|
+
}
|
|
234
|
+
const snapshot = selectKeyWithUsageFallback(state, excludeLabels, options);
|
|
235
|
+
ctx.currentKey = snapshot;
|
|
236
|
+
return snapshot;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Complete a request: if successful, mark the current key as succeeded;
|
|
240
|
+
* always clean up the request context.
|
|
241
|
+
*/
|
|
242
|
+
export function completeRequest(state, requestId, success) {
|
|
243
|
+
const ctx = state.activeRequests.get(requestId);
|
|
244
|
+
if (!ctx)
|
|
245
|
+
return;
|
|
246
|
+
if (success && ctx.currentKey) {
|
|
247
|
+
markKeySucceeded(state, ctx.currentKey.label);
|
|
248
|
+
}
|
|
249
|
+
state.activeRequests.delete(requestId);
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Key Status Mutations
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
/**
|
|
255
|
+
* Mark a key as failed according to the error type (C2).
|
|
256
|
+
* - KeyFault / ServerFault: penalise the key (KeyFault disables immediately)
|
|
257
|
+
* - RequestFault: no penalisation (bad request, not bad key)
|
|
258
|
+
*/
|
|
259
|
+
export function markKeyFailed(state, keyLabel, errorType) {
|
|
260
|
+
if (errorType === 'RequestFault')
|
|
261
|
+
return;
|
|
262
|
+
const key = state.keys.find((k) => k.label === keyLabel);
|
|
263
|
+
if (!key)
|
|
264
|
+
return;
|
|
265
|
+
if (errorType === 'KeyFault') {
|
|
266
|
+
// Disable immediately — the key is compromised / rate-limited
|
|
267
|
+
key.consecutiveFailures = state.circuitBreakerThreshold;
|
|
268
|
+
key.enabled = false;
|
|
269
|
+
key.disabledAt = Date.now(); // C1: start cooldown timer
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// ServerFault: normal circuit-breaker increment
|
|
273
|
+
key.consecutiveFailures++;
|
|
274
|
+
if (key.consecutiveFailures >= state.circuitBreakerThreshold) {
|
|
275
|
+
key.enabled = false;
|
|
276
|
+
key.disabledAt = Date.now(); // C1: start cooldown timer
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Mark a key as succeeded — resets consecutive failures.
|
|
282
|
+
*/
|
|
283
|
+
export function markKeySucceeded(state, keyLabel) {
|
|
284
|
+
const key = state.keys.find((k) => k.label === keyLabel);
|
|
285
|
+
if (!key)
|
|
286
|
+
return;
|
|
287
|
+
key.consecutiveFailures = 0;
|
|
288
|
+
// C1: clear cooldown on success so next selection isn't penalised
|
|
289
|
+
key.disabledAt = null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Manually re-enable a previously disabled key.
|
|
293
|
+
*/
|
|
294
|
+
function reenableKey(state, keyLabel) {
|
|
295
|
+
const key = state.keys.find((k) => k.label === keyLabel);
|
|
296
|
+
if (!key)
|
|
297
|
+
return;
|
|
298
|
+
key.enabled = true;
|
|
299
|
+
key.consecutiveFailures = 0;
|
|
300
|
+
key.disabledAt = null; // C1: clear cooldown
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Status Queries
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
/**
|
|
306
|
+
* Check whether a key is currently disabled.
|
|
307
|
+
* A key that has passed its cooldown window is considered no longer disabled
|
|
308
|
+
* (lazy re-enable happens on next selection, but this query reflects that).
|
|
309
|
+
*/
|
|
310
|
+
function isKeyDisabled(state, keyLabel) {
|
|
311
|
+
const key = state.keys.find((k) => k.label === keyLabel);
|
|
312
|
+
if (!key)
|
|
313
|
+
return true;
|
|
314
|
+
if (!key.enabled) {
|
|
315
|
+
// C1: past cooldown → effectively not disabled anymore
|
|
316
|
+
if (key.disabledAt !== null &&
|
|
317
|
+
Date.now() - key.disabledAt >= state.circuitBreakerCooldownMs) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return key.consecutiveFailures >= state.circuitBreakerThreshold;
|
|
323
|
+
}
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Error Classification (C2)
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
/**
|
|
328
|
+
* Classify an HTTP error status + optional response body into an ErrorType.
|
|
329
|
+
*
|
|
330
|
+
* Rules:
|
|
331
|
+
* 401 → KeyFault (invalid/revoked key — immediate disable)
|
|
332
|
+
* 429 → KeyFault (rate-limited)
|
|
333
|
+
* 500 / 502 / 503 → KeyFault if body mentions quota/balance, else ServerFault
|
|
334
|
+
* 400 / 404 / 422 → RequestFault (bad request — don't penalise the key)
|
|
335
|
+
* everything else → ServerFault
|
|
336
|
+
*/
|
|
337
|
+
export function classifyHttpError(status, _body) {
|
|
338
|
+
if (status === 401)
|
|
339
|
+
return 'KeyFault';
|
|
340
|
+
if (status === 429)
|
|
341
|
+
return 'KeyFault';
|
|
342
|
+
if (status === 400 || status === 404 || status === 422)
|
|
343
|
+
return 'RequestFault';
|
|
344
|
+
if (status === 500 || status === 502 || status === 503) {
|
|
345
|
+
// Server errors: if the body hints at a quota/balance issue treat as KeyFault
|
|
346
|
+
if (_body && /quota|balance|insufficient|limit/i.test(_body)) {
|
|
347
|
+
return 'KeyFault';
|
|
348
|
+
}
|
|
349
|
+
return 'ServerFault';
|
|
350
|
+
}
|
|
351
|
+
return 'ServerFault';
|
|
352
|
+
}
|
|
353
|
+
//# sourceMappingURL=proxy-logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-logic.js","sourceRoot":"","sources":["../src/proxy-logic.ts"],"names":[],"mappings":"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"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proxy.ts — Hono HTTP proxy app. Saros predicts account exhaustion and cycles between accounts.
|
|
3
|
+
*
|
|
4
|
+
* Routes /zen/go/v1/* requests to the upstream API with automatic key
|
|
5
|
+
* selection, circuit-breaker failover, and streaming pass-through.
|
|
6
|
+
*/
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import type { ProxyConfig } from './config.js';
|
|
9
|
+
declare module 'hono' {
|
|
10
|
+
interface ContextVariableMap {
|
|
11
|
+
remote: {
|
|
12
|
+
address: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build upstream URL from base + request path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildUpstreamUrl(base: string, path: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Clean headers for upstream forwarding: remove hop-by-hop headers
|
|
22
|
+
* and any pre-existing Authorization.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildUpstreamHeaders(incoming: Headers, bearerToken: string): Headers;
|
|
25
|
+
/**
|
|
26
|
+
* Clean headers for downstream (client) response: strip hop-by-hop headers,
|
|
27
|
+
* security-sensitive headers, internal debug headers, and content-length
|
|
28
|
+
* (the Response constructor recalculates content-length automatically).
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildDownstreamHeaders(upstreamHeaders: Headers): Headers;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a request body indicates streaming mode.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isStreamingRequest(bodyText: string): boolean;
|
|
35
|
+
export declare function createProxyApp(config: ProxyConfig): Hono;
|