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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +620 -0
  3. package/dist/cli/opencode-config.d.ts +28 -0
  4. package/dist/cli/opencode-config.js +126 -0
  5. package/dist/cli/opencode-config.js.map +1 -0
  6. package/dist/cli/setup.d.ts +29 -0
  7. package/dist/cli/setup.js +669 -0
  8. package/dist/cli/setup.js.map +1 -0
  9. package/dist/cli/ui.d.ts +27 -0
  10. package/dist/cli/ui.js +116 -0
  11. package/dist/cli/ui.js.map +1 -0
  12. package/dist/config.d.ts +50 -0
  13. package/dist/config.js +341 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/constants.d.ts +25 -0
  16. package/dist/constants.js +162 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/dashboard-parser.d.ts +47 -0
  19. package/dist/dashboard-parser.js +88 -0
  20. package/dist/dashboard-parser.js.map +1 -0
  21. package/dist/firefox-cookies.d.ts +42 -0
  22. package/dist/firefox-cookies.js +170 -0
  23. package/dist/firefox-cookies.js.map +1 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +101 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/key-encryption.d.ts +21 -0
  28. package/dist/key-encryption.js +59 -0
  29. package/dist/key-encryption.js.map +1 -0
  30. package/dist/logger.d.ts +11 -0
  31. package/dist/logger.js +35 -0
  32. package/dist/logger.js.map +1 -0
  33. package/dist/proxy-logic.d.ts +108 -0
  34. package/dist/proxy-logic.js +353 -0
  35. package/dist/proxy-logic.js.map +1 -0
  36. package/dist/proxy.d.ts +35 -0
  37. package/dist/proxy.js +551 -0
  38. package/dist/proxy.js.map +1 -0
  39. package/dist/scraper.d.ts +70 -0
  40. package/dist/scraper.js +239 -0
  41. package/dist/scraper.js.map +1 -0
  42. package/dist/validation.d.ts +34 -0
  43. package/dist/validation.js +64 -0
  44. package/dist/validation.js.map +1 -0
  45. 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":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAExF,MAAM,IAAI,GAAG,aAAa,CAAC;AAC3B,MAAM,OAAO,GAAG,EAAE,CAAC;AACnB,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,MAAM,GAAG,EAAE,CAAC;AAClB,MAAM,WAAW,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AAE7C;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,SAAiB,EAAE,SAAiB;IAC7D,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;IAE9D,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAEpC,OAAO,cAAc,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACxI,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,SAAiB,EAAE,SAAiB;IAC7D,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,CAAC,EAAE,AAAD,EAAG,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC;IAE9D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAElD,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACjD,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAE7B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACjF,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAC;IACvD,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AACzC,CAAC"}
@@ -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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"}
@@ -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;