@zhafron/opencode-iflow-auth 1.0.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/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # OpenCode iFlow Auth Plugin
2
+ [![npm version](https://img.shields.io/npm/v/@zhafron/opencode-iflow-auth)](https://www.npmjs.com/package/@zhafron/opencode-iflow-auth)
3
+ [![npm downloads](https://img.shields.io/npm/dm/@zhafron/opencode-iflow-auth)](https://www.npmjs.com/package/@zhafron/opencode-iflow-auth)
4
+ [![license](https://img.shields.io/npm/l/@zhafron/opencode-iflow-auth)](https://www.npmjs.com/package/@zhafron/opencode-iflow-auth)
5
+
6
+ OpenCode plugin for iFlow.cn providing access to Qwen, DeepSeek, Kimi, GLM, and iFlow ROME models with dual authentication support.
7
+
8
+ ## Features
9
+
10
+ - Dual authentication: OAuth 2.0 (PKCE) and API Key support.
11
+ - Multi-account rotation with sticky and round-robin strategies.
12
+ - Automated token refresh and rate limit handling with exponential backoff.
13
+ - Native thinking mode support for GLM-4.x models.
14
+ - Configurable request timeout and iteration limits to prevent hangs.
15
+ - Automatic port selection for OAuth callback server to avoid conflicts.
16
+
17
+ ## Installation
18
+
19
+ Add the plugin to your `opencode.json` or `opencode.jsonc`:
20
+
21
+ ```json
22
+ {
23
+ "plugin": ["@zhafron/opencode-iflow-auth"],
24
+ "provider": {
25
+ "iflow": {
26
+ "models": {
27
+ "iflow-rome-30ba3b": {
28
+ "name": "iFlow ROME 30B",
29
+ "limit": { "context": 256000, "output": 64000 },
30
+ "modalities": { "input": ["text"], "output": ["text"] }
31
+ },
32
+ "qwen3-coder-plus": {
33
+ "name": "Qwen3 Coder Plus",
34
+ "limit": { "context": 1000000, "output": 64000 },
35
+ "modalities": { "input": ["text"], "output": ["text"] }
36
+ },
37
+ "qwen3-max": {
38
+ "name": "Qwen3 Max",
39
+ "limit": { "context": 256000, "output": 32000 },
40
+ "modalities": { "input": ["text"], "output": ["text"] }
41
+ },
42
+ "qwen3-vl-plus": {
43
+ "name": "Qwen3 VL Plus",
44
+ "limit": { "context": 256000, "output": 32000 },
45
+ "modalities": { "input": ["text", "image"], "output": ["text"] }
46
+ },
47
+ "qwen3-235b-a22b-thinking-2507": {
48
+ "name": "Qwen3 235B Thinking",
49
+ "limit": { "context": 256000, "output": 64000 },
50
+ "modalities": { "input": ["text"], "output": ["text"] }
51
+ },
52
+ "kimi-k2": {
53
+ "name": "Kimi K2",
54
+ "limit": { "context": 128000, "output": 64000 },
55
+ "modalities": { "input": ["text"], "output": ["text"] }
56
+ },
57
+ "kimi-k2-0905": {
58
+ "name": "Kimi K2 0905",
59
+ "limit": { "context": 256000, "output": 64000 },
60
+ "modalities": { "input": ["text"], "output": ["text"] }
61
+ },
62
+ "glm-4.6": {
63
+ "name": "GLM-4.6 Thinking",
64
+ "limit": { "context": 200000, "output": 128000 },
65
+ "modalities": { "input": ["text", "image"], "output": ["text"] },
66
+ "variants": {
67
+ "low": { "thinkingConfig": { "thinkingBudget": 1024 } },
68
+ "medium": { "thinkingConfig": { "thinkingBudget": 8192 } },
69
+ "max": { "thinkingConfig": { "thinkingBudget": 32768 } }
70
+ }
71
+ },
72
+ "deepseek-v3": {
73
+ "name": "DeepSeek V3",
74
+ "limit": { "context": 128000, "output": 32000 },
75
+ "modalities": { "input": ["text"], "output": ["text"] }
76
+ },
77
+ "deepseek-v3.2": {
78
+ "name": "DeepSeek V3.2",
79
+ "limit": { "context": 128000, "output": 64000 },
80
+ "modalities": { "input": ["text"], "output": ["text"] }
81
+ },
82
+ "deepseek-r1": {
83
+ "name": "DeepSeek R1",
84
+ "limit": { "context": 128000, "output": 32000 },
85
+ "modalities": { "input": ["text"], "output": ["text"] },
86
+ "variants": {
87
+ "low": { "thinkingConfig": { "thinkingBudget": 1024 } },
88
+ "medium": { "thinkingConfig": { "thinkingBudget": 8192 } },
89
+ "max": { "thinkingConfig": { "thinkingBudget": 32768 } }
90
+ }
91
+ },
92
+ "qwen3-32b": {
93
+ "name": "Qwen3 32B",
94
+ "limit": { "context": 128000, "output": 32000 },
95
+ "modalities": { "input": ["text"], "output": ["text"] }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Setup
104
+
105
+ 1. Run `opencode auth login`.
106
+ 2. Select `Other`, type `iflow`, and press enter.
107
+ 3. Choose authentication method:
108
+ - **OAuth 2.0**: Follow browser flow for secure token-based authentication.
109
+ - **API Key**: Enter your iFlow API key (starts with `sk-`).
110
+ 4. Configuration template will be automatically created at `~/.config/opencode/iflow.json` on first load.
111
+
112
+ ## Configuration
113
+
114
+ The plugin supports extensive configuration options. Edit `~/.config/opencode/iflow.json`:
115
+
116
+ ```json
117
+ {
118
+ "default_auth_method": "oauth",
119
+ "account_selection_strategy": "round-robin",
120
+ "auth_server_port_start": 8087,
121
+ "auth_server_port_range": 10,
122
+ "max_request_iterations": 50,
123
+ "request_timeout_ms": 300000,
124
+ "enable_log_api_request": false
125
+ }
126
+ ```
127
+
128
+ ### Configuration Options
129
+
130
+ - `default_auth_method`: Default authentication method (`oauth`, `apikey`)
131
+ - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`)
132
+ - `auth_server_port_start`: Starting port for OAuth callback server (1024-65535)
133
+ - `auth_server_port_range`: Number of ports to try (1-100)
134
+ - `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000)
135
+ - `request_timeout_ms`: Request timeout in milliseconds (60000-600000ms)
136
+ - `enable_log_api_request`: Enable API request/response logging (errors always logged)
137
+
138
+ ### Environment Variables
139
+
140
+ All configuration options can be overridden via environment variables:
141
+
142
+ - `IFLOW_DEFAULT_AUTH_METHOD`
143
+ - `IFLOW_ACCOUNT_SELECTION_STRATEGY`
144
+ - `IFLOW_AUTH_SERVER_PORT_START`
145
+ - `IFLOW_AUTH_SERVER_PORT_RANGE`
146
+ - `IFLOW_MAX_REQUEST_ITERATIONS`
147
+ - `IFLOW_REQUEST_TIMEOUT_MS`
148
+ - `IFLOW_ENABLE_LOG_API_REQUEST`
149
+
150
+ ## Storage
151
+
152
+ **Linux/macOS:**
153
+ - Credentials: `~/.config/opencode/iflow-accounts.json`
154
+ - Plugin Config: `~/.config/opencode/iflow.json`
155
+
156
+ **Windows:**
157
+ - Credentials: `%APPDATA%\opencode\iflow-accounts.json`
158
+ - Plugin Config: `%APPDATA%\opencode\iflow.json`
159
+
160
+ ## Thinking Models
161
+
162
+ iFlow supports thinking models with customizable thinking budgets via variants:
163
+
164
+ ### GLM-4.6
165
+
166
+ Automatically enables thinking mode with configurable budget:
167
+
168
+ ```json
169
+ {
170
+ "variants": {
171
+ "low": { "thinkingConfig": { "thinkingBudget": 1024 } },
172
+ "medium": { "thinkingConfig": { "thinkingBudget": 8192 } },
173
+ "max": { "thinkingConfig": { "thinkingBudget": 32768 } }
174
+ }
175
+ }
176
+ ```
177
+
178
+ The plugin automatically transforms requests to:
179
+
180
+ ```typescript
181
+ {
182
+ "chat_template_kwargs": {
183
+ "enable_thinking": true,
184
+ "clear_thinking": false
185
+ },
186
+ "thinking_budget": 8192 // from variant config
187
+ }
188
+ ```
189
+
190
+ ### DeepSeek R1
191
+
192
+ Supports thinking budget control via variants:
193
+
194
+ ```json
195
+ {
196
+ "variants": {
197
+ "low": { "thinkingConfig": { "thinkingBudget": 1024 } },
198
+ "medium": { "thinkingConfig": { "thinkingBudget": 8192 } },
199
+ "max": { "thinkingConfig": { "thinkingBudget": 32768 } }
200
+ }
201
+ }
202
+ ```
203
+
204
+ The plugin automatically adds `thinking_budget` parameter to requests.
205
+
206
+ ### Response Format
207
+
208
+ Both models return reasoning content in the response:
209
+
210
+ ```json
211
+ {
212
+ "choices": [{
213
+ "message": {
214
+ "role": "assistant",
215
+ "content": "The answer is 4.",
216
+ "reasoning_content": "Let me think step by step..."
217
+ }
218
+ }],
219
+ "usage": {
220
+ "completion_tokens_details": {
221
+ "reasoning_tokens": 1094
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Disclaimer
228
+
229
+ This plugin is provided strictly for learning and educational purposes. It is an independent implementation and is not affiliated with, endorsed by, or supported by iFlow.cn. Use of this plugin is at your own risk.
230
+
231
+ Feel free to open a PR to optimize this plugin further.
@@ -0,0 +1,19 @@
1
+ export type IFlowAuthMethod = 'oauth' | 'apikey';
2
+ export declare function isValidAuthMethod(method: string): method is IFlowAuthMethod;
3
+ export declare const IFLOW_CONSTANTS: {
4
+ BASE_URL: string;
5
+ OAUTH_TOKEN_URL: string;
6
+ OAUTH_AUTHORIZE_URL: string;
7
+ USER_INFO_URL: string;
8
+ SUCCESS_REDIRECT: string;
9
+ CLIENT_ID: string;
10
+ CLIENT_SECRET: string;
11
+ AXIOS_TIMEOUT: number;
12
+ USER_AGENT: string;
13
+ CALLBACK_PORT_START: number;
14
+ CALLBACK_PORT_RANGE: number;
15
+ };
16
+ export declare const SUPPORTED_MODELS: string[];
17
+ export declare const THINKING_MODELS: string[];
18
+ export declare function isThinkingModel(model: string): boolean;
19
+ export declare function applyThinkingConfig(body: any, model: string): any;
@@ -0,0 +1,61 @@
1
+ export function isValidAuthMethod(method) {
2
+ return method === 'oauth' || method === 'apikey';
3
+ }
4
+ export const IFLOW_CONSTANTS = {
5
+ BASE_URL: 'https://apis.iflow.cn/v1',
6
+ OAUTH_TOKEN_URL: 'https://iflow.cn/oauth/token',
7
+ OAUTH_AUTHORIZE_URL: 'https://iflow.cn/oauth',
8
+ USER_INFO_URL: 'https://iflow.cn/api/oauth/getUserInfo',
9
+ SUCCESS_REDIRECT: 'https://iflow.cn/oauth/success',
10
+ CLIENT_ID: '10009311001',
11
+ CLIENT_SECRET: '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW',
12
+ AXIOS_TIMEOUT: 120000,
13
+ USER_AGENT: 'OpenCode-iFlow',
14
+ CALLBACK_PORT_START: 8087,
15
+ CALLBACK_PORT_RANGE: 10
16
+ };
17
+ export const SUPPORTED_MODELS = [
18
+ 'iflow-rome-30ba3b',
19
+ 'qwen3-coder-plus',
20
+ 'qwen3-max',
21
+ 'qwen3-vl-plus',
22
+ 'qwen3-max-preview',
23
+ 'qwen3-32b',
24
+ 'qwen3-235b-a22b-thinking-2507',
25
+ 'qwen3-235b-a22b-instruct',
26
+ 'qwen3-235b',
27
+ 'kimi-k2-0905',
28
+ 'kimi-k2',
29
+ 'glm-4.6',
30
+ 'deepseek-v3.2',
31
+ 'deepseek-r1',
32
+ 'deepseek-v3'
33
+ ];
34
+ export const THINKING_MODELS = ['glm-4.6', 'qwen3-235b-a22b-thinking-2507', 'deepseek-r1'];
35
+ export function isThinkingModel(model) {
36
+ return THINKING_MODELS.some((m) => model.startsWith(m));
37
+ }
38
+ export function applyThinkingConfig(body, model) {
39
+ const thinkingBudget = body.providerOptions?.thinkingConfig?.thinkingBudget;
40
+ if (model.startsWith('glm-4')) {
41
+ const result = {
42
+ ...body,
43
+ chat_template_kwargs: {
44
+ enable_thinking: true,
45
+ clear_thinking: false
46
+ }
47
+ };
48
+ if (thinkingBudget) {
49
+ result.thinking_budget = thinkingBudget;
50
+ }
51
+ return result;
52
+ }
53
+ if (model.startsWith('deepseek-r1')) {
54
+ const result = { ...body };
55
+ if (thinkingBudget) {
56
+ result.thinking_budget = thinkingBudget;
57
+ }
58
+ return result;
59
+ }
60
+ return body;
61
+ }
@@ -0,0 +1,6 @@
1
+ export interface IFlowApiKeyResult {
2
+ apiKey: string;
3
+ email: string;
4
+ authMethod: 'apikey';
5
+ }
6
+ export declare function validateApiKey(apiKey: string): Promise<IFlowApiKeyResult>;
@@ -0,0 +1,17 @@
1
+ import { IFLOW_CONSTANTS } from '../constants';
2
+ export async function validateApiKey(apiKey) {
3
+ const response = await fetch(`${IFLOW_CONSTANTS.BASE_URL}/models`, {
4
+ headers: {
5
+ Authorization: `Bearer ${apiKey}`,
6
+ 'User-Agent': IFLOW_CONSTANTS.USER_AGENT
7
+ }
8
+ });
9
+ if (!response.ok) {
10
+ throw new Error(`API key validation failed: ${response.status}`);
11
+ }
12
+ return {
13
+ apiKey,
14
+ email: 'api-key-user',
15
+ authMethod: 'apikey'
16
+ };
17
+ }
@@ -0,0 +1,20 @@
1
+ export interface IFlowOAuthAuthorization {
2
+ authUrl: string;
3
+ state: string;
4
+ redirectUri: string;
5
+ }
6
+ export interface IFlowOAuthTokenResult {
7
+ accessToken: string;
8
+ refreshToken: string;
9
+ expiresAt: number;
10
+ apiKey: string;
11
+ email: string;
12
+ authMethod: 'oauth';
13
+ }
14
+ export declare function authorizeIFlowOAuth(port: number): Promise<IFlowOAuthAuthorization>;
15
+ export declare function exchangeOAuthCode(code: string, redirectUri: string): Promise<IFlowOAuthTokenResult>;
16
+ export declare function refreshOAuthToken(refreshToken: string): Promise<IFlowOAuthTokenResult>;
17
+ export declare function fetchUserInfo(accessToken: string): Promise<{
18
+ apiKey: string;
19
+ email: string;
20
+ }>;
@@ -0,0 +1,113 @@
1
+ import { IFLOW_CONSTANTS } from '../constants';
2
+ import { randomBytes } from 'node:crypto';
3
+ function base64URLEncode(buffer) {
4
+ return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
5
+ }
6
+ function generateState() {
7
+ return base64URLEncode(randomBytes(16));
8
+ }
9
+ export async function authorizeIFlowOAuth(port) {
10
+ const state = generateState();
11
+ const redirectUri = `http://localhost:${port}/oauth2callback`;
12
+ const params = new URLSearchParams({
13
+ loginMethod: 'phone',
14
+ type: 'phone',
15
+ redirect: redirectUri,
16
+ state,
17
+ client_id: IFLOW_CONSTANTS.CLIENT_ID
18
+ });
19
+ const authUrl = `${IFLOW_CONSTANTS.OAUTH_AUTHORIZE_URL}?${params.toString()}`;
20
+ return { authUrl, state, redirectUri };
21
+ }
22
+ export async function exchangeOAuthCode(code, redirectUri) {
23
+ const params = new URLSearchParams({
24
+ grant_type: 'authorization_code',
25
+ code,
26
+ redirect_uri: redirectUri,
27
+ client_id: IFLOW_CONSTANTS.CLIENT_ID,
28
+ client_secret: IFLOW_CONSTANTS.CLIENT_SECRET
29
+ });
30
+ const basicAuth = Buffer.from(`${IFLOW_CONSTANTS.CLIENT_ID}:${IFLOW_CONSTANTS.CLIENT_SECRET}`).toString('base64');
31
+ const response = await fetch(IFLOW_CONSTANTS.OAUTH_TOKEN_URL, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/x-www-form-urlencoded',
35
+ Accept: 'application/json',
36
+ Authorization: `Basic ${basicAuth}`
37
+ },
38
+ body: params.toString()
39
+ });
40
+ if (!response.ok) {
41
+ const errorText = await response.text().catch(() => '');
42
+ throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
43
+ }
44
+ const data = await response.json();
45
+ const userInfo = await fetchUserInfo(data.access_token);
46
+ const expiresIn = data.expires_in || 3600;
47
+ const expiresAt = Date.now() + expiresIn * 1000;
48
+ return {
49
+ accessToken: data.access_token,
50
+ refreshToken: data.refresh_token,
51
+ expiresAt,
52
+ apiKey: userInfo.apiKey,
53
+ email: userInfo.email,
54
+ authMethod: 'oauth'
55
+ };
56
+ }
57
+ export async function refreshOAuthToken(refreshToken) {
58
+ const params = new URLSearchParams({
59
+ grant_type: 'refresh_token',
60
+ refresh_token: refreshToken,
61
+ client_id: IFLOW_CONSTANTS.CLIENT_ID,
62
+ client_secret: IFLOW_CONSTANTS.CLIENT_SECRET
63
+ });
64
+ const basicAuth = Buffer.from(`${IFLOW_CONSTANTS.CLIENT_ID}:${IFLOW_CONSTANTS.CLIENT_SECRET}`).toString('base64');
65
+ const response = await fetch(IFLOW_CONSTANTS.OAUTH_TOKEN_URL, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/x-www-form-urlencoded',
69
+ Accept: 'application/json',
70
+ Authorization: `Basic ${basicAuth}`
71
+ },
72
+ body: params.toString()
73
+ });
74
+ if (!response.ok) {
75
+ const errorText = await response.text().catch(() => '');
76
+ throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
77
+ }
78
+ const data = await response.json();
79
+ const userInfo = await fetchUserInfo(data.access_token);
80
+ const expiresIn = data.expires_in || 3600;
81
+ const expiresAt = Date.now() + expiresIn * 1000;
82
+ return {
83
+ accessToken: data.access_token,
84
+ refreshToken: data.refresh_token || refreshToken,
85
+ expiresAt,
86
+ apiKey: userInfo.apiKey,
87
+ email: userInfo.email,
88
+ authMethod: 'oauth'
89
+ };
90
+ }
91
+ export async function fetchUserInfo(accessToken) {
92
+ const response = await fetch(`${IFLOW_CONSTANTS.USER_INFO_URL}?accessToken=${encodeURIComponent(accessToken)}`, {
93
+ headers: {
94
+ Accept: 'application/json'
95
+ }
96
+ });
97
+ if (!response.ok) {
98
+ const errorText = await response.text().catch(() => '');
99
+ throw new Error(`User info fetch failed: ${response.status} ${errorText}`);
100
+ }
101
+ const data = await response.json();
102
+ if (!data.success || !data.data) {
103
+ throw new Error('User info request not successful');
104
+ }
105
+ if (!data.data.apiKey) {
106
+ throw new Error('Missing apiKey in user info response');
107
+ }
108
+ const email = data.data.email || data.data.phone || 'oauth-user';
109
+ return {
110
+ apiKey: data.data.apiKey,
111
+ email
112
+ };
113
+ }
@@ -0,0 +1,3 @@
1
+ export { IFlowOAuthPlugin } from './plugin.js';
2
+ export type { IFlowConfig } from './plugin/config/index.js';
3
+ export type { IFlowAuthMethod, ManagedAccount } from './plugin/types.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { IFlowOAuthPlugin } from './plugin.js';
@@ -0,0 +1,24 @@
1
+ import type { ManagedAccount, AccountSelectionStrategy, IFlowAuthDetails, RefreshParts } from './types';
2
+ export declare function generateAccountId(): string;
3
+ export declare function encodeRefreshToken(parts: RefreshParts): string;
4
+ export declare function decodeRefreshToken(encoded: string): RefreshParts;
5
+ export declare class AccountManager {
6
+ private accounts;
7
+ private cursor;
8
+ private strategy;
9
+ private lastToastTime;
10
+ constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
11
+ static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
12
+ getAccountCount(): number;
13
+ getAccounts(): ManagedAccount[];
14
+ shouldShowToast(debounce?: number): boolean;
15
+ getMinWaitTime(): number;
16
+ getCurrentOrNext(): ManagedAccount | null;
17
+ addAccount(a: ManagedAccount): void;
18
+ removeAccount(a: ManagedAccount): void;
19
+ updateFromAuth(a: ManagedAccount, auth: IFlowAuthDetails): void;
20
+ markRateLimited(a: ManagedAccount, ms: number): void;
21
+ markUnhealthy(a: ManagedAccount, reason: string, recovery?: number): void;
22
+ saveToDisk(): Promise<void>;
23
+ toAuthDetails(a: ManagedAccount): IFlowAuthDetails;
24
+ }
@@ -0,0 +1,147 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { loadAccounts, saveAccounts } from './storage';
3
+ export function generateAccountId() {
4
+ return randomBytes(16).toString('hex');
5
+ }
6
+ export function encodeRefreshToken(parts) {
7
+ return Buffer.from(JSON.stringify(parts)).toString('base64');
8
+ }
9
+ export function decodeRefreshToken(encoded) {
10
+ try {
11
+ return JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
12
+ }
13
+ catch {
14
+ return { authMethod: 'apikey' };
15
+ }
16
+ }
17
+ export class AccountManager {
18
+ accounts;
19
+ cursor;
20
+ strategy;
21
+ lastToastTime = 0;
22
+ constructor(accounts, strategy = 'sticky') {
23
+ this.accounts = accounts;
24
+ this.cursor = 0;
25
+ this.strategy = strategy;
26
+ }
27
+ static async loadFromDisk(strategy) {
28
+ const s = await loadAccounts();
29
+ return new AccountManager(s.accounts, strategy || 'sticky');
30
+ }
31
+ getAccountCount() {
32
+ return this.accounts.length;
33
+ }
34
+ getAccounts() {
35
+ return [...this.accounts];
36
+ }
37
+ shouldShowToast(debounce = 30000) {
38
+ if (Date.now() - this.lastToastTime < debounce)
39
+ return false;
40
+ this.lastToastTime = Date.now();
41
+ return true;
42
+ }
43
+ getMinWaitTime() {
44
+ const now = Date.now();
45
+ const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
46
+ return waits.length > 0 ? Math.min(...waits) : 0;
47
+ }
48
+ getCurrentOrNext() {
49
+ const now = Date.now();
50
+ const available = this.accounts.filter((a) => {
51
+ if (!a.isHealthy) {
52
+ if (a.recoveryTime && now >= a.recoveryTime) {
53
+ a.isHealthy = true;
54
+ delete a.unhealthyReason;
55
+ delete a.recoveryTime;
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
61
+ });
62
+ if (available.length === 0)
63
+ return null;
64
+ let selected;
65
+ if (this.strategy === 'sticky') {
66
+ selected = available.find((_, i) => i === this.cursor) || available[0];
67
+ }
68
+ else if (this.strategy === 'round-robin') {
69
+ selected = available[this.cursor % available.length];
70
+ this.cursor = (this.cursor + 1) % available.length;
71
+ }
72
+ if (selected) {
73
+ selected.lastUsed = now;
74
+ this.cursor = this.accounts.indexOf(selected);
75
+ return selected;
76
+ }
77
+ return null;
78
+ }
79
+ addAccount(a) {
80
+ const i = this.accounts.findIndex((x) => x.id === a.id);
81
+ if (i === -1)
82
+ this.accounts.push(a);
83
+ else
84
+ this.accounts[i] = a;
85
+ }
86
+ removeAccount(a) {
87
+ const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
88
+ if (removedIndex === -1)
89
+ return;
90
+ this.accounts = this.accounts.filter((x) => x.id !== a.id);
91
+ if (this.accounts.length === 0) {
92
+ this.cursor = 0;
93
+ }
94
+ else if (this.cursor >= this.accounts.length) {
95
+ this.cursor = this.accounts.length - 1;
96
+ }
97
+ else if (removedIndex <= this.cursor && this.cursor > 0) {
98
+ this.cursor--;
99
+ }
100
+ }
101
+ updateFromAuth(a, auth) {
102
+ const acc = this.accounts.find((x) => x.id === a.id);
103
+ if (acc) {
104
+ acc.apiKey = auth.apiKey;
105
+ if (auth.authMethod === 'oauth') {
106
+ acc.accessToken = auth.access;
107
+ acc.expiresAt = auth.expires;
108
+ const p = decodeRefreshToken(auth.refresh);
109
+ acc.refreshToken = p.refreshToken;
110
+ }
111
+ acc.lastUsed = Date.now();
112
+ if (auth.email)
113
+ acc.email = auth.email;
114
+ }
115
+ }
116
+ markRateLimited(a, ms) {
117
+ const acc = this.accounts.find((x) => x.id === a.id);
118
+ if (acc)
119
+ acc.rateLimitResetTime = Date.now() + ms;
120
+ }
121
+ markUnhealthy(a, reason, recovery) {
122
+ const acc = this.accounts.find((x) => x.id === a.id);
123
+ if (acc) {
124
+ acc.isHealthy = false;
125
+ acc.unhealthyReason = reason;
126
+ acc.recoveryTime = recovery;
127
+ }
128
+ }
129
+ async saveToDisk() {
130
+ const metadata = this.accounts.map(({ lastUsed, ...rest }) => rest);
131
+ await saveAccounts({ version: 1, accounts: metadata, activeIndex: this.cursor });
132
+ }
133
+ toAuthDetails(a) {
134
+ const p = {
135
+ refreshToken: a.refreshToken,
136
+ authMethod: a.authMethod
137
+ };
138
+ return {
139
+ refresh: encodeRefreshToken(p),
140
+ access: a.accessToken || '',
141
+ expires: a.expiresAt || 0,
142
+ authMethod: a.authMethod,
143
+ apiKey: a.apiKey,
144
+ email: a.email
145
+ };
146
+ }
147
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getIDCAuthHtml(verificationUrl: string, userCode: string, statusUrl: string): string;
2
+ export declare function getSuccessHtml(): string;
3
+ export declare function getErrorHtml(message: string): string;