@zhafron/opencode-kiro-auth 1.4.9 → 1.5.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/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -9
- package/dist/core/account/account-selector.d.ts +21 -0
- package/dist/core/account/account-selector.js +64 -0
- package/dist/core/account/usage-tracker.d.ts +17 -0
- package/dist/core/account/usage-tracker.js +39 -0
- package/dist/core/auth/auth-handler.d.ts +15 -0
- package/dist/core/auth/auth-handler.js +33 -0
- package/dist/core/auth/idc-auth-method.d.ts +17 -0
- package/dist/core/auth/idc-auth-method.js +194 -0
- package/dist/core/auth/token-refresher.d.ts +22 -0
- package/dist/core/auth/token-refresher.js +53 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/request/error-handler.d.ts +29 -0
- package/dist/core/request/error-handler.js +113 -0
- package/dist/core/request/request-handler.d.ts +25 -0
- package/dist/core/request/request-handler.js +180 -0
- package/dist/core/request/response-handler.d.ts +5 -0
- package/dist/core/request/response-handler.js +61 -0
- package/dist/core/request/retry-strategy.d.ts +20 -0
- package/dist/core/request/retry-strategy.js +32 -0
- package/dist/infrastructure/database/account-cache.d.ts +14 -0
- package/dist/infrastructure/database/account-cache.js +44 -0
- package/dist/infrastructure/database/account-repository.d.ts +12 -0
- package/dist/infrastructure/database/account-repository.js +64 -0
- package/dist/infrastructure/index.d.ts +7 -0
- package/dist/infrastructure/index.js +7 -0
- package/dist/infrastructure/transformers/event-stream-parser.d.ts +7 -0
- package/dist/infrastructure/transformers/event-stream-parser.js +115 -0
- package/dist/infrastructure/transformers/history-builder.d.ts +5 -0
- package/dist/infrastructure/transformers/history-builder.js +171 -0
- package/dist/infrastructure/transformers/message-transformer.d.ts +6 -0
- package/dist/infrastructure/transformers/message-transformer.js +102 -0
- package/dist/infrastructure/transformers/tool-call-parser.d.ts +4 -0
- package/dist/infrastructure/transformers/tool-call-parser.js +45 -0
- package/dist/infrastructure/transformers/tool-transformer.d.ts +2 -0
- package/dist/infrastructure/transformers/tool-transformer.js +19 -0
- package/dist/kiro/auth.d.ts +0 -1
- package/dist/kiro/auth.js +0 -7
- package/dist/plugin/accounts.d.ts +0 -1
- package/dist/plugin/accounts.js +38 -15
- package/dist/plugin/cli.d.ts +2 -1
- package/dist/plugin/cli.js +38 -2
- package/dist/plugin/health.d.ts +1 -0
- package/dist/plugin/health.js +9 -0
- package/dist/plugin/models.d.ts +0 -2
- package/dist/plugin/models.js +0 -6
- package/dist/plugin/request.d.ts +0 -2
- package/dist/plugin/request.js +5 -282
- package/dist/plugin/response.d.ts +2 -5
- package/dist/plugin/response.js +3 -161
- package/dist/plugin/storage/locked-operations.d.ts +5 -0
- package/dist/plugin/storage/locked-operations.js +91 -0
- package/dist/plugin/storage/migrations.d.ts +2 -0
- package/dist/plugin/storage/migrations.js +109 -0
- package/dist/plugin/storage/sqlite.d.ts +8 -2
- package/dist/plugin/storage/sqlite.js +75 -59
- package/dist/plugin/streaming/index.d.ts +2 -0
- package/dist/plugin/streaming/index.js +2 -0
- package/dist/plugin/streaming/openai-converter.d.ts +2 -0
- package/dist/plugin/streaming/openai-converter.js +68 -0
- package/dist/plugin/streaming/stream-parser.d.ts +5 -0
- package/dist/plugin/streaming/stream-parser.js +136 -0
- package/dist/plugin/streaming/stream-state.d.ts +5 -0
- package/dist/plugin/streaming/stream-state.js +59 -0
- package/dist/plugin/streaming/stream-transformer.d.ts +1 -0
- package/dist/plugin/{streaming.js → streaming/stream-transformer.js} +6 -268
- package/dist/plugin/streaming/types.d.ts +25 -0
- package/dist/plugin/streaming/types.js +2 -0
- package/dist/plugin/sync/kiro-cli-parser.d.ts +8 -0
- package/dist/plugin/sync/kiro-cli-parser.js +72 -0
- package/dist/plugin/sync/kiro-cli.js +7 -77
- package/dist/plugin/types.d.ts +0 -13
- package/dist/plugin.d.ts +6 -6
- package/dist/plugin.js +16 -502
- package/package.json +11 -3
- package/dist/plugin/storage/migration.d.ts +0 -1
- package/dist/plugin/storage/migration.js +0 -52
- package/dist/plugin/streaming.d.ts +0 -3
package/dist/plugin.js
CHANGED
|
@@ -1,521 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { loadConfig } from './plugin/config';
|
|
8
|
-
import { KiroTokenRefreshError } from './plugin/errors';
|
|
9
|
-
import * as logger from './plugin/logger';
|
|
10
|
-
import { transformToCodeWhisperer } from './plugin/request';
|
|
11
|
-
import { parseEventStream } from './plugin/response';
|
|
12
|
-
import { startIDCAuthServer } from './plugin/server';
|
|
13
|
-
import { migrateJsonToSqlite } from './plugin/storage/migration';
|
|
14
|
-
import { kiroDb } from './plugin/storage/sqlite';
|
|
15
|
-
import { transformKiroStream } from './plugin/streaming';
|
|
16
|
-
import { syncFromKiroCli } from './plugin/sync/kiro-cli';
|
|
17
|
-
import { refreshAccessToken } from './plugin/token';
|
|
18
|
-
import { fetchUsageLimits, updateAccountQuota } from './plugin/usage';
|
|
1
|
+
import { KIRO_CONSTANTS } from './constants.js';
|
|
2
|
+
import { AuthHandler } from './core/auth/auth-handler.js';
|
|
3
|
+
import { RequestHandler } from './core/request/request-handler.js';
|
|
4
|
+
import { AccountCache } from './infrastructure/database/account-cache.js';
|
|
5
|
+
import { AccountRepository } from './infrastructure/database/account-repository.js';
|
|
6
|
+
import { AccountManager } from './plugin/accounts.js';
|
|
7
|
+
import { loadConfig } from './plugin/config/index.js';
|
|
19
8
|
const KIRO_PROVIDER_ID = 'kiro';
|
|
20
|
-
const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
|
|
21
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
|
-
const isNetworkError = (e) => e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message);
|
|
23
|
-
const extractModel = (url) => url.match(/models\/([^/:]+)/)?.[1] || null;
|
|
24
|
-
const formatUsageMessage = (usedCount, limitCount, email) => {
|
|
25
|
-
if (limitCount > 0) {
|
|
26
|
-
const percentage = Math.round((usedCount / limitCount) * 100);
|
|
27
|
-
return `Usage (${email}): ${usedCount}/${limitCount} (${percentage}%)`;
|
|
28
|
-
}
|
|
29
|
-
return `Usage (${email}): ${usedCount}`;
|
|
30
|
-
};
|
|
31
|
-
const openBrowser = (url) => {
|
|
32
|
-
const escapedUrl = url.replace(/"/g, '\\"');
|
|
33
|
-
const platform = process.platform;
|
|
34
|
-
const cmd = platform === 'win32'
|
|
35
|
-
? `cmd /c start "" "${escapedUrl}"`
|
|
36
|
-
: platform === 'darwin'
|
|
37
|
-
? `open "${escapedUrl}"`
|
|
38
|
-
: `xdg-open "${escapedUrl}"`;
|
|
39
|
-
exec(cmd, (error) => {
|
|
40
|
-
if (error)
|
|
41
|
-
logger.warn(`Browser error: ${error.message}`);
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
9
|
export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
45
10
|
const config = loadConfig(directory);
|
|
46
11
|
const showToast = (message, variant) => {
|
|
47
12
|
client.tui.showToast({ body: { message, variant } }).catch(() => { });
|
|
48
13
|
};
|
|
14
|
+
const cache = new AccountCache(60000);
|
|
15
|
+
const repository = new AccountRepository(cache);
|
|
16
|
+
const authHandler = new AuthHandler(config, repository);
|
|
17
|
+
const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
18
|
+
authHandler.setAccountManager(accountManager);
|
|
19
|
+
const requestHandler = new RequestHandler(accountManager, config, repository);
|
|
49
20
|
return {
|
|
50
21
|
auth: {
|
|
51
22
|
provider: id,
|
|
52
23
|
loader: async (getAuth) => {
|
|
53
24
|
await getAuth();
|
|
54
|
-
await
|
|
55
|
-
if (config.auto_sync_kiro_cli)
|
|
56
|
-
await syncFromKiroCli();
|
|
57
|
-
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
58
|
-
const allAccs = am.getAccounts();
|
|
59
|
-
for (const acc of allAccs) {
|
|
60
|
-
if (acc.isHealthy && (!acc.lastSync || Date.now() - acc.lastSync > 3600000)) {
|
|
61
|
-
try {
|
|
62
|
-
const auth = am.toAuthDetails(acc);
|
|
63
|
-
const u = await fetchUsageLimits(auth);
|
|
64
|
-
am.updateUsage(acc.id, {
|
|
65
|
-
usedCount: u.usedCount,
|
|
66
|
-
limitCount: u.limitCount,
|
|
67
|
-
email: u.email
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
catch { }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
await am.saveToDisk();
|
|
25
|
+
await authHandler.initialize();
|
|
74
26
|
return {
|
|
75
27
|
apiKey: '',
|
|
76
28
|
baseURL: KIRO_CONSTANTS.BASE_URL.replace('/generateAssistantResponse', '').replace('{{region}}', config.default_region || 'us-east-1'),
|
|
77
|
-
|
|
78
|
-
const url = typeof input === 'string' ? input : input.url;
|
|
79
|
-
if (!KIRO_API_PATTERN.test(url))
|
|
80
|
-
return fetch(input, init);
|
|
81
|
-
const body = init?.body ? JSON.parse(init.body) : {};
|
|
82
|
-
const model = extractModel(url) || body.model || 'claude-sonnet-4-5';
|
|
83
|
-
const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig;
|
|
84
|
-
const budget = body.providerOptions?.thinkingConfig?.thinkingBudget || 20000;
|
|
85
|
-
let retry = 0, iterations = 0, reductionFactor = 1.0;
|
|
86
|
-
let triedEmptySync = false;
|
|
87
|
-
const startTime = Date.now(), maxIterations = config.max_request_iterations, timeoutMs = config.request_timeout_ms;
|
|
88
|
-
while (true) {
|
|
89
|
-
iterations++;
|
|
90
|
-
if (iterations > maxIterations)
|
|
91
|
-
throw new Error(`Exceeded max iterations (${maxIterations})`);
|
|
92
|
-
if (Date.now() - startTime > timeoutMs)
|
|
93
|
-
throw new Error('Request timeout');
|
|
94
|
-
let count = am.getAccountCount();
|
|
95
|
-
if (count === 0 && config.auto_sync_kiro_cli && !triedEmptySync) {
|
|
96
|
-
triedEmptySync = true;
|
|
97
|
-
await syncFromKiroCli();
|
|
98
|
-
const refreshedAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
99
|
-
for (const a of refreshedAm.getAccounts())
|
|
100
|
-
am.addAccount(a);
|
|
101
|
-
count = am.getAccountCount();
|
|
102
|
-
}
|
|
103
|
-
if (count === 0)
|
|
104
|
-
throw new Error('No accounts');
|
|
105
|
-
let acc = am.getCurrentOrNext();
|
|
106
|
-
if (!acc) {
|
|
107
|
-
const wait = am.getMinWaitTime();
|
|
108
|
-
if (wait > 0 && wait < 30000) {
|
|
109
|
-
if (am.shouldShowToast())
|
|
110
|
-
showToast(`All accounts rate-limited. Waiting ${Math.ceil(wait / 1000)}s...`, 'warning');
|
|
111
|
-
await sleep(wait);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
throw new Error('All accounts are unhealthy or rate-limited');
|
|
115
|
-
}
|
|
116
|
-
if (am.shouldShowToast())
|
|
117
|
-
showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
118
|
-
if (am.shouldShowUsageToast() &&
|
|
119
|
-
acc.usedCount !== undefined &&
|
|
120
|
-
acc.limitCount !== undefined) {
|
|
121
|
-
const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
|
|
122
|
-
showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
|
|
123
|
-
}
|
|
124
|
-
const auth = am.toAuthDetails(acc);
|
|
125
|
-
if (accessTokenExpired(auth, config.token_expiry_buffer_ms)) {
|
|
126
|
-
try {
|
|
127
|
-
const newAuth = await refreshAccessToken(auth);
|
|
128
|
-
am.updateFromAuth(acc, newAuth);
|
|
129
|
-
await am.saveToDisk();
|
|
130
|
-
}
|
|
131
|
-
catch (e) {
|
|
132
|
-
if (config.auto_sync_kiro_cli)
|
|
133
|
-
await syncFromKiroCli();
|
|
134
|
-
const refreshedAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
135
|
-
const stillAcc = refreshedAm.getAccounts().find((a) => a.id === acc.id);
|
|
136
|
-
if (stillAcc &&
|
|
137
|
-
!accessTokenExpired(refreshedAm.toAuthDetails(stillAcc), config.token_expiry_buffer_ms)) {
|
|
138
|
-
showToast('Credentials recovered from Kiro CLI sync.', 'info');
|
|
139
|
-
acc = stillAcc;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (e instanceof KiroTokenRefreshError &&
|
|
143
|
-
(e.code === 'ExpiredTokenException' ||
|
|
144
|
-
e.code === 'InvalidTokenException' ||
|
|
145
|
-
e.code === 'HTTP_401' ||
|
|
146
|
-
e.code === 'HTTP_403')) {
|
|
147
|
-
am.markUnhealthy(acc, e.message);
|
|
148
|
-
await am.saveToDisk();
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
throw e;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const prepRequest = (f) => transformToCodeWhisperer(url, init?.body, model, auth, think, budget, f);
|
|
155
|
-
let prep = prepRequest(reductionFactor);
|
|
156
|
-
const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
157
|
-
if (config.enable_log_api_request && apiTimestamp) {
|
|
158
|
-
let b = null;
|
|
159
|
-
try {
|
|
160
|
-
b = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
161
|
-
}
|
|
162
|
-
catch { }
|
|
163
|
-
logger.logApiRequest({
|
|
164
|
-
url: prep.url,
|
|
165
|
-
method: prep.init.method,
|
|
166
|
-
headers: prep.init.headers,
|
|
167
|
-
body: b,
|
|
168
|
-
conversationId: prep.conversationId,
|
|
169
|
-
model: prep.effectiveModel,
|
|
170
|
-
email: acc.email
|
|
171
|
-
}, apiTimestamp);
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
const res = await fetch(prep.url, prep.init);
|
|
175
|
-
if (config.enable_log_api_request && apiTimestamp) {
|
|
176
|
-
const h = {};
|
|
177
|
-
res.headers.forEach((v, k) => {
|
|
178
|
-
h[k] = v;
|
|
179
|
-
});
|
|
180
|
-
logger.logApiResponse({
|
|
181
|
-
status: res.status,
|
|
182
|
-
statusText: res.statusText,
|
|
183
|
-
headers: h,
|
|
184
|
-
conversationId: prep.conversationId,
|
|
185
|
-
model: prep.effectiveModel
|
|
186
|
-
}, apiTimestamp);
|
|
187
|
-
}
|
|
188
|
-
if (res.ok) {
|
|
189
|
-
if (acc.failCount && acc.failCount > 0) {
|
|
190
|
-
acc.failCount = 0;
|
|
191
|
-
kiroDb.upsertAccount(acc);
|
|
192
|
-
}
|
|
193
|
-
if (config.usage_tracking_enabled) {
|
|
194
|
-
const sync = async (att = 0) => {
|
|
195
|
-
try {
|
|
196
|
-
const u = await fetchUsageLimits(auth);
|
|
197
|
-
updateAccountQuota(acc, u, am);
|
|
198
|
-
await am.saveToDisk();
|
|
199
|
-
}
|
|
200
|
-
catch (e) {
|
|
201
|
-
if (att < config.usage_sync_max_retries) {
|
|
202
|
-
await sleep(1000 * Math.pow(2, att));
|
|
203
|
-
return sync(att + 1);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
sync().catch(() => { });
|
|
208
|
-
}
|
|
209
|
-
if (prep.streaming) {
|
|
210
|
-
const s = transformKiroStream(res, model, prep.conversationId);
|
|
211
|
-
return new Response(new ReadableStream({
|
|
212
|
-
async start(c) {
|
|
213
|
-
try {
|
|
214
|
-
for await (const e of s)
|
|
215
|
-
c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
216
|
-
c.close();
|
|
217
|
-
}
|
|
218
|
-
catch (err) {
|
|
219
|
-
c.error(err);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
223
|
-
}
|
|
224
|
-
const text = await res.text(), p = parseEventStream(text), oai = {
|
|
225
|
-
id: prep.conversationId,
|
|
226
|
-
object: 'chat.completion',
|
|
227
|
-
created: Math.floor(Date.now() / 1000),
|
|
228
|
-
model,
|
|
229
|
-
choices: [
|
|
230
|
-
{
|
|
231
|
-
index: 0,
|
|
232
|
-
message: { role: 'assistant', content: p.content },
|
|
233
|
-
finish_reason: p.stopReason === 'tool_use' ? 'tool_calls' : 'stop'
|
|
234
|
-
}
|
|
235
|
-
],
|
|
236
|
-
usage: {
|
|
237
|
-
prompt_tokens: p.inputTokens || 0,
|
|
238
|
-
completion_tokens: p.outputTokens || 0,
|
|
239
|
-
total_tokens: (p.inputTokens || 0) + (p.outputTokens || 0)
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
if (p.toolCalls.length > 0)
|
|
243
|
-
oai.choices[0].message.tool_calls = p.toolCalls.map((tc) => ({
|
|
244
|
-
id: tc.toolUseId,
|
|
245
|
-
type: 'function',
|
|
246
|
-
function: {
|
|
247
|
-
name: tc.name,
|
|
248
|
-
arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input)
|
|
249
|
-
}
|
|
250
|
-
}));
|
|
251
|
-
return new Response(JSON.stringify(oai), {
|
|
252
|
-
headers: { 'Content-Type': 'application/json' }
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
if (res.status === 400 && reductionFactor > 0.4) {
|
|
256
|
-
reductionFactor -= 0.2;
|
|
257
|
-
showToast(`Context too long. Retrying with ${Math.round(reductionFactor * 100)}%...`, 'warning');
|
|
258
|
-
prep = prepRequest(reductionFactor);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (res.status === 401 && retry < config.rate_limit_max_retries) {
|
|
262
|
-
retry++;
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (res.status === 500) {
|
|
266
|
-
acc.failCount = (acc.failCount || 0) + 1;
|
|
267
|
-
let errorMessage = 'Internal Server Error';
|
|
268
|
-
try {
|
|
269
|
-
const errorBody = await res.text();
|
|
270
|
-
const errorData = JSON.parse(errorBody);
|
|
271
|
-
if (errorData.message) {
|
|
272
|
-
errorMessage = errorData.message;
|
|
273
|
-
}
|
|
274
|
-
else if (errorData.Message) {
|
|
275
|
-
errorMessage = errorData.Message;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
catch (e) {
|
|
279
|
-
// If we can't parse the error, use default message
|
|
280
|
-
}
|
|
281
|
-
if (acc.failCount < 5) {
|
|
282
|
-
const delay = 1000 * Math.pow(2, acc.failCount - 1);
|
|
283
|
-
showToast(`Server Error (500): ${errorMessage}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
|
|
284
|
-
await sleep(delay);
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
am.markUnhealthy(acc, `Server Error (500) after 5 attempts: ${errorMessage}`);
|
|
289
|
-
await am.saveToDisk();
|
|
290
|
-
showToast(`Server Error (500): ${errorMessage}. Marking account as unhealthy and switching...`, 'warning');
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (res.status === 429) {
|
|
295
|
-
const w = parseInt(res.headers.get('retry-after') || '60') * 1000;
|
|
296
|
-
am.markRateLimited(acc, w);
|
|
297
|
-
await am.saveToDisk();
|
|
298
|
-
if (count > 1) {
|
|
299
|
-
showToast(`Rate limited. Switching account...`, 'warning');
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
showToast(`Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
|
|
303
|
-
await sleep(w);
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if ((res.status === 402 || res.status === 403) && count > 1) {
|
|
307
|
-
let errorReason = res.status === 402 ? 'Quota' : 'Forbidden';
|
|
308
|
-
let isPermanent = false;
|
|
309
|
-
try {
|
|
310
|
-
const errorBody = await res.text();
|
|
311
|
-
const errorData = JSON.parse(errorBody);
|
|
312
|
-
if (errorData.reason === 'INVALID_MODEL_ID') {
|
|
313
|
-
logger.warn(`Invalid model ID for ${acc.email}: ${errorData.message}`);
|
|
314
|
-
throw new Error(`Invalid model: ${errorData.message}`);
|
|
315
|
-
}
|
|
316
|
-
if (errorData.reason === 'TEMPORARILY_SUSPENDED') {
|
|
317
|
-
errorReason = 'Account Suspended';
|
|
318
|
-
isPermanent = true;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
catch (e) {
|
|
322
|
-
if (e instanceof Error && e.message.includes('Invalid model')) {
|
|
323
|
-
throw e;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (isPermanent) {
|
|
327
|
-
acc.failCount = 10;
|
|
328
|
-
}
|
|
329
|
-
am.markUnhealthy(acc, errorReason);
|
|
330
|
-
await am.saveToDisk();
|
|
331
|
-
showToast(`${errorReason}. Switching account...`, 'warning');
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const h = {};
|
|
335
|
-
res.headers.forEach((v, k) => {
|
|
336
|
-
h[k] = v;
|
|
337
|
-
});
|
|
338
|
-
const rData = {
|
|
339
|
-
status: res.status,
|
|
340
|
-
statusText: res.statusText,
|
|
341
|
-
headers: h,
|
|
342
|
-
error: `Kiro Error: ${res.status}`,
|
|
343
|
-
conversationId: prep.conversationId,
|
|
344
|
-
model: prep.effectiveModel
|
|
345
|
-
};
|
|
346
|
-
let lastB = null;
|
|
347
|
-
try {
|
|
348
|
-
lastB = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
349
|
-
}
|
|
350
|
-
catch { }
|
|
351
|
-
if (!config.enable_log_api_request)
|
|
352
|
-
logger.logApiError({
|
|
353
|
-
url: prep.url,
|
|
354
|
-
method: prep.init.method,
|
|
355
|
-
headers: prep.init.headers,
|
|
356
|
-
body: lastB,
|
|
357
|
-
conversationId: prep.conversationId,
|
|
358
|
-
model: prep.effectiveModel,
|
|
359
|
-
email: acc.email
|
|
360
|
-
}, rData, logger.getTimestamp());
|
|
361
|
-
throw new Error(`Kiro Error: ${res.status}`);
|
|
362
|
-
}
|
|
363
|
-
catch (e) {
|
|
364
|
-
if (isNetworkError(e) && retry < config.rate_limit_max_retries) {
|
|
365
|
-
const d = 5000 * Math.pow(2, retry);
|
|
366
|
-
showToast(`Network error. Retrying in ${Math.ceil(d / 1000)}s...`, 'warning');
|
|
367
|
-
await sleep(d);
|
|
368
|
-
retry++;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
throw e;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
29
|
+
fetch: (input, init) => requestHandler.handle(input, init, showToast)
|
|
375
30
|
};
|
|
376
31
|
},
|
|
377
|
-
methods:
|
|
378
|
-
{
|
|
379
|
-
id: 'idc',
|
|
380
|
-
label: 'AWS Builder ID (IDC)',
|
|
381
|
-
type: 'oauth',
|
|
382
|
-
authorize: async (inputs) => new Promise(async (resolve) => {
|
|
383
|
-
const region = config.default_region;
|
|
384
|
-
if (inputs) {
|
|
385
|
-
const accounts = [];
|
|
386
|
-
let startFresh = true;
|
|
387
|
-
const existingAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
388
|
-
const idcAccs = existingAm.getAccounts().filter((a) => a.authMethod === 'idc');
|
|
389
|
-
if (idcAccs.length > 0) {
|
|
390
|
-
const existingAccounts = idcAccs.map((acc, idx) => ({
|
|
391
|
-
email: acc.email,
|
|
392
|
-
index: idx
|
|
393
|
-
}));
|
|
394
|
-
startFresh = (await promptLoginMode(existingAccounts)) === 'fresh';
|
|
395
|
-
}
|
|
396
|
-
while (true) {
|
|
397
|
-
try {
|
|
398
|
-
const authData = await authorizeKiroIDC(region);
|
|
399
|
-
const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
|
|
400
|
-
openBrowser(url);
|
|
401
|
-
const res = await waitForAuth();
|
|
402
|
-
const u = await fetchUsageLimits({
|
|
403
|
-
refresh: '',
|
|
404
|
-
access: res.accessToken,
|
|
405
|
-
expires: res.expiresAt,
|
|
406
|
-
authMethod: 'idc',
|
|
407
|
-
region,
|
|
408
|
-
clientId: res.clientId,
|
|
409
|
-
clientSecret: res.clientSecret
|
|
410
|
-
});
|
|
411
|
-
if (!u.email) {
|
|
412
|
-
console.log('\n[Error] Failed to fetch account email. Skipping...\n');
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
accounts.push(res);
|
|
416
|
-
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
417
|
-
if (accounts.length === 1 && startFresh)
|
|
418
|
-
am.getAccounts()
|
|
419
|
-
.filter((a) => a.authMethod === 'idc')
|
|
420
|
-
.forEach((a) => am.removeAccount(a));
|
|
421
|
-
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
422
|
-
const acc = {
|
|
423
|
-
id,
|
|
424
|
-
email: u.email,
|
|
425
|
-
authMethod: 'idc',
|
|
426
|
-
region,
|
|
427
|
-
clientId: res.clientId,
|
|
428
|
-
clientSecret: res.clientSecret,
|
|
429
|
-
refreshToken: res.refreshToken,
|
|
430
|
-
accessToken: res.accessToken,
|
|
431
|
-
expiresAt: res.expiresAt,
|
|
432
|
-
rateLimitResetTime: 0,
|
|
433
|
-
isHealthy: true,
|
|
434
|
-
failCount: 0
|
|
435
|
-
};
|
|
436
|
-
am.addAccount(acc);
|
|
437
|
-
am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
|
|
438
|
-
await am.saveToDisk();
|
|
439
|
-
console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
|
|
440
|
-
if (!(await promptAddAnotherAccount(am.getAccountCount())))
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
catch (e) {
|
|
444
|
-
console.log(`\n[Error] Login failed: ${e.message}\n`);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const finalAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
449
|
-
return resolve({
|
|
450
|
-
url: '',
|
|
451
|
-
instructions: `Complete (${finalAm.getAccountCount()} accounts).`,
|
|
452
|
-
method: 'auto',
|
|
453
|
-
callback: async () => ({
|
|
454
|
-
type: 'success',
|
|
455
|
-
key: finalAm.getAccounts()[0]?.accessToken
|
|
456
|
-
})
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
try {
|
|
460
|
-
const authData = await authorizeKiroIDC(region);
|
|
461
|
-
const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
|
|
462
|
-
openBrowser(url);
|
|
463
|
-
resolve({
|
|
464
|
-
url,
|
|
465
|
-
instructions: `Open: ${url}`,
|
|
466
|
-
method: 'auto',
|
|
467
|
-
callback: async () => {
|
|
468
|
-
try {
|
|
469
|
-
const res = await waitForAuth(), am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
470
|
-
const u = await fetchUsageLimits({
|
|
471
|
-
refresh: '',
|
|
472
|
-
access: res.accessToken,
|
|
473
|
-
expires: res.expiresAt,
|
|
474
|
-
authMethod: 'idc',
|
|
475
|
-
region,
|
|
476
|
-
clientId: res.clientId,
|
|
477
|
-
clientSecret: res.clientSecret
|
|
478
|
-
});
|
|
479
|
-
if (!u.email)
|
|
480
|
-
throw new Error('No email');
|
|
481
|
-
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
482
|
-
const acc = {
|
|
483
|
-
id,
|
|
484
|
-
email: u.email,
|
|
485
|
-
authMethod: 'idc',
|
|
486
|
-
region,
|
|
487
|
-
clientId: res.clientId,
|
|
488
|
-
clientSecret: res.clientSecret,
|
|
489
|
-
refreshToken: res.refreshToken,
|
|
490
|
-
accessToken: res.accessToken,
|
|
491
|
-
expiresAt: res.expiresAt,
|
|
492
|
-
rateLimitResetTime: 0,
|
|
493
|
-
isHealthy: true,
|
|
494
|
-
failCount: 0
|
|
495
|
-
};
|
|
496
|
-
am.addAccount(acc);
|
|
497
|
-
if (u.email)
|
|
498
|
-
am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
|
|
499
|
-
await am.saveToDisk();
|
|
500
|
-
return { type: 'success', key: res.accessToken };
|
|
501
|
-
}
|
|
502
|
-
catch (e) {
|
|
503
|
-
return { type: 'failed' };
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
catch (e) {
|
|
509
|
-
resolve({
|
|
510
|
-
url: '',
|
|
511
|
-
instructions: 'Failed',
|
|
512
|
-
method: 'auto',
|
|
513
|
-
callback: async () => ({ type: 'failed' })
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
})
|
|
517
|
-
}
|
|
518
|
-
]
|
|
32
|
+
methods: authHandler.getMethods()
|
|
519
33
|
}
|
|
520
34
|
};
|
|
521
35
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhafron/opencode-kiro-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc -p tsconfig.build.json",
|
|
10
10
|
"format": "prettier --write 'src/**/*.ts'",
|
|
11
|
-
"typecheck": "tsc --noEmit"
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"prepare": "husky"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"opencode",
|
|
@@ -38,6 +39,8 @@
|
|
|
38
39
|
"@types/node": "^20.0.0",
|
|
39
40
|
"@types/proper-lockfile": "^4.1.4",
|
|
40
41
|
"bun-types": "^1.3.6",
|
|
42
|
+
"husky": "^9.1.7",
|
|
43
|
+
"lint-staged": "^16.2.7",
|
|
41
44
|
"prettier": "^3.4.2",
|
|
42
45
|
"prettier-plugin-organize-imports": "^4.1.0",
|
|
43
46
|
"typescript": "^5.7.3"
|
|
@@ -53,5 +56,10 @@
|
|
|
53
56
|
"dist",
|
|
54
57
|
"package.json",
|
|
55
58
|
"README.md"
|
|
56
|
-
]
|
|
59
|
+
],
|
|
60
|
+
"lint-staged": {
|
|
61
|
+
"*.ts": [
|
|
62
|
+
"prettier --write"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
57
65
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function migrateJsonToSqlite(): Promise<void>;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import * as logger from '../logger';
|
|
5
|
-
import { kiroDb } from './sqlite';
|
|
6
|
-
function getBaseDir() {
|
|
7
|
-
const p = process.platform;
|
|
8
|
-
if (p === 'win32')
|
|
9
|
-
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
|
|
10
|
-
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
|
|
11
|
-
}
|
|
12
|
-
export async function migrateJsonToSqlite() {
|
|
13
|
-
const base = getBaseDir();
|
|
14
|
-
const accPath = join(base, 'kiro-accounts.json');
|
|
15
|
-
const usePath = join(base, 'kiro-usage.json');
|
|
16
|
-
try {
|
|
17
|
-
const accExists = await fs
|
|
18
|
-
.access(accPath)
|
|
19
|
-
.then(() => true)
|
|
20
|
-
.catch(() => false);
|
|
21
|
-
const useExists = await fs
|
|
22
|
-
.access(usePath)
|
|
23
|
-
.then(() => true)
|
|
24
|
-
.catch(() => false);
|
|
25
|
-
if (accExists) {
|
|
26
|
-
const accData = JSON.parse(await fs.readFile(accPath, 'utf-8'));
|
|
27
|
-
const useData = useExists ? JSON.parse(await fs.readFile(usePath, 'utf-8')) : { usage: {} };
|
|
28
|
-
if (accData.accounts && Array.isArray(accData.accounts)) {
|
|
29
|
-
for (const acc of accData.accounts) {
|
|
30
|
-
const usage = useData.usage[acc.id] || {};
|
|
31
|
-
kiroDb.upsertAccount({
|
|
32
|
-
...acc,
|
|
33
|
-
email: acc.realEmail || acc.email,
|
|
34
|
-
rateLimitResetTime: acc.rateLimitResetTime || 0,
|
|
35
|
-
isHealthy: acc.isHealthy !== false,
|
|
36
|
-
failCount: 0,
|
|
37
|
-
lastUsed: acc.lastUsed || 0,
|
|
38
|
-
usedCount: usage.usedCount || 0,
|
|
39
|
-
limitCount: usage.limitCount || 0,
|
|
40
|
-
lastSync: usage.lastSync || 0
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
await fs.rename(accPath, accPath + '.bak');
|
|
45
|
-
if (useExists)
|
|
46
|
-
await fs.rename(usePath, usePath + '.bak');
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
logger.error('Migration failed', e);
|
|
51
|
-
}
|
|
52
|
-
}
|