@kervnet/opencode-kiro-auth 1.5.1
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 +159 -0
- package/dist/constants.d.ts +24 -0
- package/dist/constants.js +55 -0
- package/dist/core/account/account-selector.d.ts +25 -0
- package/dist/core/account/account-selector.js +84 -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 +43 -0
- package/dist/core/auth/idc-auth-method.d.ts +17 -0
- package/dist/core/auth/idc-auth-method.js +200 -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 +30 -0
- package/dist/core/request/error-handler.js +113 -0
- package/dist/core/request/request-handler.d.ts +27 -0
- package/dist/core/request/request-handler.js +199 -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 +18 -0
- package/dist/core/request/retry-strategy.js +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -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 +4 -0
- package/dist/kiro/auth.js +25 -0
- package/dist/kiro/oauth-idc.d.ts +24 -0
- package/dist/kiro/oauth-idc.js +151 -0
- package/dist/plugin/accounts.d.ts +29 -0
- package/dist/plugin/accounts.js +235 -0
- package/dist/plugin/auth-page.d.ts +3 -0
- package/dist/plugin/auth-page.js +573 -0
- package/dist/plugin/cli.d.ts +8 -0
- package/dist/plugin/cli.js +103 -0
- package/dist/plugin/config/index.d.ts +3 -0
- package/dist/plugin/config/index.js +2 -0
- package/dist/plugin/config/loader.d.ts +6 -0
- package/dist/plugin/config/loader.js +129 -0
- package/dist/plugin/config/schema.d.ts +56 -0
- package/dist/plugin/config/schema.js +36 -0
- package/dist/plugin/errors.d.ts +17 -0
- package/dist/plugin/errors.js +34 -0
- package/dist/plugin/health.d.ts +1 -0
- package/dist/plugin/health.js +9 -0
- package/dist/plugin/image-handler.d.ts +14 -0
- package/dist/plugin/image-handler.js +64 -0
- package/dist/plugin/logger.d.ts +8 -0
- package/dist/plugin/logger.js +63 -0
- package/dist/plugin/models.d.ts +1 -0
- package/dist/plugin/models.js +8 -0
- package/dist/plugin/request.d.ts +2 -0
- package/dist/plugin/request.js +239 -0
- package/dist/plugin/response.d.ts +3 -0
- package/dist/plugin/response.js +95 -0
- package/dist/plugin/server.d.ts +24 -0
- package/dist/plugin/server.js +166 -0
- 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 +17 -0
- package/dist/plugin/storage/sqlite.js +134 -0
- 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/stream-transformer.js +248 -0
- package/dist/plugin/streaming/types.d.ts +25 -0
- package/dist/plugin/streaming/types.js +2 -0
- package/dist/plugin/sync/aws-sso.d.ts +2 -0
- package/dist/plugin/sync/aws-sso.js +50 -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.d.ts +2 -0
- package/dist/plugin/sync/kiro-cli.js +197 -0
- package/dist/plugin/token.d.ts +2 -0
- package/dist/plugin/token.js +79 -0
- package/dist/plugin/types.d.ts +109 -0
- package/dist/plugin/types.js +0 -0
- package/dist/plugin/usage.d.ts +3 -0
- package/dist/plugin/usage.js +45 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.js +37 -0
- package/package.json +65 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { authorizeKiroIDC } from '../../kiro/oauth-idc.js';
|
|
3
|
+
import { createDeterministicAccountId } from '../../plugin/accounts.js';
|
|
4
|
+
import { promptAddAnotherAccount, promptDeleteAccount, promptLoginMode } from '../../plugin/cli.js';
|
|
5
|
+
import * as logger from '../../plugin/logger.js';
|
|
6
|
+
import { startIDCAuthServer } from '../../plugin/server.js';
|
|
7
|
+
import { fetchUsageLimits } from '../../plugin/usage.js';
|
|
8
|
+
const openBrowser = (url) => {
|
|
9
|
+
const escapedUrl = url.replace(/"/g, '\\"');
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const cmd = platform === 'win32'
|
|
12
|
+
? `cmd /c start "" "${escapedUrl}"`
|
|
13
|
+
: platform === 'darwin'
|
|
14
|
+
? `open "${escapedUrl}"`
|
|
15
|
+
: `xdg-open "${escapedUrl}"`;
|
|
16
|
+
exec(cmd, (error) => {
|
|
17
|
+
if (error)
|
|
18
|
+
logger.warn(`Browser error: ${error.message}`);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export class IdcAuthMethod {
|
|
22
|
+
config;
|
|
23
|
+
repository;
|
|
24
|
+
constructor(config, repository) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.repository = repository;
|
|
27
|
+
}
|
|
28
|
+
async authorize(inputs) {
|
|
29
|
+
return new Promise(async (resolve) => {
|
|
30
|
+
const region = this.config.default_region;
|
|
31
|
+
if (inputs) {
|
|
32
|
+
await this.handleMultipleLogin(region, resolve);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await this.handleSingleLogin(region, resolve);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async handleMultipleLogin(region, resolve) {
|
|
40
|
+
const accounts = [];
|
|
41
|
+
let startFresh = true;
|
|
42
|
+
while (true) {
|
|
43
|
+
const existingAccounts = await this.repository.findAll();
|
|
44
|
+
const idcAccs = existingAccounts.filter((a) => a.authMethod === 'idc');
|
|
45
|
+
if (idcAccs.length === 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const existingAccountsList = idcAccs.map((acc, idx) => ({
|
|
49
|
+
email: acc.email,
|
|
50
|
+
index: idx
|
|
51
|
+
}));
|
|
52
|
+
const mode = await promptLoginMode(existingAccountsList);
|
|
53
|
+
if (mode === 'delete') {
|
|
54
|
+
const deleteIndices = await promptDeleteAccount(existingAccountsList);
|
|
55
|
+
if (deleteIndices !== null && deleteIndices.length > 0) {
|
|
56
|
+
for (const idx of deleteIndices) {
|
|
57
|
+
const accToDelete = idcAccs[idx];
|
|
58
|
+
if (accToDelete) {
|
|
59
|
+
await this.repository.delete(accToDelete.id);
|
|
60
|
+
console.log(`[Success] Deleted: ${accToDelete.email}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log(`\n[Success] Deleted ${deleteIndices.length} account(s)\n`);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (mode === 'add') {
|
|
68
|
+
startFresh = false;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (mode === 'fresh') {
|
|
72
|
+
startFresh = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
while (true) {
|
|
77
|
+
try {
|
|
78
|
+
const authData = await authorizeKiroIDC(region);
|
|
79
|
+
const { url, waitForAuth } = await startIDCAuthServer(authData, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
80
|
+
openBrowser(url);
|
|
81
|
+
const res = await waitForAuth();
|
|
82
|
+
const u = await fetchUsageLimits({
|
|
83
|
+
refresh: '',
|
|
84
|
+
access: res.accessToken,
|
|
85
|
+
expires: res.expiresAt,
|
|
86
|
+
authMethod: 'idc',
|
|
87
|
+
region,
|
|
88
|
+
clientId: res.clientId,
|
|
89
|
+
clientSecret: res.clientSecret
|
|
90
|
+
});
|
|
91
|
+
if (!u.email) {
|
|
92
|
+
console.log('\n[Error] Failed to fetch account email. Skipping...\n');
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
accounts.push(res);
|
|
96
|
+
if (accounts.length === 1 && startFresh) {
|
|
97
|
+
const allAccounts = await this.repository.findAll();
|
|
98
|
+
const idcAccountsToRemove = allAccounts.filter((a) => a.authMethod === 'idc');
|
|
99
|
+
for (const acc of idcAccountsToRemove) {
|
|
100
|
+
await this.repository.delete(acc.id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
104
|
+
const acc = {
|
|
105
|
+
id,
|
|
106
|
+
email: u.email,
|
|
107
|
+
authMethod: 'idc',
|
|
108
|
+
region,
|
|
109
|
+
clientId: res.clientId,
|
|
110
|
+
clientSecret: res.clientSecret,
|
|
111
|
+
refreshToken: res.refreshToken,
|
|
112
|
+
accessToken: res.accessToken,
|
|
113
|
+
expiresAt: res.expiresAt,
|
|
114
|
+
rateLimitResetTime: 0,
|
|
115
|
+
isHealthy: true,
|
|
116
|
+
failCount: 0,
|
|
117
|
+
usedCount: u.usedCount,
|
|
118
|
+
limitCount: u.limitCount
|
|
119
|
+
};
|
|
120
|
+
await this.repository.save(acc);
|
|
121
|
+
const currentCount = (await this.repository.findAll()).length;
|
|
122
|
+
console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
|
|
123
|
+
if (!(await promptAddAnotherAccount(currentCount)))
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
console.log(`\n[Error] Login failed: ${e.message}\n`);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const finalAccounts = await this.repository.findAll();
|
|
132
|
+
return resolve({
|
|
133
|
+
url: '',
|
|
134
|
+
instructions: `Complete (${finalAccounts.length} accounts).`,
|
|
135
|
+
method: 'auto',
|
|
136
|
+
callback: async () => ({
|
|
137
|
+
type: 'success',
|
|
138
|
+
key: finalAccounts[0]?.accessToken || ''
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async handleSingleLogin(region, resolve) {
|
|
143
|
+
try {
|
|
144
|
+
const authData = await authorizeKiroIDC(region);
|
|
145
|
+
const { url, waitForAuth } = await startIDCAuthServer(authData, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
146
|
+
openBrowser(url);
|
|
147
|
+
resolve({
|
|
148
|
+
url,
|
|
149
|
+
instructions: `Open: ${url}`,
|
|
150
|
+
method: 'auto',
|
|
151
|
+
callback: async () => {
|
|
152
|
+
try {
|
|
153
|
+
const res = await waitForAuth();
|
|
154
|
+
const u = await fetchUsageLimits({
|
|
155
|
+
refresh: '',
|
|
156
|
+
access: res.accessToken,
|
|
157
|
+
expires: res.expiresAt,
|
|
158
|
+
authMethod: 'idc',
|
|
159
|
+
region,
|
|
160
|
+
clientId: res.clientId,
|
|
161
|
+
clientSecret: res.clientSecret
|
|
162
|
+
});
|
|
163
|
+
if (!u.email)
|
|
164
|
+
throw new Error('No email');
|
|
165
|
+
const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
|
|
166
|
+
const acc = {
|
|
167
|
+
id,
|
|
168
|
+
email: u.email,
|
|
169
|
+
authMethod: 'idc',
|
|
170
|
+
region,
|
|
171
|
+
clientId: res.clientId,
|
|
172
|
+
clientSecret: res.clientSecret,
|
|
173
|
+
refreshToken: res.refreshToken,
|
|
174
|
+
accessToken: res.accessToken,
|
|
175
|
+
expiresAt: res.expiresAt,
|
|
176
|
+
rateLimitResetTime: 0,
|
|
177
|
+
isHealthy: true,
|
|
178
|
+
failCount: 0,
|
|
179
|
+
usedCount: u.usedCount,
|
|
180
|
+
limitCount: u.limitCount
|
|
181
|
+
};
|
|
182
|
+
await this.repository.save(acc);
|
|
183
|
+
return { type: 'success', key: res.accessToken };
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
return { type: 'failed' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
resolve({
|
|
193
|
+
url: '',
|
|
194
|
+
instructions: 'Failed',
|
|
195
|
+
method: 'auto',
|
|
196
|
+
callback: async () => ({ type: 'failed' })
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { KiroAuthDetails, ManagedAccount } from '../../plugin/types';
|
|
4
|
+
type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
|
|
5
|
+
interface TokenRefresherConfig {
|
|
6
|
+
token_expiry_buffer_ms: number;
|
|
7
|
+
auto_sync_kiro_cli: boolean;
|
|
8
|
+
account_selection_strategy: 'sticky' | 'round-robin' | 'lowest-usage';
|
|
9
|
+
}
|
|
10
|
+
export declare class TokenRefresher {
|
|
11
|
+
private config;
|
|
12
|
+
private accountManager;
|
|
13
|
+
private syncFromKiroCli;
|
|
14
|
+
private repository;
|
|
15
|
+
constructor(config: TokenRefresherConfig, accountManager: AccountManager, syncFromKiroCli: () => Promise<void>, repository: AccountRepository);
|
|
16
|
+
refreshIfNeeded(account: ManagedAccount, auth: KiroAuthDetails, showToast: ToastFunction): Promise<{
|
|
17
|
+
account: ManagedAccount;
|
|
18
|
+
shouldContinue: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
private handleRefreshError;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { accessTokenExpired } from '../../kiro/auth';
|
|
2
|
+
import { KiroTokenRefreshError } from '../../plugin/errors';
|
|
3
|
+
import { refreshAccessToken } from '../../plugin/token';
|
|
4
|
+
export class TokenRefresher {
|
|
5
|
+
config;
|
|
6
|
+
accountManager;
|
|
7
|
+
syncFromKiroCli;
|
|
8
|
+
repository;
|
|
9
|
+
constructor(config, accountManager, syncFromKiroCli, repository) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.accountManager = accountManager;
|
|
12
|
+
this.syncFromKiroCli = syncFromKiroCli;
|
|
13
|
+
this.repository = repository;
|
|
14
|
+
}
|
|
15
|
+
async refreshIfNeeded(account, auth, showToast) {
|
|
16
|
+
if (!accessTokenExpired(auth, this.config.token_expiry_buffer_ms)) {
|
|
17
|
+
return { account, shouldContinue: false };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const newAuth = await refreshAccessToken(auth);
|
|
21
|
+
this.accountManager.updateFromAuth(account, newAuth);
|
|
22
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
23
|
+
return { account, shouldContinue: false };
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return await this.handleRefreshError(e, account, showToast);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async handleRefreshError(error, account, showToast) {
|
|
30
|
+
if (this.config.auto_sync_kiro_cli) {
|
|
31
|
+
await this.syncFromKiroCli();
|
|
32
|
+
}
|
|
33
|
+
this.repository.invalidateCache();
|
|
34
|
+
const accounts = await this.repository.findAll();
|
|
35
|
+
const stillAcc = accounts.find((a) => a.id === account.id);
|
|
36
|
+
if (stillAcc &&
|
|
37
|
+
!accessTokenExpired(this.accountManager.toAuthDetails(stillAcc), this.config.token_expiry_buffer_ms)) {
|
|
38
|
+
showToast('Credentials recovered from Kiro CLI sync.', 'info');
|
|
39
|
+
return { account: stillAcc, shouldContinue: true };
|
|
40
|
+
}
|
|
41
|
+
if (error instanceof KiroTokenRefreshError &&
|
|
42
|
+
(error.code === 'ExpiredTokenException' ||
|
|
43
|
+
error.code === 'InvalidTokenException' ||
|
|
44
|
+
error.code === 'HTTP_401' ||
|
|
45
|
+
error.code === 'HTTP_403' ||
|
|
46
|
+
error.message.includes('Invalid refresh token provided'))) {
|
|
47
|
+
this.accountManager.markUnhealthy(account, error.message);
|
|
48
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
49
|
+
return { account, shouldContinue: true };
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './account/account-selector.js';
|
|
2
|
+
export * from './account/usage-tracker.js';
|
|
3
|
+
export * from './auth/auth-handler.js';
|
|
4
|
+
export * from './auth/idc-auth-method.js';
|
|
5
|
+
export * from './auth/token-refresher.js';
|
|
6
|
+
export * from './request/error-handler.js';
|
|
7
|
+
export * from './request/request-handler.js';
|
|
8
|
+
export * from './request/response-handler.js';
|
|
9
|
+
export * from './request/retry-strategy.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './account/account-selector.js';
|
|
2
|
+
export * from './account/usage-tracker.js';
|
|
3
|
+
export * from './auth/auth-handler.js';
|
|
4
|
+
export * from './auth/idc-auth-method.js';
|
|
5
|
+
export * from './auth/token-refresher.js';
|
|
6
|
+
export * from './request/error-handler.js';
|
|
7
|
+
export * from './request/request-handler.js';
|
|
8
|
+
export * from './request/response-handler.js';
|
|
9
|
+
export * from './request/retry-strategy.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { ManagedAccount } from '../../plugin/types';
|
|
4
|
+
type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
|
|
5
|
+
interface RequestContext {
|
|
6
|
+
reductionFactor: number;
|
|
7
|
+
retry: number;
|
|
8
|
+
}
|
|
9
|
+
interface ErrorHandlerConfig {
|
|
10
|
+
rate_limit_max_retries: number;
|
|
11
|
+
rate_limit_retry_delay_ms: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class ErrorHandler {
|
|
14
|
+
private config;
|
|
15
|
+
private accountManager;
|
|
16
|
+
private repository;
|
|
17
|
+
constructor(config: ErrorHandlerConfig, accountManager: AccountManager, repository: AccountRepository);
|
|
18
|
+
handle(error: any, response: Response, account: ManagedAccount, context: RequestContext, showToast: ToastFunction): Promise<{
|
|
19
|
+
shouldRetry: boolean;
|
|
20
|
+
newContext?: RequestContext;
|
|
21
|
+
switchAccount?: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
handleNetworkError(error: any, context: RequestContext, showToast: ToastFunction): Promise<{
|
|
24
|
+
shouldRetry: boolean;
|
|
25
|
+
newContext?: RequestContext;
|
|
26
|
+
}>;
|
|
27
|
+
private isNetworkError;
|
|
28
|
+
private sleep;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export class ErrorHandler {
|
|
2
|
+
config;
|
|
3
|
+
accountManager;
|
|
4
|
+
repository;
|
|
5
|
+
constructor(config, accountManager, repository) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.accountManager = accountManager;
|
|
8
|
+
this.repository = repository;
|
|
9
|
+
}
|
|
10
|
+
async handle(error, response, account, context, showToast) {
|
|
11
|
+
if (response.status === 400 && context.reductionFactor > 0.4) {
|
|
12
|
+
const newFactor = context.reductionFactor - 0.2;
|
|
13
|
+
showToast(`Context too long. Retrying with ${Math.round(newFactor * 100)}%...`, 'warning');
|
|
14
|
+
return {
|
|
15
|
+
shouldRetry: true,
|
|
16
|
+
newContext: { ...context, reductionFactor: newFactor }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (response.status === 401 && context.retry < this.config.rate_limit_max_retries) {
|
|
20
|
+
return {
|
|
21
|
+
shouldRetry: true,
|
|
22
|
+
newContext: { ...context, retry: context.retry + 1 }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (response.status === 500) {
|
|
26
|
+
account.failCount = (account.failCount || 0) + 1;
|
|
27
|
+
let errorMessage = 'Internal Server Error';
|
|
28
|
+
try {
|
|
29
|
+
const errorBody = await response.text();
|
|
30
|
+
const errorData = JSON.parse(errorBody);
|
|
31
|
+
if (errorData.message) {
|
|
32
|
+
errorMessage = errorData.message;
|
|
33
|
+
}
|
|
34
|
+
else if (errorData.Message) {
|
|
35
|
+
errorMessage = errorData.Message;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) { }
|
|
39
|
+
if (account.failCount < 5) {
|
|
40
|
+
const delay = 1000 * Math.pow(2, account.failCount - 1);
|
|
41
|
+
showToast(`Server Error (500): ${errorMessage}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
|
|
42
|
+
await this.sleep(delay);
|
|
43
|
+
return { shouldRetry: true };
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.accountManager.markUnhealthy(account, `Server Error (500) after 5 attempts: ${errorMessage}`);
|
|
47
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
48
|
+
showToast(`Server Error (500): ${errorMessage}. Marking account as unhealthy and switching...`, 'warning');
|
|
49
|
+
return { shouldRetry: true, switchAccount: true };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 429) {
|
|
53
|
+
const w = parseInt(response.headers.get('retry-after') || '60') * 1000;
|
|
54
|
+
this.accountManager.markRateLimited(account, w);
|
|
55
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
56
|
+
const count = this.accountManager.getAccountCount();
|
|
57
|
+
if (count > 1) {
|
|
58
|
+
showToast(`Rate limited (${account.email}). Switching account...`, 'warning');
|
|
59
|
+
return { shouldRetry: true, switchAccount: true };
|
|
60
|
+
}
|
|
61
|
+
showToast(`Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
|
|
62
|
+
await this.sleep(w);
|
|
63
|
+
return { shouldRetry: true };
|
|
64
|
+
}
|
|
65
|
+
if ((response.status === 402 || response.status === 403) &&
|
|
66
|
+
this.accountManager.getAccountCount() > 1) {
|
|
67
|
+
let errorReason = response.status === 402 ? 'Quota' : 'Forbidden';
|
|
68
|
+
let isPermanent = false;
|
|
69
|
+
try {
|
|
70
|
+
const errorBody = await response.text();
|
|
71
|
+
const errorData = JSON.parse(errorBody);
|
|
72
|
+
if (errorData.reason === 'INVALID_MODEL_ID') {
|
|
73
|
+
throw new Error(`Invalid model: ${errorData.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (errorData.reason === 'TEMPORARILY_SUSPENDED') {
|
|
76
|
+
errorReason = 'Account Suspended';
|
|
77
|
+
isPermanent = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof Error && e.message.includes('Invalid model')) {
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (isPermanent) {
|
|
86
|
+
account.failCount = 10;
|
|
87
|
+
}
|
|
88
|
+
this.accountManager.markUnhealthy(account, errorReason);
|
|
89
|
+
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
90
|
+
showToast(`${errorReason} (${account.email}). Switching account...`, 'warning');
|
|
91
|
+
return { shouldRetry: true, switchAccount: true };
|
|
92
|
+
}
|
|
93
|
+
return { shouldRetry: false };
|
|
94
|
+
}
|
|
95
|
+
async handleNetworkError(error, context, showToast) {
|
|
96
|
+
if (this.isNetworkError(error) && context.retry < this.config.rate_limit_max_retries) {
|
|
97
|
+
const d = this.config.rate_limit_retry_delay_ms * Math.pow(2, context.retry);
|
|
98
|
+
showToast(`Network error. Retrying in ${Math.ceil(d / 1000)}s...`, 'warning');
|
|
99
|
+
await this.sleep(d);
|
|
100
|
+
return {
|
|
101
|
+
shouldRetry: true,
|
|
102
|
+
newContext: { ...context, retry: context.retry + 1 }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { shouldRetry: false };
|
|
106
|
+
}
|
|
107
|
+
isNetworkError(e) {
|
|
108
|
+
return (e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message));
|
|
109
|
+
}
|
|
110
|
+
sleep(ms) {
|
|
111
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AccountRepository } from '../../infrastructure/database/account-repository';
|
|
2
|
+
import type { AccountManager } from '../../plugin/accounts';
|
|
3
|
+
import type { KiroConfig } from '../../plugin/config';
|
|
4
|
+
type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
|
|
5
|
+
export declare class RequestHandler {
|
|
6
|
+
private accountManager;
|
|
7
|
+
private config;
|
|
8
|
+
private repository;
|
|
9
|
+
private accountSelector;
|
|
10
|
+
private tokenRefresher;
|
|
11
|
+
private errorHandler;
|
|
12
|
+
private responseHandler;
|
|
13
|
+
private usageTracker;
|
|
14
|
+
private retryStrategy;
|
|
15
|
+
constructor(accountManager: AccountManager, config: KiroConfig, repository: AccountRepository);
|
|
16
|
+
handle(input: any, init: any, showToast: ToastFunction): Promise<Response>;
|
|
17
|
+
private handleKiroRequest;
|
|
18
|
+
private extractModel;
|
|
19
|
+
private prepareRequest;
|
|
20
|
+
private handleSuccessfulRequest;
|
|
21
|
+
private logRequest;
|
|
22
|
+
private logResponse;
|
|
23
|
+
private logError;
|
|
24
|
+
private allAccountsPermanentlyUnhealthy;
|
|
25
|
+
private sleep;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { isPermanentError } from '../../plugin/health';
|
|
2
|
+
import * as logger from '../../plugin/logger';
|
|
3
|
+
import { transformToCodeWhisperer } from '../../plugin/request';
|
|
4
|
+
import { syncFromKiroCli } from '../../plugin/sync/kiro-cli';
|
|
5
|
+
import { AccountSelector } from '../account/account-selector';
|
|
6
|
+
import { UsageTracker } from '../account/usage-tracker';
|
|
7
|
+
import { TokenRefresher } from '../auth/token-refresher';
|
|
8
|
+
import { ErrorHandler } from './error-handler';
|
|
9
|
+
import { ResponseHandler } from './response-handler';
|
|
10
|
+
import { RetryStrategy } from './retry-strategy';
|
|
11
|
+
const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
|
|
12
|
+
export class RequestHandler {
|
|
13
|
+
accountManager;
|
|
14
|
+
config;
|
|
15
|
+
repository;
|
|
16
|
+
accountSelector;
|
|
17
|
+
tokenRefresher;
|
|
18
|
+
errorHandler;
|
|
19
|
+
responseHandler;
|
|
20
|
+
usageTracker;
|
|
21
|
+
retryStrategy;
|
|
22
|
+
constructor(accountManager, config, repository) {
|
|
23
|
+
this.accountManager = accountManager;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.repository = repository;
|
|
26
|
+
this.accountSelector = new AccountSelector(accountManager, config, syncFromKiroCli, repository);
|
|
27
|
+
this.tokenRefresher = new TokenRefresher(config, accountManager, syncFromKiroCli, repository);
|
|
28
|
+
this.errorHandler = new ErrorHandler(config, accountManager, repository);
|
|
29
|
+
this.responseHandler = new ResponseHandler();
|
|
30
|
+
this.usageTracker = new UsageTracker(config, accountManager, repository);
|
|
31
|
+
this.retryStrategy = new RetryStrategy(config);
|
|
32
|
+
}
|
|
33
|
+
async handle(input, init, showToast) {
|
|
34
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
35
|
+
if (!KIRO_API_PATTERN.test(url)) {
|
|
36
|
+
return fetch(input, init);
|
|
37
|
+
}
|
|
38
|
+
return this.handleKiroRequest(url, init, showToast);
|
|
39
|
+
}
|
|
40
|
+
async handleKiroRequest(url, init, showToast) {
|
|
41
|
+
const body = init?.body ? JSON.parse(init.body) : {};
|
|
42
|
+
const model = this.extractModel(url) || body.model || 'claude-sonnet-4-5';
|
|
43
|
+
const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig;
|
|
44
|
+
const budget = body.providerOptions?.thinkingConfig?.thinkingBudget || 20000;
|
|
45
|
+
let reductionFactor = 1.0;
|
|
46
|
+
let retry = 0;
|
|
47
|
+
let consecutiveNullAccounts = 0;
|
|
48
|
+
const retryContext = this.retryStrategy.createContext();
|
|
49
|
+
while (true) {
|
|
50
|
+
const check = this.retryStrategy.shouldContinue(retryContext);
|
|
51
|
+
if (!check.canContinue) {
|
|
52
|
+
throw new Error(check.error);
|
|
53
|
+
}
|
|
54
|
+
if (this.allAccountsPermanentlyUnhealthy()) {
|
|
55
|
+
throw new Error('All accounts are permanently unhealthy (quota exceeded or suspended)');
|
|
56
|
+
}
|
|
57
|
+
let acc = await this.accountSelector.selectHealthyAccount(showToast);
|
|
58
|
+
if (!acc) {
|
|
59
|
+
consecutiveNullAccounts++;
|
|
60
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, consecutiveNullAccounts - 1), 10000);
|
|
61
|
+
await this.sleep(backoffDelay);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
consecutiveNullAccounts = 0;
|
|
65
|
+
const auth = this.accountManager.toAuthDetails(acc);
|
|
66
|
+
const tokenResult = await this.tokenRefresher.refreshIfNeeded(acc, auth, showToast);
|
|
67
|
+
if (tokenResult.shouldContinue) {
|
|
68
|
+
acc = tokenResult.account;
|
|
69
|
+
await this.sleep(500);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const prep = this.prepareRequest(url, init?.body, model, auth, think, budget, reductionFactor);
|
|
73
|
+
const apiTimestamp = this.config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
74
|
+
if (apiTimestamp) {
|
|
75
|
+
this.logRequest(prep, acc, apiTimestamp);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(prep.url, prep.init);
|
|
79
|
+
if (apiTimestamp) {
|
|
80
|
+
this.logResponse(res, prep, apiTimestamp);
|
|
81
|
+
}
|
|
82
|
+
if (res.ok) {
|
|
83
|
+
this.handleSuccessfulRequest(acc);
|
|
84
|
+
this.usageTracker.syncUsage(acc, auth);
|
|
85
|
+
return await this.responseHandler.handleSuccess(res, model, prep.conversationId, prep.streaming);
|
|
86
|
+
}
|
|
87
|
+
const errorResult = await this.errorHandler.handle(null, res, acc, { reductionFactor, retry }, showToast);
|
|
88
|
+
if (errorResult.shouldRetry) {
|
|
89
|
+
if (errorResult.newContext) {
|
|
90
|
+
reductionFactor = errorResult.newContext.reductionFactor;
|
|
91
|
+
retry = errorResult.newContext.retry;
|
|
92
|
+
}
|
|
93
|
+
if (errorResult.switchAccount) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
this.logError(prep, res, acc, apiTimestamp);
|
|
99
|
+
throw new Error(`Kiro Error: ${res.status}`);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
const networkResult = await this.errorHandler.handleNetworkError(e, { reductionFactor, retry }, showToast);
|
|
103
|
+
if (networkResult.shouldRetry) {
|
|
104
|
+
if (networkResult.newContext) {
|
|
105
|
+
retry = networkResult.newContext.retry;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
extractModel(url) {
|
|
114
|
+
return url.match(/models\/([^/:]+)/)?.[1] || null;
|
|
115
|
+
}
|
|
116
|
+
prepareRequest(url, body, model, auth, think, budget, reductionFactor) {
|
|
117
|
+
return transformToCodeWhisperer(url, body, model, auth, think, budget, reductionFactor);
|
|
118
|
+
}
|
|
119
|
+
handleSuccessfulRequest(acc) {
|
|
120
|
+
if (acc.failCount && acc.failCount > 0) {
|
|
121
|
+
if (!isPermanentError(acc.unhealthyReason)) {
|
|
122
|
+
acc.failCount = 0;
|
|
123
|
+
acc.isHealthy = true;
|
|
124
|
+
delete acc.unhealthyReason;
|
|
125
|
+
delete acc.recoveryTime;
|
|
126
|
+
this.repository.save(acc).catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
logRequest(prep, acc, timestamp) {
|
|
131
|
+
let b = null;
|
|
132
|
+
try {
|
|
133
|
+
b = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
logger.logApiRequest({
|
|
137
|
+
url: prep.url,
|
|
138
|
+
method: prep.init.method,
|
|
139
|
+
headers: prep.init.headers,
|
|
140
|
+
body: b,
|
|
141
|
+
conversationId: prep.conversationId,
|
|
142
|
+
model: prep.effectiveModel,
|
|
143
|
+
email: acc.email
|
|
144
|
+
}, timestamp);
|
|
145
|
+
}
|
|
146
|
+
logResponse(res, prep, timestamp) {
|
|
147
|
+
const h = {};
|
|
148
|
+
res.headers.forEach((v, k) => {
|
|
149
|
+
h[k] = v;
|
|
150
|
+
});
|
|
151
|
+
logger.logApiResponse({
|
|
152
|
+
status: res.status,
|
|
153
|
+
statusText: res.statusText,
|
|
154
|
+
headers: h,
|
|
155
|
+
conversationId: prep.conversationId,
|
|
156
|
+
model: prep.effectiveModel
|
|
157
|
+
}, timestamp);
|
|
158
|
+
}
|
|
159
|
+
logError(prep, res, acc, apiTimestamp) {
|
|
160
|
+
const h = {};
|
|
161
|
+
res.headers.forEach((v, k) => {
|
|
162
|
+
h[k] = v;
|
|
163
|
+
});
|
|
164
|
+
const rData = {
|
|
165
|
+
status: res.status,
|
|
166
|
+
statusText: res.statusText,
|
|
167
|
+
headers: h,
|
|
168
|
+
error: `Kiro Error: ${res.status}`,
|
|
169
|
+
conversationId: prep.conversationId,
|
|
170
|
+
model: prep.effectiveModel
|
|
171
|
+
};
|
|
172
|
+
let lastB = null;
|
|
173
|
+
try {
|
|
174
|
+
lastB = prep.init.body ? JSON.parse(prep.init.body) : null;
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
if (!this.config.enable_log_api_request) {
|
|
178
|
+
logger.logApiError({
|
|
179
|
+
url: prep.url,
|
|
180
|
+
method: prep.init.method,
|
|
181
|
+
headers: prep.init.headers,
|
|
182
|
+
body: lastB,
|
|
183
|
+
conversationId: prep.conversationId,
|
|
184
|
+
model: prep.effectiveModel,
|
|
185
|
+
email: acc.email
|
|
186
|
+
}, rData, logger.getTimestamp());
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
allAccountsPermanentlyUnhealthy() {
|
|
190
|
+
const accounts = this.accountManager.getAccounts();
|
|
191
|
+
if (accounts.length === 0) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return accounts.every((acc) => !acc.isHealthy && isPermanentError(acc.unhealthyReason));
|
|
195
|
+
}
|
|
196
|
+
sleep(ms) {
|
|
197
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
198
|
+
}
|
|
199
|
+
}
|