@zhafron/opencode-kiro-auth 1.7.0 → 1.8.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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessTokenExpired } from '../../kiro/auth';
|
|
2
2
|
import { KiroTokenRefreshError } from '../../plugin/errors';
|
|
3
|
+
import * as logger from '../../plugin/logger';
|
|
3
4
|
import { refreshAccessToken } from '../../plugin/token';
|
|
4
5
|
export class TokenRefresher {
|
|
5
6
|
config;
|
|
@@ -27,6 +28,11 @@ export class TokenRefresher {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
async handleRefreshError(error, account, showToast) {
|
|
31
|
+
logger.error('Token refresh failed', {
|
|
32
|
+
email: account.email,
|
|
33
|
+
code: error instanceof KiroTokenRefreshError ? error.code : undefined,
|
|
34
|
+
message: error instanceof Error ? error.message : String(error)
|
|
35
|
+
});
|
|
30
36
|
if (this.config.auto_sync_kiro_cli) {
|
|
31
37
|
await this.syncFromKiroCli();
|
|
32
38
|
}
|
|
@@ -48,6 +54,11 @@ export class TokenRefresher {
|
|
|
48
54
|
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
49
55
|
return { account, shouldContinue: true };
|
|
50
56
|
}
|
|
57
|
+
logger.error('Token refresh unrecoverable', {
|
|
58
|
+
email: account.email,
|
|
59
|
+
code: error instanceof KiroTokenRefreshError ? error.code : undefined,
|
|
60
|
+
message: error instanceof Error ? error.message : String(error)
|
|
61
|
+
});
|
|
51
62
|
throw error;
|
|
52
63
|
}
|
|
53
64
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as logger from '../../plugin/logger';
|
|
1
2
|
export class ErrorHandler {
|
|
2
3
|
config;
|
|
3
4
|
accountManager;
|
|
@@ -8,13 +9,32 @@ export class ErrorHandler {
|
|
|
8
9
|
this.repository = repository;
|
|
9
10
|
}
|
|
10
11
|
async handle(error, response, account, context, showToast) {
|
|
11
|
-
if (response.status === 400
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
if (response.status === 400) {
|
|
13
|
+
const body = await response.text();
|
|
14
|
+
const errorData = (() => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(body);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
22
|
+
if (errorData?.reason === 'INVALID_MODEL_ID') {
|
|
23
|
+
throw new Error(`Invalid model: ${errorData.message}`);
|
|
24
|
+
}
|
|
25
|
+
logger.warn('HTTP 400 response body', {
|
|
26
|
+
body,
|
|
27
|
+
reductionFactor: context.reductionFactor,
|
|
28
|
+
email: account.email
|
|
29
|
+
});
|
|
30
|
+
if (context.reductionFactor > 0.4) {
|
|
31
|
+
const newFactor = context.reductionFactor - 0.2;
|
|
32
|
+
showToast(`Context too long. Retrying with ${Math.round(newFactor * 100)}%...`, 'warning');
|
|
33
|
+
return {
|
|
34
|
+
shouldRetry: true,
|
|
35
|
+
newContext: { ...context, reductionFactor: newFactor }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
18
38
|
}
|
|
19
39
|
if (response.status === 401 && context.retry < this.config.rate_limit_max_retries) {
|
|
20
40
|
return {
|
|
@@ -65,21 +85,21 @@ export class ErrorHandler {
|
|
|
65
85
|
this.accountManager.getAccountCount() > 1) {
|
|
66
86
|
let errorReason = response.status === 402 ? 'Quota' : 'Forbidden';
|
|
67
87
|
let isPermanent = false;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error(`Invalid model: ${errorData.message}`);
|
|
88
|
+
const errorBody = await response.text();
|
|
89
|
+
const errorData = (() => {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(errorBody);
|
|
73
92
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
isPermanent = true;
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
77
95
|
}
|
|
96
|
+
})();
|
|
97
|
+
if (errorData?.reason === 'INVALID_MODEL_ID') {
|
|
98
|
+
throw new Error(`Invalid model: ${errorData.message}`);
|
|
78
99
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
100
|
+
if (errorData?.reason === 'TEMPORARILY_SUSPENDED') {
|
|
101
|
+
errorReason = 'Account Suspended';
|
|
102
|
+
isPermanent = true;
|
|
83
103
|
}
|
|
84
104
|
if (isPermanent) {
|
|
85
105
|
account.failCount = 10;
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -131,7 +131,11 @@ export class AccountManager {
|
|
|
131
131
|
delete a.unhealthyReason;
|
|
132
132
|
delete a.recoveryTime;
|
|
133
133
|
}
|
|
134
|
-
kiroDb.upsertAccount(a).catch(() => {
|
|
134
|
+
kiroDb.upsertAccount(a).catch((e) => logger.warn('DB write failed', {
|
|
135
|
+
method: 'updateUsage',
|
|
136
|
+
email: a.email,
|
|
137
|
+
error: e instanceof Error ? e.message : String(e)
|
|
138
|
+
}));
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
addAccount(a) {
|
|
@@ -140,14 +144,22 @@ export class AccountManager {
|
|
|
140
144
|
this.accounts.push(a);
|
|
141
145
|
else
|
|
142
146
|
this.accounts[i] = a;
|
|
143
|
-
kiroDb.upsertAccount(a).catch(() => {
|
|
147
|
+
kiroDb.upsertAccount(a).catch((e) => logger.warn('DB write failed', {
|
|
148
|
+
method: 'addAccount',
|
|
149
|
+
email: a.email,
|
|
150
|
+
error: e instanceof Error ? e.message : String(e)
|
|
151
|
+
}));
|
|
144
152
|
}
|
|
145
153
|
removeAccount(a) {
|
|
146
154
|
const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
|
|
147
155
|
if (removedIndex === -1)
|
|
148
156
|
return;
|
|
149
157
|
this.accounts = this.accounts.filter((x) => x.id !== a.id);
|
|
150
|
-
kiroDb.deleteAccount(a.id).catch(() => {
|
|
158
|
+
kiroDb.deleteAccount(a.id).catch((e) => logger.warn('DB write failed', {
|
|
159
|
+
method: 'removeAccount',
|
|
160
|
+
email: a.email,
|
|
161
|
+
error: e instanceof Error ? e.message : String(e)
|
|
162
|
+
}));
|
|
151
163
|
if (this.accounts.length === 0)
|
|
152
164
|
this.cursor = 0;
|
|
153
165
|
else if (this.cursor >= this.accounts.length)
|
|
@@ -173,15 +185,27 @@ export class AccountManager {
|
|
|
173
185
|
acc.isHealthy = true;
|
|
174
186
|
delete acc.unhealthyReason;
|
|
175
187
|
delete acc.recoveryTime;
|
|
176
|
-
kiroDb.upsertAccount(acc).catch(() => {
|
|
177
|
-
|
|
188
|
+
kiroDb.upsertAccount(acc).catch((e) => logger.warn('DB write failed', {
|
|
189
|
+
method: 'updateFromAuth',
|
|
190
|
+
email: acc.email,
|
|
191
|
+
error: e instanceof Error ? e.message : String(e)
|
|
192
|
+
}));
|
|
193
|
+
writeToKiroCli(acc).catch((e) => logger.warn('CLI write failed', {
|
|
194
|
+
method: 'updateFromAuth',
|
|
195
|
+
email: acc.email,
|
|
196
|
+
error: e instanceof Error ? e.message : String(e)
|
|
197
|
+
}));
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
200
|
markRateLimited(a, ms) {
|
|
181
201
|
const acc = this.accounts.find((x) => x.id === a.id);
|
|
182
202
|
if (acc) {
|
|
183
203
|
acc.rateLimitResetTime = Date.now() + ms;
|
|
184
|
-
kiroDb.upsertAccount(acc).catch(() => {
|
|
204
|
+
kiroDb.upsertAccount(acc).catch((e) => logger.warn('DB write failed', {
|
|
205
|
+
method: 'markRateLimited',
|
|
206
|
+
email: acc.email,
|
|
207
|
+
error: e instanceof Error ? e.message : String(e)
|
|
208
|
+
}));
|
|
185
209
|
}
|
|
186
210
|
}
|
|
187
211
|
markUnhealthy(a, reason, recovery) {
|
|
@@ -209,7 +233,11 @@ export class AccountManager {
|
|
|
209
233
|
acc.recoveryTime = recovery || Date.now() + 3600000;
|
|
210
234
|
}
|
|
211
235
|
}
|
|
212
|
-
kiroDb.upsertAccount(acc).catch(() => {
|
|
236
|
+
kiroDb.upsertAccount(acc).catch((e) => logger.warn('DB write failed', {
|
|
237
|
+
method: 'markUnhealthy',
|
|
238
|
+
email: acc.email,
|
|
239
|
+
error: e instanceof Error ? e.message : String(e)
|
|
240
|
+
}));
|
|
213
241
|
}
|
|
214
242
|
async saveToDisk() {
|
|
215
243
|
await kiroDb.batchUpsertAccounts(this.accounts);
|
package/dist/plugin/request.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import * as os from 'os';
|
|
3
|
-
import { KIRO_CONSTANTS } from '../constants.js';
|
|
3
|
+
import { KIRO_CONSTANTS, buildUrl, extractRegionFromArn } from '../constants.js';
|
|
4
4
|
import { buildHistory, extractToolNamesFromHistory, historyHasToolCalling, injectSystemPrompt, truncateHistory } from '../infrastructure/transformers/history-builder.js';
|
|
5
5
|
import { findOriginalToolCall, getContentText, mergeAdjacentMessages, truncate } from '../infrastructure/transformers/message-transformer.js';
|
|
6
6
|
import { convertToolsToCodeWhisperer, deduplicateToolResults } from '../infrastructure/transformers/tool-transformer.js';
|
|
@@ -221,7 +221,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
|
|
|
221
221
|
const osN = osP === 'win32' ? `windows#${osR}` : osP === 'darwin' ? `macos#${osR}` : `${osP}#${osR}`;
|
|
222
222
|
const ua = `aws-sdk-js/3.738.0 ua/2.1 os/${osN} lang/js md/nodejs#${nodeV} api/codewhisperer#3.738.0 m/E KiroIDE`;
|
|
223
223
|
return {
|
|
224
|
-
url: KIRO_CONSTANTS.BASE_URL
|
|
224
|
+
url: buildUrl(KIRO_CONSTANTS.BASE_URL, extractRegionFromArn(auth.profileArn) ?? auth.region),
|
|
225
225
|
init: {
|
|
226
226
|
method: 'POST',
|
|
227
227
|
headers: {
|
|
@@ -4,6 +4,7 @@ export function runMigrations(db) {
|
|
|
4
4
|
migrateUsageTable(db);
|
|
5
5
|
migrateStartUrlColumn(db);
|
|
6
6
|
migrateOidcRegionColumn(db);
|
|
7
|
+
migrateDropRefreshTokenUniqueIndex(db);
|
|
7
8
|
}
|
|
8
9
|
function migrateToUniqueRefreshToken(db) {
|
|
9
10
|
const hasIndex = db
|
|
@@ -126,3 +127,29 @@ function migrateOidcRegionColumn(db) {
|
|
|
126
127
|
// Backfill: historically `region` was used for both service + OIDC.
|
|
127
128
|
db.run('UPDATE accounts SET oidc_region = region WHERE oidc_region IS NULL OR oidc_region = \"\"');
|
|
128
129
|
}
|
|
130
|
+
function migrateDropRefreshTokenUniqueIndex(db) {
|
|
131
|
+
// Drop the UNIQUE index on refresh_token — it was only needed for ON CONFLICT(refresh_token)
|
|
132
|
+
// upsert mechanics. Now that we use ON CONFLICT(id), this index is unnecessary and actively
|
|
133
|
+
// harmful: duplicate rows (same account, different legacy vs hash id) share the same
|
|
134
|
+
// refresh_token, causing UNIQUE constraint violations on every upsert.
|
|
135
|
+
db.run('DROP INDEX IF EXISTS idx_refresh_token_unique');
|
|
136
|
+
// Clean up duplicate rows: same email + same refresh_token but different ids.
|
|
137
|
+
// Keep the deterministic hash id (64-char hex), delete legacy kiro-cli-sync-* rows.
|
|
138
|
+
const duplicates = db
|
|
139
|
+
.prepare(`SELECT email, refresh_token FROM accounts
|
|
140
|
+
GROUP BY email, refresh_token
|
|
141
|
+
HAVING COUNT(*) > 1`)
|
|
142
|
+
.all();
|
|
143
|
+
for (const dup of duplicates) {
|
|
144
|
+
const rows = db
|
|
145
|
+
.prepare(`SELECT id FROM accounts WHERE email = ? AND refresh_token = ?
|
|
146
|
+
ORDER BY
|
|
147
|
+
CASE WHEN id LIKE 'kiro-cli-sync-%' THEN 1 ELSE 0 END ASC,
|
|
148
|
+
last_used DESC, expires_at DESC`)
|
|
149
|
+
.all(dup.email, dup.refresh_token);
|
|
150
|
+
// Keep the first row (deterministic hash id preferred), delete the rest
|
|
151
|
+
for (const row of rows.slice(1)) {
|
|
152
|
+
db.prepare('DELETE FROM accounts WHERE id = ?').run(row.id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -50,10 +50,10 @@ export class KiroDatabase {
|
|
|
50
50
|
is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
|
|
51
51
|
used_count, limit_count, last_sync
|
|
52
52
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
53
|
-
ON CONFLICT(
|
|
53
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
54
54
|
id=excluded.id, email=excluded.email, auth_method=excluded.auth_method,
|
|
55
55
|
region=excluded.region, oidc_region=excluded.oidc_region, client_id=excluded.client_id, client_secret=excluded.client_secret,
|
|
56
|
-
profile_arn=excluded.profile_arn, start_url=excluded.start_url,
|
|
56
|
+
profile_arn=excluded.profile_arn, start_url=excluded.start_url, refresh_token=excluded.refresh_token,
|
|
57
57
|
access_token=excluded.access_token, expires_at=excluded.expires_at,
|
|
58
58
|
rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
|
|
59
59
|
unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
|