@asframe/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 +278 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.js +303 -0
- package/dist/iflow/apikey.d.ts +6 -0
- package/dist/iflow/apikey.js +17 -0
- package/dist/iflow/oauth.d.ts +20 -0
- package/dist/iflow/oauth.js +113 -0
- package/dist/iflow/proxy.d.ts +23 -0
- package/dist/iflow/proxy.js +435 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/plugin/accounts.d.ts +24 -0
- package/dist/plugin/accounts.js +205 -0
- package/dist/plugin/auth-page.d.ts +3 -0
- package/dist/plugin/auth-page.js +573 -0
- package/dist/plugin/cli.d.ts +11 -0
- package/dist/plugin/cli.js +77 -0
- package/dist/plugin/config/index.d.ts +2 -0
- package/dist/plugin/config/index.js +2 -0
- package/dist/plugin/config/loader.d.ts +3 -0
- package/dist/plugin/config/loader.js +110 -0
- package/dist/plugin/config/schema.d.ts +35 -0
- package/dist/plugin/config/schema.js +22 -0
- package/dist/plugin/errors.d.ts +14 -0
- package/dist/plugin/errors.js +25 -0
- package/dist/plugin/logger.d.ts +8 -0
- package/dist/plugin/logger.js +63 -0
- package/dist/plugin/server.d.ts +7 -0
- package/dist/plugin/server.js +98 -0
- package/dist/plugin/storage.d.ts +4 -0
- package/dist/plugin/storage.js +91 -0
- package/dist/plugin/token.d.ts +3 -0
- package/dist/plugin/token.js +26 -0
- package/dist/plugin/types.d.ts +58 -0
- package/dist/plugin/types.js +1 -0
- package/dist/plugin-iflow.d.ts +2 -0
- package/dist/plugin-iflow.js +141 -0
- package/dist/plugin-proxy.d.ts +2 -0
- package/dist/plugin-proxy.js +155 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +2 -0
- package/package.json +63 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { loadAccounts, saveAccounts } from './storage.js';
|
|
6
|
+
const DEBUG = process.env.IFLOW_AUTH_DEBUG === 'true';
|
|
7
|
+
function log(...args) {
|
|
8
|
+
if (DEBUG) {
|
|
9
|
+
console.error('[iflow-auth]', ...args);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function generateAccountId() {
|
|
13
|
+
return randomBytes(16).toString('hex');
|
|
14
|
+
}
|
|
15
|
+
export function encodeRefreshToken(parts) {
|
|
16
|
+
return Buffer.from(JSON.stringify(parts)).toString('base64');
|
|
17
|
+
}
|
|
18
|
+
export function decodeRefreshToken(encoded) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { authMethod: 'apikey' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function getOpenCodeDataDir() {
|
|
27
|
+
const platform = process.platform;
|
|
28
|
+
if (platform === 'win32') {
|
|
29
|
+
return join(homedir(), '.local', 'share');
|
|
30
|
+
}
|
|
31
|
+
return process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
32
|
+
}
|
|
33
|
+
async function readOpenCodeAuth() {
|
|
34
|
+
try {
|
|
35
|
+
const dataDir = getOpenCodeDataDir();
|
|
36
|
+
const authPath = join(dataDir, 'opencode', 'auth.json');
|
|
37
|
+
log('Reading auth from:', authPath);
|
|
38
|
+
const content = await fs.readFile(authPath, 'utf-8');
|
|
39
|
+
log('Auth content:', content.substring(0, 200));
|
|
40
|
+
const auth = JSON.parse(content);
|
|
41
|
+
log('Parsed auth keys:', Object.keys(auth));
|
|
42
|
+
log('iflow entry:', auth.iflow);
|
|
43
|
+
if (auth.iflow && auth.iflow.type === 'api' && auth.iflow.key) {
|
|
44
|
+
log('Found iflow API key');
|
|
45
|
+
return { key: auth.iflow.key };
|
|
46
|
+
}
|
|
47
|
+
log('No iflow API key found');
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
log('Error reading auth:', e.message);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class AccountManager {
|
|
56
|
+
accounts;
|
|
57
|
+
cursor;
|
|
58
|
+
strategy;
|
|
59
|
+
lastToastTime = 0;
|
|
60
|
+
constructor(accounts, strategy = 'sticky') {
|
|
61
|
+
this.accounts = accounts;
|
|
62
|
+
this.cursor = 0;
|
|
63
|
+
this.strategy = strategy;
|
|
64
|
+
}
|
|
65
|
+
static async loadFromDisk(strategy) {
|
|
66
|
+
log('loadFromDisk called');
|
|
67
|
+
const s = await loadAccounts();
|
|
68
|
+
log('Loaded accounts count:', s.accounts.length);
|
|
69
|
+
if (s.accounts.length === 0) {
|
|
70
|
+
log('No accounts, trying to read OpenCode auth');
|
|
71
|
+
const openCodeAuth = await readOpenCodeAuth();
|
|
72
|
+
log('OpenCode auth result:', openCodeAuth ? 'found' : 'not found');
|
|
73
|
+
if (openCodeAuth) {
|
|
74
|
+
const account = {
|
|
75
|
+
id: generateAccountId(),
|
|
76
|
+
email: 'iflow-user',
|
|
77
|
+
authMethod: 'apikey',
|
|
78
|
+
apiKey: openCodeAuth.key,
|
|
79
|
+
rateLimitResetTime: 0,
|
|
80
|
+
isHealthy: true,
|
|
81
|
+
};
|
|
82
|
+
s.accounts.push(account);
|
|
83
|
+
log('Added account from OpenCode auth');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
log('Final accounts count:', s.accounts.length);
|
|
87
|
+
return new AccountManager(s.accounts, strategy || 'sticky');
|
|
88
|
+
}
|
|
89
|
+
getAccountCount() {
|
|
90
|
+
return this.accounts.length;
|
|
91
|
+
}
|
|
92
|
+
getAccounts() {
|
|
93
|
+
return [...this.accounts];
|
|
94
|
+
}
|
|
95
|
+
shouldShowToast(debounce = 30000) {
|
|
96
|
+
if (Date.now() - this.lastToastTime < debounce)
|
|
97
|
+
return false;
|
|
98
|
+
this.lastToastTime = Date.now();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
getMinWaitTime() {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
|
|
104
|
+
return waits.length > 0 ? Math.min(...waits) : 0;
|
|
105
|
+
}
|
|
106
|
+
getCurrentOrNext() {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const available = this.accounts.filter((a) => {
|
|
109
|
+
if (!a.isHealthy) {
|
|
110
|
+
if (a.recoveryTime && now >= a.recoveryTime) {
|
|
111
|
+
a.isHealthy = true;
|
|
112
|
+
delete a.unhealthyReason;
|
|
113
|
+
delete a.recoveryTime;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
|
|
119
|
+
});
|
|
120
|
+
if (available.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
let selected;
|
|
123
|
+
if (this.strategy === 'sticky') {
|
|
124
|
+
selected = available.find((_, i) => i === this.cursor) || available[0];
|
|
125
|
+
}
|
|
126
|
+
else if (this.strategy === 'round-robin') {
|
|
127
|
+
selected = available[this.cursor % available.length];
|
|
128
|
+
this.cursor = (this.cursor + 1) % available.length;
|
|
129
|
+
}
|
|
130
|
+
if (selected) {
|
|
131
|
+
selected.lastUsed = now;
|
|
132
|
+
this.cursor = this.accounts.indexOf(selected);
|
|
133
|
+
return selected;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
addAccount(a) {
|
|
138
|
+
const i = this.accounts.findIndex((x) => x.id === a.id);
|
|
139
|
+
if (i === -1)
|
|
140
|
+
this.accounts.push(a);
|
|
141
|
+
else
|
|
142
|
+
this.accounts[i] = a;
|
|
143
|
+
}
|
|
144
|
+
removeAccount(a) {
|
|
145
|
+
const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
|
|
146
|
+
if (removedIndex === -1)
|
|
147
|
+
return;
|
|
148
|
+
this.accounts = this.accounts.filter((x) => x.id !== a.id);
|
|
149
|
+
if (this.accounts.length === 0) {
|
|
150
|
+
this.cursor = 0;
|
|
151
|
+
}
|
|
152
|
+
else if (this.cursor >= this.accounts.length) {
|
|
153
|
+
this.cursor = this.accounts.length - 1;
|
|
154
|
+
}
|
|
155
|
+
else if (removedIndex <= this.cursor && this.cursor > 0) {
|
|
156
|
+
this.cursor--;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
updateFromAuth(a, auth) {
|
|
160
|
+
const acc = this.accounts.find((x) => x.id === a.id);
|
|
161
|
+
if (acc) {
|
|
162
|
+
acc.apiKey = auth.apiKey;
|
|
163
|
+
if (auth.authMethod === 'oauth') {
|
|
164
|
+
acc.accessToken = auth.access;
|
|
165
|
+
acc.expiresAt = auth.expires;
|
|
166
|
+
const p = decodeRefreshToken(auth.refresh);
|
|
167
|
+
acc.refreshToken = p.refreshToken;
|
|
168
|
+
}
|
|
169
|
+
acc.lastUsed = Date.now();
|
|
170
|
+
if (auth.email)
|
|
171
|
+
acc.email = auth.email;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
markRateLimited(a, ms) {
|
|
175
|
+
const acc = this.accounts.find((x) => x.id === a.id);
|
|
176
|
+
if (acc)
|
|
177
|
+
acc.rateLimitResetTime = Date.now() + ms;
|
|
178
|
+
}
|
|
179
|
+
markUnhealthy(a, reason, recovery) {
|
|
180
|
+
const acc = this.accounts.find((x) => x.id === a.id);
|
|
181
|
+
if (acc) {
|
|
182
|
+
acc.isHealthy = false;
|
|
183
|
+
acc.unhealthyReason = reason;
|
|
184
|
+
acc.recoveryTime = recovery;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async saveToDisk() {
|
|
188
|
+
const metadata = this.accounts.map(({ lastUsed, ...rest }) => rest);
|
|
189
|
+
await saveAccounts({ version: 1, accounts: metadata, activeIndex: this.cursor });
|
|
190
|
+
}
|
|
191
|
+
toAuthDetails(a) {
|
|
192
|
+
const p = {
|
|
193
|
+
refreshToken: a.refreshToken,
|
|
194
|
+
authMethod: a.authMethod
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
refresh: encodeRefreshToken(p),
|
|
198
|
+
access: a.accessToken || '',
|
|
199
|
+
expires: a.expiresAt || 0,
|
|
200
|
+
authMethod: a.authMethod,
|
|
201
|
+
apiKey: a.apiKey,
|
|
202
|
+
email: a.email
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|