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,239 @@
1
+ /**
2
+ * scraper.ts — Dashboard scraper; Saros uses this to predict account exhaustion.
3
+ *
4
+ * Provides both one-shot scraping (scrapeDashboard) and a background scraper
5
+ * that periodically fetches dashboard pages for all configured accounts.
6
+ * Uses recursive setTimeout (not setInterval) with AbortController to prevent
7
+ * overlapping cycles and to support clean shutdown.
8
+ */
9
+ import { parseAllUsage } from './dashboard-parser.js';
10
+ import { logger } from './logger.js';
11
+ /** Module-level store: workspaceId → AccountUsage */
12
+ const usageStore = new Map();
13
+ /** Active timer handle, if any */
14
+ let scraperTimer = null;
15
+ /** AbortController for the current scrape cycle, if any */
16
+ let abortController = null;
17
+ // ---------------------------------------------------------------------------
18
+ // Constants
19
+ // ---------------------------------------------------------------------------
20
+ const FIREFOX_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0';
21
+ const DEFAULT_TIMEOUT_MS = 15_000;
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Combine multiple AbortSignals into one — fires when any of them fire.
27
+ * Cleans up listeners when any signal fires or when the resulting signal
28
+ * is garbage collected.
29
+ */
30
+ function anySignal(signals) {
31
+ const controller = new AbortController();
32
+ for (const signal of signals) {
33
+ if (signal.aborted) {
34
+ controller.abort(signal.reason);
35
+ return controller.signal;
36
+ }
37
+ signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
38
+ }
39
+ return controller.signal;
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Public API
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Fetch the OpenCode-Go dashboard and parse usage percentages.
46
+ *
47
+ * Makes a single HTTP GET request to `https://opencode.ai/workspace/{workspaceId}/go`
48
+ * with the provided auth cookie. On a successful 2xx response the HTML is parsed
49
+ * with {@link parseAllUsage} and the resulting usage data is returned.
50
+ *
51
+ * @param workspaceId — The OpenCode workspace identifier.
52
+ * @param authCookie — The `auth` cookie value for authentication.
53
+ * @param options - Optional settings.
54
+ * @param options.timeoutMs - Request timeout in milliseconds (default: 15000).
55
+ * @returns A {@link ScrapeResult} with status, parsed data, and timing info.
56
+ */
57
+ export async function scrapeDashboard(workspaceId, authCookie, options) {
58
+ const start = performance.now();
59
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60
+ const url = `https://opencode.ai/workspace/${encodeURIComponent(workspaceId)}/go`;
61
+ try {
62
+ // Combine external signal with timeout via race
63
+ const externalSignal = options?.signal;
64
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
65
+ const combinedSignal = externalSignal
66
+ ? anySignal([externalSignal, timeoutSignal])
67
+ : timeoutSignal;
68
+ const response = await fetch(url, {
69
+ headers: {
70
+ Cookie: `auth=${authCookie}`,
71
+ 'User-Agent': FIREFOX_UA,
72
+ },
73
+ signal: combinedSignal,
74
+ });
75
+ const httpStatus = response.status;
76
+ const durationMs = Math.round(performance.now() - start);
77
+ // Classify HTTP error status into a user-facing error message
78
+ if (!response.ok) {
79
+ let error;
80
+ if (httpStatus === 401 || httpStatus === 403) {
81
+ error = 'Cookie expired or invalid';
82
+ }
83
+ else if (httpStatus === 429) {
84
+ error = 'Rate limited by opencode.ai';
85
+ }
86
+ else {
87
+ error = `HTTP ${httpStatus}`;
88
+ }
89
+ return {
90
+ success: false,
91
+ error,
92
+ httpStatus,
93
+ durationMs,
94
+ };
95
+ }
96
+ // Parse HTML body
97
+ const html = await response.text();
98
+ const usage = parseAllUsage(html);
99
+ return {
100
+ success: true,
101
+ usage,
102
+ httpStatus,
103
+ durationMs,
104
+ };
105
+ }
106
+ catch (err) {
107
+ const durationMs = Math.round(performance.now() - start);
108
+ // Timeout
109
+ if (err instanceof DOMException && err.name === 'TimeoutError') {
110
+ return {
111
+ success: false,
112
+ error: 'Request timed out',
113
+ durationMs,
114
+ };
115
+ }
116
+ // Intentional abort (from our AbortController during stop)
117
+ if (err instanceof DOMException && err.name === 'AbortError') {
118
+ return {
119
+ success: false,
120
+ error: 'Aborted',
121
+ durationMs,
122
+ };
123
+ }
124
+ // Network or other errors
125
+ const message = err instanceof Error ? err.message : 'Unknown fetch error';
126
+ return {
127
+ success: false,
128
+ error: message,
129
+ durationMs,
130
+ };
131
+ }
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Usage store accessors
135
+ // ---------------------------------------------------------------------------
136
+ /**
137
+ * Get all usage data (for health endpoint).
138
+ */
139
+ export function getAllUsage() {
140
+ return new Map(usageStore);
141
+ }
142
+ /**
143
+ * Clear the in-memory usage store (for testing).
144
+ * Production code should NOT call this unless resetting state is intentional.
145
+ */
146
+ export function clearUsageStore() {
147
+ usageStore.clear();
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // Timer management
151
+ // ---------------------------------------------------------------------------
152
+ /**
153
+ * Scrape a single account and update the store.
154
+ */
155
+ async function scrapeAndStore(account, signal) {
156
+ const result = await scrapeDashboard(account.workspaceId, account.authCookie, { signal });
157
+ if (result.success && result.usage) {
158
+ usageStore.set(account.workspaceId, {
159
+ usage: result.usage,
160
+ lastScrapedAt: new Date(),
161
+ });
162
+ logger.info({ workspaceId: account.workspaceId, durationMs: result.durationMs }, 'Scraped dashboard for %s', account.workspaceId);
163
+ }
164
+ else {
165
+ const prev = usageStore.get(account.workspaceId);
166
+ usageStore.set(account.workspaceId, {
167
+ usage: prev?.usage ?? { rolling: null, weekly: null, monthly: null },
168
+ lastScrapedAt: new Date(),
169
+ lastError: result.error ?? 'Unknown error',
170
+ });
171
+ logger.warn({ workspaceId: account.workspaceId, error: result.error, httpStatus: result.httpStatus }, 'Failed to scrape dashboard for %s: %s', account.workspaceId, result.error);
172
+ }
173
+ // Error already logged and stored above — no further action needed
174
+ }
175
+ /**
176
+ * Run a full scrape cycle across all configured accounts sequentially.
177
+ * Checks the abort signal between accounts and returns early if aborted.
178
+ */
179
+ async function runScrapeCycle(accounts, signal) {
180
+ for (const account of accounts) {
181
+ if (signal?.aborted)
182
+ return;
183
+ await scrapeAndStore(account, signal);
184
+ }
185
+ }
186
+ /**
187
+ * Start the background scraper.
188
+ * Scrapes all configured accounts every intervalMs using recursive setTimeout,
189
+ * preventing overlapping cycles. If already running, aborts the previous cycle
190
+ * and clears its timer first.
191
+ */
192
+ export function startScraper(accounts, intervalMs) {
193
+ // Cancel any in-flight cycle and clear any existing timer
194
+ stopScraper();
195
+ const controller = new AbortController();
196
+ abortController = controller;
197
+ const schedule = async () => {
198
+ if (controller.signal.aborted)
199
+ return;
200
+ await runScrapeCycle(accounts, controller.signal).catch((err) => {
201
+ logger.error({ err }, 'Scrape cycle failed');
202
+ });
203
+ if (controller.signal.aborted)
204
+ return;
205
+ scraperTimer = setTimeout(schedule, intervalMs);
206
+ // Allow the process to exit even if the timer is still running
207
+ if (scraperTimer && typeof scraperTimer === 'object' && 'unref' in scraperTimer) {
208
+ scraperTimer.unref();
209
+ }
210
+ };
211
+ // Start first cycle immediately
212
+ schedule().catch((err) => {
213
+ logger.error({ err }, 'Initial scrape cycle failed');
214
+ });
215
+ logger.info('Background scraper started (interval: %d ms, accounts: %d)', intervalMs, accounts.length);
216
+ }
217
+ /**
218
+ * Stop the background scraper.
219
+ * Aborts any in-flight scrape cycle and clears the scheduled timer.
220
+ */
221
+ export function stopScraper() {
222
+ if (abortController !== null) {
223
+ abortController.abort();
224
+ abortController = null;
225
+ }
226
+ if (scraperTimer !== null) {
227
+ clearTimeout(scraperTimer);
228
+ scraperTimer = null;
229
+ }
230
+ // Only log if we actually stopped something — allows idempotent calls from startScraper
231
+ // without spurious "stopped" messages when nothing was running.
232
+ }
233
+ /**
234
+ * Check if the scraper is currently running (either a cycle in flight or a timer scheduled).
235
+ */
236
+ export function isScraperRunning() {
237
+ return scraperTimer !== null || abortController !== null;
238
+ }
239
+ //# sourceMappingURL=scraper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scraper.js","sourceRoot":"","sources":["../src/scraper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * validation.ts — Shared validation functions for config and setup.
3
+ *
4
+ * Centralises input validation patterns used across config.ts and setup.ts
5
+ * to ensure consistent rules and reduce duplication.
6
+ */
7
+ /**
8
+ * Check if the given value is a valid TCP port number (1–65535).
9
+ */
10
+ export declare function isValidPort(value: unknown): value is number;
11
+ /**
12
+ * Check if the given string looks like a valid API key.
13
+ * Must start with `sk-` and be at least {@link MIN_KEY_LENGTH} characters long.
14
+ */
15
+ export declare function isValidApiKey(key: unknown): key is string;
16
+ /**
17
+ * Check if the given value is a valid HTTPS URL.
18
+ * SSRF prevention: only HTTPS URLs are accepted.
19
+ */
20
+ export declare function isValidHttpsUrl(url: unknown): url is string;
21
+ /**
22
+ * Check if the given string is a valid positive integer within an optional range.
23
+ * Trims whitespace before parsing.
24
+ *
25
+ * @param val - The string value to check.
26
+ * @param min - Minimum accepted value (default: 1).
27
+ * @param max - Maximum accepted value (default: 100).
28
+ * @returns `true` if the value is a positive integer in the given range.
29
+ */
30
+ export declare function isValidPositiveInt(val: string, min?: number, max?: number): boolean;
31
+ /**
32
+ * Check if the given string is a valid workspace ID (matches `wrk_[A-Za-z0-9]+`).
33
+ */
34
+ export declare function isValidWorkspaceId(id: unknown): id is string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * validation.ts — Shared validation functions for config and setup.
3
+ *
4
+ * Centralises input validation patterns used across config.ts and setup.ts
5
+ * to ensure consistent rules and reduce duplication.
6
+ */
7
+ import { API_KEY_PREFIX, MIN_KEY_LENGTH, WORKSPACE_ID_REGEX } from './constants.js';
8
+ /**
9
+ * Check if the given value is a valid TCP port number (1–65535).
10
+ */
11
+ export function isValidPort(value) {
12
+ if (typeof value === 'string') {
13
+ const trimmed = value.trim();
14
+ if (!/^\d+$/.test(trimmed))
15
+ return false;
16
+ value = Number.parseInt(trimmed, 10);
17
+ }
18
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535;
19
+ }
20
+ /**
21
+ * Check if the given string looks like a valid API key.
22
+ * Must start with `sk-` and be at least {@link MIN_KEY_LENGTH} characters long.
23
+ */
24
+ export function isValidApiKey(key) {
25
+ return typeof key === 'string' && key.startsWith(API_KEY_PREFIX) && key.length >= MIN_KEY_LENGTH;
26
+ }
27
+ /**
28
+ * Check if the given value is a valid HTTPS URL.
29
+ * SSRF prevention: only HTTPS URLs are accepted.
30
+ */
31
+ export function isValidHttpsUrl(url) {
32
+ if (typeof url !== 'string')
33
+ return false;
34
+ try {
35
+ const parsed = new URL(url);
36
+ return parsed.protocol === 'https:';
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * Check if the given string is a valid positive integer within an optional range.
44
+ * Trims whitespace before parsing.
45
+ *
46
+ * @param val - The string value to check.
47
+ * @param min - Minimum accepted value (default: 1).
48
+ * @param max - Maximum accepted value (default: 100).
49
+ * @returns `true` if the value is a positive integer in the given range.
50
+ */
51
+ export function isValidPositiveInt(val, min = 1, max = 100) {
52
+ const trimmed = val.trim();
53
+ if (!/^\d+$/.test(trimmed))
54
+ return false;
55
+ const n = Number.parseInt(trimmed, 10);
56
+ return n >= min && n <= max;
57
+ }
58
+ /**
59
+ * Check if the given string is a valid workspace ID (matches `wrk_[A-Za-z0-9]+`).
60
+ */
61
+ export function isValidWorkspaceId(id) {
62
+ return typeof id === 'string' && WORKSPACE_ID_REGEX.test(id);
63
+ }
64
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpF;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QACzC,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,KAAK,CAAC;AAC9F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,cAAc,CAAC;AACnG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG;IAChE,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAW;IAC5C,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "saros-proxy",
3
+ "version": "0.1.0",
4
+ "description": "Saros — predictive multi-account proxy for the OpenCode-Go API. Cycles between accounts before they exhaust.",
5
+ "type": "module",
6
+ "bin": {
7
+ "saros-proxy": "dist/index.js",
8
+ "saros-proxy-setup": "dist/cli/setup.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "setup": "tsx src/cli/setup.ts",
17
+ "prototype": "tsx prototype/prototype.ts",
18
+ "proto:usage": "tsx prototype/usage-switching.ts",
19
+ "proto:probe": "tsx prototype/api-probe.ts",
20
+ "proto:scrape": "tsx prototype/dashboard-scraper.ts",
21
+ "proto:setup": "tsx prototype/setup-wizard.ts",
22
+ "proto:autostart": "tsx prototype/auto-start.ts",
23
+ "dev": "tsx src/index.ts",
24
+ "build": "tsc",
25
+ "lint": "tsc --noEmit",
26
+ "prepublishOnly": "npm run build",
27
+ "test": "vitest",
28
+ "docker:build": "docker compose build proxy",
29
+ "docker:run": "docker compose up proxy",
30
+ "docker:test": "docker compose run --rm test"
31
+ },
32
+ "keywords": [
33
+ "opencode",
34
+ "proxy",
35
+ "multi-account",
36
+ "failover",
37
+ "load-balancer",
38
+ "api-gateway",
39
+ "circuit-breaker",
40
+ "saros",
41
+ "saros-proxy"
42
+ ],
43
+ "author": "Saros Contributors",
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">=22.0.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/vitorvilaca3011/saros-proxy.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/vitorvilaca3011/saros-proxy/issues"
54
+ },
55
+ "homepage": "https://github.com/vitorvilaca3011/saros-proxy#readme",
56
+ "devDependencies": {
57
+ "@types/node": "^25.9.1",
58
+ "@vitest/coverage-v8": "^4.1.8",
59
+ "pino-pretty": "^13.1.3",
60
+ "tsx": "^4.22.4",
61
+ "typescript": "^6.0.3",
62
+ "vitest": "^4.1.8"
63
+ },
64
+ "dependencies": {
65
+ "@clack/prompts": "^1.5.1",
66
+ "@hono/node-server": "^2.0.4",
67
+ "chalk": "^5.6.2",
68
+ "hono": "^4.12.23",
69
+ "hono-rate-limiter": "^0.5.3",
70
+ "pino": "^10.3.1",
71
+ "yaml": "^2.9.0"
72
+ }
73
+ }