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,162 @@
1
+ /**
2
+ * constants.ts — Shared configuration constants for the proxy.
3
+ *
4
+ * All magic numbers and default values used across the codebase
5
+ * are centralized here to avoid duplication and improve maintainability.
6
+ */
7
+ export const DEFAULT_PORT = 3000;
8
+ export const DEFAULT_HOST = '127.0.0.1';
9
+ export const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
10
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
11
+ export const DEFAULT_SCRAPE_INTERVAL_MS = 90_000;
12
+ export const MIN_SCRAPE_INTERVAL_MS = 10_000;
13
+ export const MAX_SCRAPE_INTERVAL_MS = 3_600_000;
14
+ export const MIN_KEY_LENGTH = 20;
15
+ export const API_KEY_PREFIX = 'sk-';
16
+ export const DEFAULT_UPSTREAM_URL = 'https://opencode.ai';
17
+ export const MAX_BODY_SIZE = 10 * 1024 * 1024;
18
+ export const MAX_RETRIES = 2;
19
+ export const RATE_LIMIT_WINDOW_MS = 60_000;
20
+ export const RATE_LIMIT_MAX = 100;
21
+ export const FORCE_SHUTDOWN_TIMEOUT_MS = 5_000;
22
+ export const MAX_STREAMING_BUFFER = 10_000;
23
+ export const DEFAULT_USAGE_THRESHOLD = 50;
24
+ export const WORKSPACE_ID_REGEX = /^wrk_[A-Za-z0-9]+$/;
25
+ // OpenCode client model definitions for proxy provider configuration
26
+ // Source: https://opencode.ai/zen/go/v1/models (18 models as of 2026-06-06)
27
+ export const OPENCODE_MODELS = {
28
+ // GLM series (~200K context)
29
+ 'glm-5': {
30
+ id: 'glm-5',
31
+ name: 'GLM-5',
32
+ tool_call: true,
33
+ limit: { context: 200000, output: 8192 },
34
+ modalities: { input: ['text', 'image'], output: ['text'] },
35
+ },
36
+ 'glm-5.1': {
37
+ id: 'glm-5.1',
38
+ name: 'GLM-5.1',
39
+ tool_call: true,
40
+ limit: { context: 200000, output: 8192 },
41
+ modalities: { input: ['text', 'image'], output: ['text'] },
42
+ },
43
+ // Kimi series (~256K context)
44
+ 'kimi-k2.5': {
45
+ id: 'kimi-k2.5',
46
+ name: 'Kimi K2.5',
47
+ tool_call: true,
48
+ limit: { context: 256000, output: 8192 },
49
+ modalities: { input: ['text', 'image'], output: ['text'] },
50
+ },
51
+ 'kimi-k2.6': {
52
+ id: 'kimi-k2.6',
53
+ name: 'Kimi K2.6',
54
+ tool_call: true,
55
+ limit: { context: 256000, output: 8192 },
56
+ modalities: { input: ['text', 'image'], output: ['text'] },
57
+ },
58
+ // DeepSeek V4 series
59
+ 'deepseek-v4-pro': {
60
+ id: 'deepseek-v4-pro',
61
+ name: 'DeepSeek V4 Pro',
62
+ tool_call: true,
63
+ limit: { context: 128000, output: 8192 },
64
+ modalities: { input: ['text', 'image'], output: ['text'] },
65
+ },
66
+ 'deepseek-v4-flash': {
67
+ id: 'deepseek-v4-flash',
68
+ name: 'DeepSeek V4 Flash',
69
+ tool_call: true,
70
+ limit: { context: 128000, output: 8192 },
71
+ modalities: { input: ['text', 'image'], output: ['text'] },
72
+ },
73
+ // MiMo series (200K context for v2)
74
+ 'mimo-v2.5': {
75
+ id: 'mimo-v2.5',
76
+ name: 'MiMo V2.5',
77
+ tool_call: true,
78
+ limit: { context: 128000, output: 8192 },
79
+ modalities: { input: ['text', 'image'], output: ['text'] },
80
+ },
81
+ 'mimo-v2.5-pro': {
82
+ id: 'mimo-v2.5-pro',
83
+ name: 'MiMo V2.5 Pro',
84
+ tool_call: true,
85
+ limit: { context: 128000, output: 8192 },
86
+ modalities: { input: ['text', 'image'], output: ['text'] },
87
+ },
88
+ 'mimo-v2-pro': {
89
+ id: 'mimo-v2-pro',
90
+ name: 'MiMo V2 Pro',
91
+ tool_call: true,
92
+ limit: { context: 200000, output: 8192 },
93
+ modalities: { input: ['text', 'image'], output: ['text'] },
94
+ },
95
+ 'mimo-v2-omni': {
96
+ id: 'mimo-v2-omni',
97
+ name: 'MiMo V2 Omni',
98
+ tool_call: true,
99
+ limit: { context: 200000, output: 8192 },
100
+ modalities: { input: ['text', 'image'], output: ['text'] },
101
+ },
102
+ // MiniMax series (Anthropic-compatible API)
103
+ 'minimax-m3': {
104
+ id: 'minimax-m3',
105
+ name: 'MiniMax M3',
106
+ tool_call: true,
107
+ limit: { context: 128000, output: 8192 },
108
+ modalities: { input: ['text', 'image'], output: ['text'] },
109
+ },
110
+ 'minimax-m2.7': {
111
+ id: 'minimax-m2.7',
112
+ name: 'MiniMax M2.7',
113
+ tool_call: true,
114
+ limit: { context: 128000, output: 8192 },
115
+ modalities: { input: ['text', 'image'], output: ['text'] },
116
+ },
117
+ 'minimax-m2.5': {
118
+ id: 'minimax-m2.5',
119
+ name: 'MiniMax M2.5',
120
+ tool_call: true,
121
+ limit: { context: 128000, output: 8192 },
122
+ modalities: { input: ['text', 'image'], output: ['text'] },
123
+ },
124
+ // Qwen series (1M context for 3.6/3.5 Plus)
125
+ 'qwen3.7-max': {
126
+ id: 'qwen3.7-max',
127
+ name: 'Qwen 3.7 Max',
128
+ tool_call: true,
129
+ limit: { context: 256000, output: 8192 },
130
+ modalities: { input: ['text', 'image'], output: ['text'] },
131
+ },
132
+ 'qwen3.7-plus': {
133
+ id: 'qwen3.7-plus',
134
+ name: 'Qwen 3.7 Plus',
135
+ tool_call: true,
136
+ limit: { context: 256000, output: 8192 },
137
+ modalities: { input: ['text', 'image'], output: ['text'] },
138
+ },
139
+ 'qwen3.6-plus': {
140
+ id: 'qwen3.6-plus',
141
+ name: 'Qwen 3.6 Plus',
142
+ tool_call: true,
143
+ limit: { context: 1000000, output: 65536 },
144
+ modalities: { input: ['text', 'image'], output: ['text'] },
145
+ },
146
+ 'qwen3.5-plus': {
147
+ id: 'qwen3.5-plus',
148
+ name: 'Qwen 3.5 Plus',
149
+ tool_call: true,
150
+ limit: { context: 1000000, output: 65536 },
151
+ modalities: { input: ['text', 'image'], output: ['text'] },
152
+ },
153
+ // HY3 Preview
154
+ 'hy3-preview': {
155
+ id: 'hy3-preview',
156
+ name: 'HY3 Preview',
157
+ tool_call: false,
158
+ limit: { context: 128000, output: 8192 },
159
+ modalities: { input: ['text'], output: ['text'] },
160
+ },
161
+ };
162
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * dashboard-parser.ts — Parse usage percentages from the OpenCode-Go dashboard HTML.
3
+ *
4
+ * The dashboard returns SolidJS SSR hydration data with usage percentages
5
+ * for three time windows: rolling (~5h), weekly, and monthly.
6
+ *
7
+ * Example hydration data:
8
+ * ```
9
+ * rollingUsage:$R[3]={usagePercent:45.2,resetInSec:18000}
10
+ * weeklyUsage:$R[4]={usagePercent:60.5,resetInSec:604800}
11
+ * monthlyUsage:$R[5]={usagePercent:30.0,resetInSec:2592000}
12
+ * ```
13
+ *
14
+ * Field order (usagePercent vs resetInSec) can vary between responses.
15
+ * These functions handle both orderings by trying two regex patterns per window.
16
+ */
17
+ export interface UsageData {
18
+ rolling: number | null;
19
+ weekly: number | null;
20
+ monthly: number | null;
21
+ }
22
+ /**
23
+ * Parse all three usage windows (rolling, weekly, monthly) from dashboard HTML.
24
+ * Returns null for any window that couldn't be parsed.
25
+ *
26
+ * @param html — Raw HTML from the OpenCode-Go dashboard page.
27
+ * @returns An object with `rolling`, `weekly`, and `monthly` usage percentages,
28
+ * or null for windows that could not be parsed.
29
+ */
30
+ export declare function parseAllUsage(html: string): UsageData;
31
+ /**
32
+ * Parse a single usage window from dashboard HTML.
33
+ *
34
+ * Tries two regex patterns to handle field order variance
35
+ * (usagePercent before or after resetInSec).
36
+ *
37
+ * Returns null if:
38
+ * - The window data is not found in the HTML
39
+ * - The extracted value is not a finite number
40
+ * - The HTML is malformed or from a non-dashboard page
41
+ *
42
+ * @param html — Raw HTML from the OpenCode-Go dashboard page.
43
+ * @param window — The time window to parse: `'rolling'`, `'weekly'`, or `'monthly'`.
44
+ * @returns The usage percentage as a number (may be negative or exceed 100),
45
+ * or null if the value could not be extracted.
46
+ */
47
+ export declare function parseUsagePercent(html: string, window: 'rolling' | 'weekly' | 'monthly'): number | null;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * dashboard-parser.ts — Parse usage percentages from the OpenCode-Go dashboard HTML.
3
+ *
4
+ * The dashboard returns SolidJS SSR hydration data with usage percentages
5
+ * for three time windows: rolling (~5h), weekly, and monthly.
6
+ *
7
+ * Example hydration data:
8
+ * ```
9
+ * rollingUsage:$R[3]={usagePercent:45.2,resetInSec:18000}
10
+ * weeklyUsage:$R[4]={usagePercent:60.5,resetInSec:604800}
11
+ * monthlyUsage:$R[5]={usagePercent:30.0,resetInSec:2592000}
12
+ * ```
13
+ *
14
+ * Field order (usagePercent vs resetInSec) can vary between responses.
15
+ * These functions handle both orderings by trying two regex patterns per window.
16
+ */
17
+ // ---------------------------------------------------------------------------
18
+ // Regex patterns
19
+ // ---------------------------------------------------------------------------
20
+ const SCRAPED_NUMBER_PATTERN = String.raw `(-?\d+(?:\.\d+)?)`;
21
+ const RE_ROLLING_PCT_FIRST = new RegExp(String.raw `rollingUsage:\$R\[\d+\]=\{[^}]*usagePercent:${SCRAPED_NUMBER_PATTERN}[^}]*resetInSec:${SCRAPED_NUMBER_PATTERN}[^}]*\}`);
22
+ const RE_ROLLING_RESET_FIRST = new RegExp(String.raw `rollingUsage:\$R\[\d+\]=\{[^}]*resetInSec:${SCRAPED_NUMBER_PATTERN}[^}]*usagePercent:${SCRAPED_NUMBER_PATTERN}[^}]*\}`);
23
+ const RE_WEEKLY_PCT_FIRST = new RegExp(String.raw `weeklyUsage:\$R\[\d+\]=\{[^}]*usagePercent:${SCRAPED_NUMBER_PATTERN}[^}]*resetInSec:${SCRAPED_NUMBER_PATTERN}[^}]*\}`);
24
+ const RE_WEEKLY_RESET_FIRST = new RegExp(String.raw `weeklyUsage:\$R\[\d+\]=\{[^}]*resetInSec:${SCRAPED_NUMBER_PATTERN}[^}]*usagePercent:${SCRAPED_NUMBER_PATTERN}[^}]*\}`);
25
+ const RE_MONTHLY_PCT_FIRST = new RegExp(String.raw `monthlyUsage:\$R\[\d+\]=\{[^}]*usagePercent:${SCRAPED_NUMBER_PATTERN}[^}]*resetInSec:${SCRAPED_NUMBER_PATTERN}[^}]*\}`);
26
+ const RE_MONTHLY_RESET_FIRST = new RegExp(String.raw `monthlyUsage:\$R\[\d+\]=\{[^}]*resetInSec:${SCRAPED_NUMBER_PATTERN}[^}]*usagePercent:${SCRAPED_NUMBER_PATTERN}[^}]*\}`);
27
+ // Map from window name to its two regex alternatives
28
+ const PATTERNS = {
29
+ rolling: [RE_ROLLING_PCT_FIRST, RE_ROLLING_RESET_FIRST],
30
+ weekly: [RE_WEEKLY_PCT_FIRST, RE_WEEKLY_RESET_FIRST],
31
+ monthly: [RE_MONTHLY_PCT_FIRST, RE_MONTHLY_RESET_FIRST],
32
+ };
33
+ // ---------------------------------------------------------------------------
34
+ // Public API
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Parse all three usage windows (rolling, weekly, monthly) from dashboard HTML.
38
+ * Returns null for any window that couldn't be parsed.
39
+ *
40
+ * @param html — Raw HTML from the OpenCode-Go dashboard page.
41
+ * @returns An object with `rolling`, `weekly`, and `monthly` usage percentages,
42
+ * or null for windows that could not be parsed.
43
+ */
44
+ export function parseAllUsage(html) {
45
+ return {
46
+ rolling: parseUsagePercent(html, 'rolling'),
47
+ weekly: parseUsagePercent(html, 'weekly'),
48
+ monthly: parseUsagePercent(html, 'monthly'),
49
+ };
50
+ }
51
+ /**
52
+ * Parse a single usage window from dashboard HTML.
53
+ *
54
+ * Tries two regex patterns to handle field order variance
55
+ * (usagePercent before or after resetInSec).
56
+ *
57
+ * Returns null if:
58
+ * - The window data is not found in the HTML
59
+ * - The extracted value is not a finite number
60
+ * - The HTML is malformed or from a non-dashboard page
61
+ *
62
+ * @param html — Raw HTML from the OpenCode-Go dashboard page.
63
+ * @param window — The time window to parse: `'rolling'`, `'weekly'`, or `'monthly'`.
64
+ * @returns The usage percentage as a number (may be negative or exceed 100),
65
+ * or null if the value could not be extracted.
66
+ */
67
+ export function parseUsagePercent(html, window) {
68
+ const patterns = PATTERNS[window];
69
+ if (!patterns)
70
+ return null;
71
+ const [rePctFirst, reResetFirst] = patterns;
72
+ // Try usagePercent-first pattern
73
+ const pctMatch = rePctFirst.exec(html);
74
+ if (pctMatch) {
75
+ // pct-first: usagePercent:$1,resetInSec:$2
76
+ const val = Number(pctMatch[1]);
77
+ return Number.isFinite(val) ? val : null;
78
+ }
79
+ // Try resetInSec-first pattern
80
+ const resetMatch = reResetFirst.exec(html);
81
+ if (resetMatch) {
82
+ // reset-first: resetInSec:$1,usagePercent:$2
83
+ const val = Number(resetMatch[2]);
84
+ return Number.isFinite(val) ? val : null;
85
+ }
86
+ return null;
87
+ }
88
+ //# sourceMappingURL=dashboard-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-parser.js","sourceRoot":"","sources":["../src/dashboard-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,MAAM,sBAAsB,GAAG,MAAM,CAAC,GAAG,CAAA,mBAAmB,CAAC;AAE7D,MAAM,oBAAoB,GAAG,IAAI,MAAM,CACrC,MAAM,CAAC,GAAG,CAAA,+CAA+C,sBAAsB,mBAAmB,sBAAsB,SAAS,CAClI,CAAC;AACF,MAAM,sBAAsB,GAAG,IAAI,MAAM,CACvC,MAAM,CAAC,GAAG,CAAA,6CAA6C,sBAAsB,qBAAqB,sBAAsB,SAAS,CAClI,CAAC;AACF,MAAM,mBAAmB,GAAG,IAAI,MAAM,CACpC,MAAM,CAAC,GAAG,CAAA,8CAA8C,sBAAsB,mBAAmB,sBAAsB,SAAS,CACjI,CAAC;AACF,MAAM,qBAAqB,GAAG,IAAI,MAAM,CACtC,MAAM,CAAC,GAAG,CAAA,4CAA4C,sBAAsB,qBAAqB,sBAAsB,SAAS,CACjI,CAAC;AACF,MAAM,oBAAoB,GAAG,IAAI,MAAM,CACrC,MAAM,CAAC,GAAG,CAAA,+CAA+C,sBAAsB,mBAAmB,sBAAsB,SAAS,CAClI,CAAC;AACF,MAAM,sBAAsB,GAAG,IAAI,MAAM,CACvC,MAAM,CAAC,GAAG,CAAA,6CAA6C,sBAAsB,qBAAqB,sBAAsB,SAAS,CAClI,CAAC;AAEF,qDAAqD;AACrD,MAAM,QAAQ,GAAqC;IACjD,OAAO,EAAE,CAAC,oBAAoB,EAAE,sBAAsB,CAAC;IACvD,MAAM,EAAE,CAAC,mBAAmB,EAAE,qBAAqB,CAAC;IACpD,OAAO,EAAE,CAAC,oBAAoB,EAAE,sBAAsB,CAAC;CACxD,CAAC;AAEF,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO;QACL,OAAO,EAAE,iBAAiB,CAAC,IAAI,EAAE,SAAS,CAAC;QAC3C,MAAM,EAAE,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC;QACzC,OAAO,EAAE,iBAAiB,CAAC,IAAI,EAAE,SAAS,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAwC;IACtF,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAE3B,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC;IAE5C,iCAAiC;IACjC,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,QAAQ,EAAE,CAAC;QACb,2CAA2C;QAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3C,CAAC;IAED,+BAA+B;IAC/B,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,UAAU,EAAE,CAAC;QACf,6CAA6C;QAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3C,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * firefox-cookies.ts — Firefox cookie and history extraction.
3
+ *
4
+ * Reads cookies.sqlite and places.sqlite from the default Firefox profile.
5
+ * Uses Node.js built-in node:sqlite (requires Node >= 22).
6
+ */
7
+ /**
8
+ * Find the default Firefox profile directory on this system.
9
+ *
10
+ * Scans the platform-specific Firefox profiles directory and prefers the
11
+ * `default-release` profile if it exists, otherwise returns the first
12
+ * profile directory found.
13
+ *
14
+ * @returns The path to the Firefox profile directory, or `null` if not found.
15
+ */
16
+ export declare function findFirefoxProfileDir(): string | null;
17
+ /**
18
+ * Extract the `auth` cookie value for `opencode.ai` from Firefox's cookies.sqlite.
19
+ *
20
+ * Makes a temporary copy of the database (including WAL/SHM sidecars) to avoid
21
+ * locking conflicts with Firefox. The temp copy is cleaned up after extraction.
22
+ *
23
+ * @returns An object with `cookie` (the auth cookie value, or `null` if not found)
24
+ * and an optional `error` message describing what went wrong.
25
+ */
26
+ export declare function extractFirefoxAuthCookie(): {
27
+ cookie: string | null;
28
+ error?: string;
29
+ };
30
+ /**
31
+ * Extract OpenCode workspace IDs from Firefox's places.sqlite (browser history).
32
+ *
33
+ * Searches `moz_places` for URLs containing `opencode.ai/workspace/` and
34
+ * returns unique workspace IDs (wrk_...) from the most recently visited pages.
35
+ *
36
+ * A temporary copy of the database (including WAL/SHM sidecars) is used to
37
+ * avoid locking conflicts with Firefox, then cleaned up after extraction.
38
+ *
39
+ * @returns An array of unique workspace ID strings (e.g. `["wrk_abc123"]`).
40
+ * Returns an empty array if none are found or if an error occurs.
41
+ */
42
+ export declare function extractFirefoxWorkspaceIds(): string[];
@@ -0,0 +1,170 @@
1
+ /**
2
+ * firefox-cookies.ts — Firefox cookie and history extraction.
3
+ *
4
+ * Reads cookies.sqlite and places.sqlite from the default Firefox profile.
5
+ * Uses Node.js built-in node:sqlite (requires Node >= 22).
6
+ */
7
+ import { existsSync, copyFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { homedir, tmpdir } from 'node:os';
10
+ import { DatabaseSync } from 'node:sqlite';
11
+ import { logger } from './logger.js';
12
+ /**
13
+ * Find the default Firefox profile directory on this system.
14
+ *
15
+ * Scans the platform-specific Firefox profiles directory and prefers the
16
+ * `default-release` profile if it exists, otherwise returns the first
17
+ * profile directory found.
18
+ *
19
+ * @returns The path to the Firefox profile directory, or `null` if not found.
20
+ */
21
+ export function findFirefoxProfileDir() {
22
+ const platform = process.platform;
23
+ let root;
24
+ if (platform === 'win32') {
25
+ const appData = process.env.APPDATA;
26
+ if (!appData)
27
+ return null;
28
+ root = join(appData, 'Mozilla', 'Firefox', 'Profiles');
29
+ }
30
+ else if (platform === 'darwin') {
31
+ root = join(homedir(), 'Library', 'Application Support', 'Firefox', 'Profiles');
32
+ }
33
+ else if (platform === 'linux') {
34
+ root = join(homedir(), '.mozilla', 'firefox');
35
+ }
36
+ else {
37
+ return null;
38
+ }
39
+ if (!existsSync(root))
40
+ return null;
41
+ try {
42
+ const profiles = readdirSync(root, { withFileTypes: true })
43
+ .filter(e => e.isDirectory())
44
+ .map(e => e.name);
45
+ if (profiles.length === 0)
46
+ return null;
47
+ const preferred = profiles.find(p => p.includes('default-release')) ?? profiles[0];
48
+ return join(root, preferred);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * Extract the `auth` cookie value for `opencode.ai` from Firefox's cookies.sqlite.
56
+ *
57
+ * Makes a temporary copy of the database (including WAL/SHM sidecars) to avoid
58
+ * locking conflicts with Firefox. The temp copy is cleaned up after extraction.
59
+ *
60
+ * @returns An object with `cookie` (the auth cookie value, or `null` if not found)
61
+ * and an optional `error` message describing what went wrong.
62
+ */
63
+ export function extractFirefoxAuthCookie() {
64
+ const profileDir = findFirefoxProfileDir();
65
+ if (!profileDir)
66
+ return { cookie: null, error: 'Firefox profile not found' };
67
+ const cookiesDb = join(profileDir, 'cookies.sqlite');
68
+ if (!existsSync(cookiesDb))
69
+ return { cookie: null, error: 'cookies.sqlite not found' };
70
+ const tempDir = mkdirSync(join(tmpdir(), 'proto-setup-' + Date.now()), { recursive: true });
71
+ const tempDb = join(tempDir, 'cookies.sqlite');
72
+ try {
73
+ copyFileSync(cookiesDb, tempDb);
74
+ for (const ext of ['-wal', '-shm']) {
75
+ const sidecar = cookiesDb + ext;
76
+ if (existsSync(sidecar)) {
77
+ copyFileSync(sidecar, tempDb + ext);
78
+ }
79
+ }
80
+ const db = new DatabaseSync(tempDb, { readOnly: true });
81
+ try {
82
+ const stmt = db.prepare(`
83
+ SELECT name, value, host
84
+ FROM moz_cookies
85
+ WHERE (host = 'opencode.ai' OR host = '.opencode.ai')
86
+ AND name = 'auth'
87
+ ORDER BY expiry DESC
88
+ LIMIT 1
89
+ `);
90
+ const rows = stmt.all();
91
+ return rows.length > 0
92
+ ? { cookie: rows[0].value }
93
+ : { cookie: null, error: 'No auth cookie found for opencode.ai' };
94
+ }
95
+ finally {
96
+ db.close();
97
+ }
98
+ }
99
+ catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ if (msg.includes('EBUSY') || msg.includes('EPERM') || msg.includes('locked')) {
102
+ return { cookie: null, error: 'Firefox has the database locked. Close Firefox and try again.' };
103
+ }
104
+ return { cookie: null, error: msg };
105
+ }
106
+ finally {
107
+ rmSync(tempDir, { recursive: true, force: true });
108
+ }
109
+ }
110
+ /**
111
+ * Extract OpenCode workspace IDs from Firefox's places.sqlite (browser history).
112
+ *
113
+ * Searches `moz_places` for URLs containing `opencode.ai/workspace/` and
114
+ * returns unique workspace IDs (wrk_...) from the most recently visited pages.
115
+ *
116
+ * A temporary copy of the database (including WAL/SHM sidecars) is used to
117
+ * avoid locking conflicts with Firefox, then cleaned up after extraction.
118
+ *
119
+ * @returns An array of unique workspace ID strings (e.g. `["wrk_abc123"]`).
120
+ * Returns an empty array if none are found or if an error occurs.
121
+ */
122
+ export function extractFirefoxWorkspaceIds() {
123
+ const profileDir = findFirefoxProfileDir();
124
+ if (!profileDir)
125
+ return [];
126
+ const placesDb = join(profileDir, 'places.sqlite');
127
+ if (!existsSync(placesDb))
128
+ return [];
129
+ const tempDir = mkdirSync(join(tmpdir(), 'proto-setup-history-' + Date.now()), { recursive: true });
130
+ const tempDb = join(tempDir, 'places.sqlite');
131
+ try {
132
+ copyFileSync(placesDb, tempDb);
133
+ for (const ext of ['-wal', '-shm']) {
134
+ const sidecar = placesDb + ext;
135
+ if (existsSync(sidecar)) {
136
+ copyFileSync(sidecar, tempDb + ext);
137
+ }
138
+ }
139
+ const db = new DatabaseSync(tempDb, { readOnly: true });
140
+ try {
141
+ const stmt = db.prepare(`
142
+ SELECT url
143
+ FROM moz_places
144
+ WHERE url LIKE '%opencode.ai/workspace/%'
145
+ ORDER BY last_visit_date DESC
146
+ LIMIT 50
147
+ `);
148
+ const rows = stmt.all();
149
+ const workspaceIds = new Set();
150
+ for (const row of rows) {
151
+ const match = row.url.match(/\/workspace\/(wrk_[A-Za-z0-9]+)/);
152
+ if (match) {
153
+ workspaceIds.add(match[1]);
154
+ }
155
+ }
156
+ return Array.from(workspaceIds);
157
+ }
158
+ finally {
159
+ db.close();
160
+ }
161
+ }
162
+ catch (err) {
163
+ logger.warn({ err }, 'Failed to extract Firefox workspace IDs');
164
+ return [];
165
+ }
166
+ finally {
167
+ rmSync(tempDir, { recursive: true, force: true });
168
+ }
169
+ }
170
+ //# sourceMappingURL=firefox-cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firefox-cookies.js","sourceRoot":"","sources":["../src/firefox-cookies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * index.ts — Entry point for Saros.
4
+ *
5
+ * Usage:
6
+ * tsx src/index.ts
7
+ * tsx src/index.ts --config my-config.yaml
8
+ * tsx src/index.ts --port 4000 --host 0.0.0.0
9
+ * tsx src/index.ts setup # Run the interactive setup wizard
10
+ */
11
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * index.ts — Entry point for Saros.
4
+ *
5
+ * Usage:
6
+ * tsx src/index.ts
7
+ * tsx src/index.ts --config my-config.yaml
8
+ * tsx src/index.ts --port 4000 --host 0.0.0.0
9
+ * tsx src/index.ts setup # Run the interactive setup wizard
10
+ */
11
+ import { serve } from '@hono/node-server';
12
+ import { dirname } from 'node:path';
13
+ import { loadConfig } from './config.js';
14
+ import { createProxyApp } from './proxy.js';
15
+ import { logger, maskKey } from './logger.js';
16
+ import { startScraper, stopScraper } from './scraper.js';
17
+ import { FORCE_SHUTDOWN_TIMEOUT_MS } from './constants.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Subcommand: 'setup' — delegate to the interactive setup wizard
20
+ // ---------------------------------------------------------------------------
21
+ if (process.argv[2] === 'setup') {
22
+ const { setup } = await import('./cli/setup.js');
23
+ const { getDefaultConfigPath } = await import('./config.js');
24
+ await setup(dirname(getDefaultConfigPath()));
25
+ process.exit(0);
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Configuration
29
+ // ---------------------------------------------------------------------------
30
+ let config;
31
+ try {
32
+ config = loadConfig();
33
+ }
34
+ catch (err) {
35
+ logger.error({ err }, 'Failed to load configuration');
36
+ process.exit(1);
37
+ }
38
+ logger.info('Loaded %d API key(s):', config.keys.length);
39
+ for (const k of config.keys) {
40
+ logger.info(' - %s: %s', k.label, maskKey(k.key));
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Security startup banner
44
+ // ---------------------------------------------------------------------------
45
+ function logSecurityWarnings(cfg) {
46
+ // Not binding to localhost — warn about exposure
47
+ if (cfg.host !== '127.0.0.1' && cfg.host !== 'localhost') {
48
+ logger.warn('Proxy binding to "%s" — not localhost. Ensure firewall rules are set.', cfg.host);
49
+ }
50
+ // Unusually long request timeout
51
+ if (cfg.requestTimeoutMs > 60_000) {
52
+ logger.warn('Request timeout is %dms (>60s) — may cause resource exhaustion', cfg.requestTimeoutMs);
53
+ }
54
+ // CORS allowing all origins (empty allowedOrigins means all are allowed)
55
+ if (cfg.allowedOrigins.length === 0) {
56
+ logger.warn('CORS allows all origins — restrict in production');
57
+ }
58
+ logger.info('Security: %d API keys loaded (all masked in logs)', cfg.keys.length);
59
+ }
60
+ logSecurityWarnings(config);
61
+ // ---------------------------------------------------------------------------
62
+ // Server
63
+ // ---------------------------------------------------------------------------
64
+ const app = createProxyApp(config);
65
+ const server = serve({ fetch: app.fetch, port: config.port, hostname: config.host }, (info) => {
66
+ logger.info('Proxy listening on http://%s:%d', info.address, info.port);
67
+ logger.info('Upstream: %s', config.upstreamBaseUrl);
68
+ logger.info('API keys loaded: %d', config.keys.length);
69
+ // Start usage-based scraper if configured
70
+ if (config.scraping?.enabled && config.scraping.accounts.length > 0) {
71
+ startScraper(config.scraping.accounts, config.scraping.intervalMs);
72
+ }
73
+ else {
74
+ logger.info('Usage-based scraping disabled (not configured or no accounts)');
75
+ }
76
+ });
77
+ // ---------------------------------------------------------------------------
78
+ // Graceful shutdown
79
+ // ---------------------------------------------------------------------------
80
+ let isShuttingDown = false;
81
+ function gracefulShutdown(signal) {
82
+ if (isShuttingDown)
83
+ return;
84
+ isShuttingDown = true;
85
+ logger.info('Received %s, starting graceful shutdown...', signal);
86
+ // Stop background scraper
87
+ stopScraper();
88
+ // Stop accepting new connections
89
+ server.close(() => {
90
+ logger.info('Server closed');
91
+ process.exit(0);
92
+ });
93
+ // Force exit after forced shutdown timeout
94
+ setTimeout(() => {
95
+ logger.error('Forced shutdown after timeout');
96
+ process.exit(1);
97
+ }, FORCE_SHUTDOWN_TIMEOUT_MS);
98
+ }
99
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
100
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}