@zhafron/opencode-kiro-auth 1.6.6 → 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.
- package/README.md +53 -8
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +13 -0
- package/dist/core/account/account-selector.js +0 -9
- package/dist/core/auth/auth-handler.js +25 -1
- package/dist/core/auth/idc-auth-method.d.ts +2 -3
- package/dist/core/auth/idc-auth-method.js +106 -167
- package/dist/core/auth/token-refresher.js +11 -0
- package/dist/core/request/error-handler.js +39 -21
- package/dist/infrastructure/database/account-repository.js +1 -0
- package/dist/kiro/oauth-idc.js +20 -6
- package/dist/plugin/accounts.js +37 -7
- package/dist/plugin/config/schema.d.ts +6 -0
- package/dist/plugin/config/schema.js +2 -0
- package/dist/plugin/logger.js +16 -1
- package/dist/plugin/request.js +4 -2
- package/dist/plugin/storage/migrations.js +41 -3
- package/dist/plugin/storage/sqlite.js +8 -7
- package/dist/plugin/sync/kiro-cli-profile.d.ts +1 -0
- package/dist/plugin/sync/kiro-cli-profile.js +30 -0
- package/dist/plugin/sync/kiro-cli.js +36 -9
- package/dist/plugin/token.js +4 -1
- package/dist/plugin/types.d.ts +2 -0
- package/dist/plugin/usage.js +19 -4
- package/package.json +1 -1
- package/dist/plugin/cli.d.ts +0 -8
- package/dist/plugin/cli.js +0 -103
- package/dist/plugin/server.d.ts +0 -34
- package/dist/plugin/server.js +0 -362
package/dist/kiro/oauth-idc.js
CHANGED
|
@@ -95,7 +95,16 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
|
|
|
95
95
|
grantType: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
96
96
|
})
|
|
97
97
|
});
|
|
98
|
-
const
|
|
98
|
+
const responseText = await tokenResponse.text().catch(() => '');
|
|
99
|
+
let tokenData = {};
|
|
100
|
+
if (responseText) {
|
|
101
|
+
try {
|
|
102
|
+
tokenData = JSON.parse(responseText);
|
|
103
|
+
}
|
|
104
|
+
catch (parseError) {
|
|
105
|
+
throw new Error(`Token polling failed: invalid JSON response (HTTP ${tokenResponse.status}): ${responseText.slice(0, 300)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
99
108
|
if (tokenData.error) {
|
|
100
109
|
const errorType = tokenData.error;
|
|
101
110
|
if (errorType === 'authorization_pending') {
|
|
@@ -116,12 +125,15 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
|
|
|
116
125
|
const error = new Error(`Token polling failed: ${errorType} - ${tokenData.error_description || ''}`);
|
|
117
126
|
throw error;
|
|
118
127
|
}
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
const accessToken = tokenData.access_token || tokenData.accessToken;
|
|
129
|
+
const refreshToken = tokenData.refresh_token || tokenData.refreshToken;
|
|
130
|
+
const tokenExpiresIn = tokenData.expires_in || tokenData.expiresIn;
|
|
131
|
+
if (accessToken && refreshToken) {
|
|
132
|
+
const expiresInSeconds = tokenExpiresIn || 3600;
|
|
121
133
|
const expiresAt = Date.now() + expiresInSeconds * 1000;
|
|
122
134
|
return {
|
|
123
|
-
refreshToken
|
|
124
|
-
accessToken
|
|
135
|
+
refreshToken,
|
|
136
|
+
accessToken,
|
|
125
137
|
expiresAt,
|
|
126
138
|
email: 'builder-id@aws.amazon.com',
|
|
127
139
|
clientId,
|
|
@@ -131,9 +143,11 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
|
|
|
131
143
|
};
|
|
132
144
|
}
|
|
133
145
|
if (!tokenResponse.ok) {
|
|
134
|
-
const error = new Error(`Token request failed with status: ${tokenResponse.status}`);
|
|
146
|
+
const error = new Error(`Token request failed with status: ${tokenResponse.status} ${responseText ? `(${responseText.slice(0, 200)})` : ''}`);
|
|
135
147
|
throw error;
|
|
136
148
|
}
|
|
149
|
+
// If the service returned HTTP 200 but no tokens and no error, treat as invalid response.
|
|
150
|
+
throw new Error(`Token polling failed: missing tokens in response: ${responseText ? responseText.slice(0, 300) : '[empty]'}`);
|
|
137
151
|
}
|
|
138
152
|
catch (error) {
|
|
139
153
|
if (error instanceof Error &&
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -27,6 +27,7 @@ export class AccountManager {
|
|
|
27
27
|
email: r.email,
|
|
28
28
|
authMethod: r.auth_method,
|
|
29
29
|
region: r.region,
|
|
30
|
+
oidcRegion: r.oidc_region || undefined,
|
|
30
31
|
clientId: r.client_id,
|
|
31
32
|
clientSecret: r.client_secret,
|
|
32
33
|
profileArn: r.profile_arn,
|
|
@@ -130,7 +131,11 @@ export class AccountManager {
|
|
|
130
131
|
delete a.unhealthyReason;
|
|
131
132
|
delete a.recoveryTime;
|
|
132
133
|
}
|
|
133
|
-
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
|
+
}));
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
addAccount(a) {
|
|
@@ -139,14 +144,22 @@ export class AccountManager {
|
|
|
139
144
|
this.accounts.push(a);
|
|
140
145
|
else
|
|
141
146
|
this.accounts[i] = a;
|
|
142
|
-
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
|
+
}));
|
|
143
152
|
}
|
|
144
153
|
removeAccount(a) {
|
|
145
154
|
const removedIndex = this.accounts.findIndex((x) => x.id === a.id);
|
|
146
155
|
if (removedIndex === -1)
|
|
147
156
|
return;
|
|
148
157
|
this.accounts = this.accounts.filter((x) => x.id !== a.id);
|
|
149
|
-
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
|
+
}));
|
|
150
163
|
if (this.accounts.length === 0)
|
|
151
164
|
this.cursor = 0;
|
|
152
165
|
else if (this.cursor >= this.accounts.length)
|
|
@@ -172,15 +185,27 @@ export class AccountManager {
|
|
|
172
185
|
acc.isHealthy = true;
|
|
173
186
|
delete acc.unhealthyReason;
|
|
174
187
|
delete acc.recoveryTime;
|
|
175
|
-
kiroDb.upsertAccount(acc).catch(() => {
|
|
176
|
-
|
|
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
|
+
}));
|
|
177
198
|
}
|
|
178
199
|
}
|
|
179
200
|
markRateLimited(a, ms) {
|
|
180
201
|
const acc = this.accounts.find((x) => x.id === a.id);
|
|
181
202
|
if (acc) {
|
|
182
203
|
acc.rateLimitResetTime = Date.now() + ms;
|
|
183
|
-
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
|
+
}));
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
markUnhealthy(a, reason, recovery) {
|
|
@@ -208,7 +233,11 @@ export class AccountManager {
|
|
|
208
233
|
acc.recoveryTime = recovery || Date.now() + 3600000;
|
|
209
234
|
}
|
|
210
235
|
}
|
|
211
|
-
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
|
+
}));
|
|
212
241
|
}
|
|
213
242
|
async saveToDisk() {
|
|
214
243
|
await kiroDb.batchUpsertAccounts(this.accounts);
|
|
@@ -227,6 +256,7 @@ export class AccountManager {
|
|
|
227
256
|
expires: a.expiresAt,
|
|
228
257
|
authMethod: a.authMethod,
|
|
229
258
|
region: a.region,
|
|
259
|
+
oidcRegion: a.oidcRegion,
|
|
230
260
|
profileArn: a.profileArn,
|
|
231
261
|
clientId: a.clientId,
|
|
232
262
|
clientSecret: a.clientSecret,
|
|
@@ -6,6 +6,8 @@ export type Region = z.infer<typeof RegionSchema>;
|
|
|
6
6
|
export declare const KiroConfigSchema: z.ZodObject<{
|
|
7
7
|
$schema: z.ZodOptional<z.ZodString>;
|
|
8
8
|
idc_start_url: z.ZodOptional<z.ZodString>;
|
|
9
|
+
idc_region: z.ZodOptional<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
|
|
10
|
+
idc_profile_arn: z.ZodOptional<z.ZodString>;
|
|
9
11
|
account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin", "lowest-usage"]>>;
|
|
10
12
|
default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
|
|
11
13
|
rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
@@ -35,9 +37,13 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
35
37
|
enable_log_api_request: boolean;
|
|
36
38
|
$schema?: string | undefined;
|
|
37
39
|
idc_start_url?: string | undefined;
|
|
40
|
+
idc_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
|
|
41
|
+
idc_profile_arn?: string | undefined;
|
|
38
42
|
}, {
|
|
39
43
|
$schema?: string | undefined;
|
|
40
44
|
idc_start_url?: string | undefined;
|
|
45
|
+
idc_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
|
|
46
|
+
idc_profile_arn?: string | undefined;
|
|
41
47
|
account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
|
|
42
48
|
default_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
|
|
43
49
|
rate_limit_retry_delay_ms?: number | undefined;
|
|
@@ -39,6 +39,8 @@ export const RegionSchema = z.enum([
|
|
|
39
39
|
export const KiroConfigSchema = z.object({
|
|
40
40
|
$schema: z.string().optional(),
|
|
41
41
|
idc_start_url: z.string().url().optional(),
|
|
42
|
+
idc_region: RegionSchema.optional(),
|
|
43
|
+
idc_profile_arn: z.string().optional(),
|
|
42
44
|
account_selection_strategy: AccountSelectionStrategySchema.default('lowest-usage'),
|
|
43
45
|
default_region: RegionSchema.default('us-east-1'),
|
|
44
46
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
package/dist/plugin/logger.js
CHANGED
|
@@ -14,7 +14,22 @@ const writeToFile = (level, message, ...args) => {
|
|
|
14
14
|
mkdirSync(dir, { recursive: true });
|
|
15
15
|
const path = join(dir, 'plugin.log');
|
|
16
16
|
const timestamp = new Date().toISOString();
|
|
17
|
-
const content = `[${timestamp}] ${level}: ${message} ${args
|
|
17
|
+
const content = `[${timestamp}] ${level}: ${message} ${args
|
|
18
|
+
.map((a) => {
|
|
19
|
+
if (a instanceof Error) {
|
|
20
|
+
return `${a.name}: ${a.message}${a.stack ? `\n${a.stack}` : ''}`;
|
|
21
|
+
}
|
|
22
|
+
if (typeof a === 'object') {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(a);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return '[Unserializable object]';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return String(a);
|
|
31
|
+
})
|
|
32
|
+
.join(' ')}\n`;
|
|
18
33
|
appendFileSync(path, content);
|
|
19
34
|
}
|
|
20
35
|
catch (e) { }
|
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';
|
|
@@ -135,6 +135,8 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
|
+
if (auth.profileArn)
|
|
139
|
+
request.profileArn = auth.profileArn;
|
|
138
140
|
const toolUsesInHistory = history.flatMap((h) => h.assistantResponseMessage?.toolUses || []);
|
|
139
141
|
const allToolUseIdsInHistory = new Set(toolUsesInHistory.map((tu) => tu.toolUseId));
|
|
140
142
|
const finalCurTrs = [];
|
|
@@ -219,7 +221,7 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
|
|
|
219
221
|
const osN = osP === 'win32' ? `windows#${osR}` : osP === 'darwin' ? `macos#${osR}` : `${osP}#${osR}`;
|
|
220
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`;
|
|
221
223
|
return {
|
|
222
|
-
url: KIRO_CONSTANTS.BASE_URL
|
|
224
|
+
url: buildUrl(KIRO_CONSTANTS.BASE_URL, extractRegionFromArn(auth.profileArn) ?? auth.region),
|
|
223
225
|
init: {
|
|
224
226
|
method: 'POST',
|
|
225
227
|
headers: {
|
|
@@ -3,6 +3,8 @@ export function runMigrations(db) {
|
|
|
3
3
|
migrateRealEmailColumn(db);
|
|
4
4
|
migrateUsageTable(db);
|
|
5
5
|
migrateStartUrlColumn(db);
|
|
6
|
+
migrateOidcRegionColumn(db);
|
|
7
|
+
migrateDropRefreshTokenUniqueIndex(db);
|
|
6
8
|
}
|
|
7
9
|
function migrateToUniqueRefreshToken(db) {
|
|
8
10
|
const hasIndex = db
|
|
@@ -62,7 +64,8 @@ function migrateRealEmailColumn(db) {
|
|
|
62
64
|
db.run(`
|
|
63
65
|
CREATE TABLE accounts_new (
|
|
64
66
|
id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
|
|
65
|
-
region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
67
|
+
region TEXT NOT NULL, oidc_region TEXT, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
68
|
+
start_url TEXT,
|
|
66
69
|
refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
67
70
|
rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
|
|
68
71
|
recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
|
|
@@ -70,8 +73,8 @@ function migrateRealEmailColumn(db) {
|
|
|
70
73
|
)
|
|
71
74
|
`);
|
|
72
75
|
db.run(`
|
|
73
|
-
INSERT INTO accounts_new (id, email, auth_method, region, client_id, client_secret, profile_arn, refresh_token, access_token, expires_at, rate_limit_reset, is_healthy, unhealthy_reason, recovery_time, fail_count, last_used, used_count, limit_count, last_sync)
|
|
74
|
-
SELECT id, email, auth_method, region, client_id, client_secret, profile_arn, refresh_token, access_token, expires_at, COALESCE(rate_limit_reset, 0), COALESCE(is_healthy, 1), unhealthy_reason, recovery_time, COALESCE(fail_count, 0), COALESCE(last_used, 0), 0, 0, 0 FROM accounts
|
|
76
|
+
INSERT INTO accounts_new (id, email, auth_method, region, oidc_region, client_id, client_secret, profile_arn, start_url, refresh_token, access_token, expires_at, rate_limit_reset, is_healthy, unhealthy_reason, recovery_time, fail_count, last_used, used_count, limit_count, last_sync)
|
|
77
|
+
SELECT id, email, auth_method, region, NULL, client_id, client_secret, profile_arn, NULL, refresh_token, access_token, expires_at, COALESCE(rate_limit_reset, 0), COALESCE(is_healthy, 1), unhealthy_reason, recovery_time, COALESCE(fail_count, 0), COALESCE(last_used, 0), 0, 0, 0 FROM accounts
|
|
75
78
|
`);
|
|
76
79
|
db.run('DROP TABLE accounts');
|
|
77
80
|
db.run('ALTER TABLE accounts_new RENAME TO accounts');
|
|
@@ -115,3 +118,38 @@ function migrateStartUrlColumn(db) {
|
|
|
115
118
|
db.run('ALTER TABLE accounts ADD COLUMN start_url TEXT');
|
|
116
119
|
}
|
|
117
120
|
}
|
|
121
|
+
function migrateOidcRegionColumn(db) {
|
|
122
|
+
const columns = db.prepare('PRAGMA table_info(accounts)').all();
|
|
123
|
+
const names = new Set(columns.map((c) => c.name));
|
|
124
|
+
if (!names.has('oidc_region')) {
|
|
125
|
+
db.run('ALTER TABLE accounts ADD COLUMN oidc_region TEXT');
|
|
126
|
+
}
|
|
127
|
+
// Backfill: historically `region` was used for both service + OIDC.
|
|
128
|
+
db.run('UPDATE accounts SET oidc_region = region WHERE oidc_region IS NULL OR oidc_region = \"\"');
|
|
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
|
+
}
|
|
@@ -28,7 +28,7 @@ export class KiroDatabase {
|
|
|
28
28
|
this.db.run(`
|
|
29
29
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
30
30
|
id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
|
|
31
|
-
region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
31
|
+
region TEXT NOT NULL, oidc_region TEXT, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
32
32
|
start_url TEXT,
|
|
33
33
|
refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
34
34
|
rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
|
|
@@ -45,22 +45,22 @@ export class KiroDatabase {
|
|
|
45
45
|
this.db
|
|
46
46
|
.prepare(`
|
|
47
47
|
INSERT INTO accounts (
|
|
48
|
-
id, email, auth_method, region, client_id, client_secret,
|
|
48
|
+
id, email, auth_method, region, oidc_region, client_id, client_secret,
|
|
49
49
|
profile_arn, start_url, refresh_token, access_token, expires_at, rate_limit_reset,
|
|
50
50
|
is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
|
|
51
51
|
used_count, limit_count, last_sync
|
|
52
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
53
|
-
ON CONFLICT(
|
|
52
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
53
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
54
54
|
id=excluded.id, email=excluded.email, auth_method=excluded.auth_method,
|
|
55
|
-
region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
|
|
56
|
-
profile_arn=excluded.profile_arn, start_url=excluded.start_url,
|
|
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, 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,
|
|
60
60
|
fail_count=excluded.fail_count, last_used=excluded.last_used,
|
|
61
61
|
used_count=excluded.used_count, limit_count=excluded.limit_count, last_sync=excluded.last_sync
|
|
62
62
|
`)
|
|
63
|
-
.run(acc.id, acc.email, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
|
|
63
|
+
.run(acc.id, acc.email, acc.authMethod, acc.region, acc.oidcRegion || null, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
|
|
64
64
|
}
|
|
65
65
|
async upsertAccount(acc) {
|
|
66
66
|
await withDatabaseLock(this.path, async () => {
|
|
@@ -109,6 +109,7 @@ export class KiroDatabase {
|
|
|
109
109
|
email: row.email,
|
|
110
110
|
authMethod: row.auth_method,
|
|
111
111
|
region: row.region,
|
|
112
|
+
oidcRegion: row.oidc_region || undefined,
|
|
112
113
|
clientId: row.client_id,
|
|
113
114
|
clientSecret: row.client_secret,
|
|
114
115
|
profileArn: row.profile_arn,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readActiveProfileArnFromKiroCli(): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { getCliDbPath, safeJsonParse } from './kiro-cli-parser';
|
|
4
|
+
export function readActiveProfileArnFromKiroCli() {
|
|
5
|
+
const dbPath = getCliDbPath();
|
|
6
|
+
if (!existsSync(dbPath))
|
|
7
|
+
return undefined;
|
|
8
|
+
let cliDb;
|
|
9
|
+
try {
|
|
10
|
+
cliDb = new Database(dbPath, { readonly: true });
|
|
11
|
+
cliDb.run('PRAGMA busy_timeout = 5000');
|
|
12
|
+
const row = cliDb
|
|
13
|
+
.prepare('SELECT value FROM state WHERE key = ?')
|
|
14
|
+
.get('api.codewhisperer.profile');
|
|
15
|
+
const parsed = safeJsonParse(row?.value);
|
|
16
|
+
const arn = parsed?.arn || parsed?.profileArn || parsed?.profile_arn;
|
|
17
|
+
return typeof arn === 'string' && arn.trim() ? arn.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
try {
|
|
24
|
+
cliDb?.close();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { extractRegionFromArn, normalizeRegion } from '../../constants';
|
|
3
4
|
import { createDeterministicAccountId } from '../accounts';
|
|
4
5
|
import * as logger from '../logger';
|
|
5
6
|
import { kiroDb } from '../storage/sqlite';
|
|
6
7
|
import { fetchUsageLimits } from '../usage';
|
|
7
8
|
import { findClientCredsRecursive, getCliDbPath, makePlaceholderEmail, normalizeExpiresAt, safeJsonParse } from './kiro-cli-parser';
|
|
9
|
+
import { readActiveProfileArnFromKiroCli } from './kiro-cli-profile';
|
|
8
10
|
export async function syncFromKiroCli() {
|
|
9
11
|
const dbPath = getCliDbPath();
|
|
10
12
|
if (!existsSync(dbPath))
|
|
@@ -13,6 +15,19 @@ export async function syncFromKiroCli() {
|
|
|
13
15
|
const cliDb = new Database(dbPath, { readonly: true });
|
|
14
16
|
cliDb.run('PRAGMA busy_timeout = 5000');
|
|
15
17
|
const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
|
|
18
|
+
let activeProfileArn;
|
|
19
|
+
try {
|
|
20
|
+
const stateRow = cliDb
|
|
21
|
+
.prepare('SELECT value FROM state WHERE key = ?')
|
|
22
|
+
.get('api.codewhisperer.profile');
|
|
23
|
+
const parsed = safeJsonParse(stateRow?.value);
|
|
24
|
+
const arn = parsed?.arn || parsed?.profileArn || parsed?.profile_arn;
|
|
25
|
+
if (typeof arn === 'string' && arn.trim())
|
|
26
|
+
activeProfileArn = arn.trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore state read failures; token import can proceed.
|
|
30
|
+
}
|
|
16
31
|
const deviceRegRow = rows.find((r) => typeof r?.key === 'string' && r.key.includes('device-registration'));
|
|
17
32
|
const deviceReg = safeJsonParse(deviceRegRow?.value);
|
|
18
33
|
const regCreds = deviceReg ? findClientCredsRecursive(deviceReg) : {};
|
|
@@ -23,8 +38,16 @@ export async function syncFromKiroCli() {
|
|
|
23
38
|
continue;
|
|
24
39
|
const isIdc = row.key.includes('odic');
|
|
25
40
|
const authMethod = isIdc ? 'idc' : 'desktop';
|
|
26
|
-
const
|
|
27
|
-
|
|
41
|
+
const oidcRegion = normalizeRegion(data.region);
|
|
42
|
+
let profileArn = data.profile_arn || data.profileArn;
|
|
43
|
+
if (!profileArn && isIdc)
|
|
44
|
+
profileArn = activeProfileArn || readActiveProfileArnFromKiroCli();
|
|
45
|
+
const serviceRegion = extractRegionFromArn(profileArn) || oidcRegion;
|
|
46
|
+
const startUrl = typeof data.start_url === 'string'
|
|
47
|
+
? data.start_url
|
|
48
|
+
: typeof data.startUrl === 'string'
|
|
49
|
+
? data.startUrl
|
|
50
|
+
: undefined;
|
|
28
51
|
const accessToken = data.access_token || data.accessToken || '';
|
|
29
52
|
const refreshToken = data.refresh_token || data.refreshToken;
|
|
30
53
|
if (!refreshToken)
|
|
@@ -46,7 +69,7 @@ export async function syncFromKiroCli() {
|
|
|
46
69
|
access: accessToken,
|
|
47
70
|
expires: cliExpiresAt,
|
|
48
71
|
authMethod,
|
|
49
|
-
region,
|
|
72
|
+
region: serviceRegion,
|
|
50
73
|
profileArn,
|
|
51
74
|
clientId,
|
|
52
75
|
clientSecret,
|
|
@@ -63,7 +86,8 @@ export async function syncFromKiroCli() {
|
|
|
63
86
|
catch (e) {
|
|
64
87
|
logger.warn('Kiro CLI sync: failed to fetch usage/email; falling back', {
|
|
65
88
|
authMethod,
|
|
66
|
-
|
|
89
|
+
serviceRegion,
|
|
90
|
+
oidcRegion
|
|
67
91
|
});
|
|
68
92
|
logger.debug('Kiro CLI sync: usage fetch error', e);
|
|
69
93
|
}
|
|
@@ -80,10 +104,10 @@ export async function syncFromKiroCli() {
|
|
|
80
104
|
email = existing.email;
|
|
81
105
|
}
|
|
82
106
|
else {
|
|
83
|
-
email = makePlaceholderEmail(authMethod,
|
|
107
|
+
email = makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
|
-
const resolvedEmail = email || makePlaceholderEmail(authMethod,
|
|
110
|
+
const resolvedEmail = email || makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
|
|
87
111
|
const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, profileArn);
|
|
88
112
|
const existingById = all.find((a) => a.id === id);
|
|
89
113
|
if (existingById &&
|
|
@@ -91,7 +115,7 @@ export async function syncFromKiroCli() {
|
|
|
91
115
|
existingById.expires_at >= cliExpiresAt)
|
|
92
116
|
continue;
|
|
93
117
|
if (usageOk) {
|
|
94
|
-
const placeholderEmail = makePlaceholderEmail(authMethod,
|
|
118
|
+
const placeholderEmail = makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
|
|
95
119
|
const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, profileArn);
|
|
96
120
|
if (placeholderId !== id) {
|
|
97
121
|
const placeholderRow = all.find((a) => a.id === placeholderId);
|
|
@@ -100,7 +124,8 @@ export async function syncFromKiroCli() {
|
|
|
100
124
|
id: placeholderId,
|
|
101
125
|
email: placeholderRow.email,
|
|
102
126
|
authMethod,
|
|
103
|
-
region: placeholderRow.region ||
|
|
127
|
+
region: placeholderRow.region || serviceRegion,
|
|
128
|
+
oidcRegion: placeholderRow.oidc_region || oidcRegion,
|
|
104
129
|
clientId,
|
|
105
130
|
clientSecret,
|
|
106
131
|
profileArn,
|
|
@@ -123,10 +148,12 @@ export async function syncFromKiroCli() {
|
|
|
123
148
|
id,
|
|
124
149
|
email: resolvedEmail,
|
|
125
150
|
authMethod,
|
|
126
|
-
region,
|
|
151
|
+
region: serviceRegion,
|
|
152
|
+
oidcRegion,
|
|
127
153
|
clientId,
|
|
128
154
|
clientSecret,
|
|
129
155
|
profileArn,
|
|
156
|
+
startUrl,
|
|
130
157
|
refreshToken,
|
|
131
158
|
accessToken,
|
|
132
159
|
expiresAt: cliExpiresAt,
|
package/dist/plugin/token.js
CHANGED
|
@@ -3,8 +3,9 @@ import { KiroTokenRefreshError } from './errors';
|
|
|
3
3
|
export async function refreshAccessToken(auth) {
|
|
4
4
|
const p = decodeRefreshToken(auth.refresh);
|
|
5
5
|
const isIdc = auth.authMethod === 'idc';
|
|
6
|
+
const oidcRegion = auth.oidcRegion || auth.region;
|
|
6
7
|
const url = isIdc
|
|
7
|
-
? `https://oidc.${
|
|
8
|
+
? `https://oidc.${oidcRegion}.amazonaws.com/token`
|
|
8
9
|
: `https://prod.${auth.region}.auth.desktop.kiro.dev/refreshToken`;
|
|
9
10
|
if (isIdc && (!p.clientId || !p.clientSecret)) {
|
|
10
11
|
throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
|
|
@@ -62,6 +63,8 @@ export async function refreshAccessToken(auth) {
|
|
|
62
63
|
expires: Date.now() + (d.expires_in || d.expiresIn || 3600) * 1000,
|
|
63
64
|
authMethod: auth.authMethod,
|
|
64
65
|
region: auth.region,
|
|
66
|
+
oidcRegion: auth.oidcRegion,
|
|
67
|
+
profileArn: auth.profileArn,
|
|
65
68
|
clientId: auth.clientId,
|
|
66
69
|
clientSecret: auth.clientSecret,
|
|
67
70
|
email: auth.email || d.userInfo?.email
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface KiroAuthDetails {
|
|
|
8
8
|
expires: number;
|
|
9
9
|
authMethod: KiroAuthMethod;
|
|
10
10
|
region: KiroRegion;
|
|
11
|
+
oidcRegion?: KiroRegion;
|
|
11
12
|
clientId?: string;
|
|
12
13
|
clientSecret?: string;
|
|
13
14
|
email?: string;
|
|
@@ -25,6 +26,7 @@ export interface ManagedAccount {
|
|
|
25
26
|
email: string;
|
|
26
27
|
authMethod: KiroAuthMethod;
|
|
27
28
|
region: KiroRegion;
|
|
29
|
+
oidcRegion?: KiroRegion;
|
|
28
30
|
clientId?: string;
|
|
29
31
|
clientSecret?: string;
|
|
30
32
|
profileArn?: string;
|
package/dist/plugin/usage.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export async function fetchUsageLimits(auth) {
|
|
2
|
-
const url = `https://q.${auth.region}.amazonaws.com/getUsageLimits
|
|
2
|
+
const url = new URL(`https://q.${auth.region}.amazonaws.com/getUsageLimits`);
|
|
3
|
+
url.searchParams.set('isEmailRequired', 'true');
|
|
4
|
+
url.searchParams.set('origin', 'AI_EDITOR');
|
|
5
|
+
url.searchParams.set('resourceType', 'AGENTIC_REQUEST');
|
|
6
|
+
if (auth.profileArn)
|
|
7
|
+
url.searchParams.set('profileArn', auth.profileArn);
|
|
3
8
|
try {
|
|
4
|
-
const res = await fetch(url, {
|
|
9
|
+
const res = await fetch(url.toString(), {
|
|
5
10
|
method: 'GET',
|
|
6
11
|
headers: {
|
|
7
12
|
Authorization: `Bearer ${auth.access}`,
|
|
@@ -10,8 +15,18 @@ export async function fetchUsageLimits(auth) {
|
|
|
10
15
|
'amz-sdk-request': 'attempt=1; max=1'
|
|
11
16
|
}
|
|
12
17
|
});
|
|
13
|
-
if (!res.ok)
|
|
14
|
-
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const body = await res.text().catch(() => '');
|
|
20
|
+
const requestId = res.headers.get('x-amzn-requestid') ||
|
|
21
|
+
res.headers.get('x-amzn-request-id') ||
|
|
22
|
+
res.headers.get('x-amz-request-id') ||
|
|
23
|
+
'';
|
|
24
|
+
const errType = res.headers.get('x-amzn-errortype') || res.headers.get('x-amzn-error-type') || '';
|
|
25
|
+
const msg = body && body.length > 0
|
|
26
|
+
? `${body.slice(0, 2000)}${body.length > 2000 ? '…' : ''}`
|
|
27
|
+
: `HTTP ${res.status}`;
|
|
28
|
+
throw new Error(`Status: ${res.status}${errType ? ` (${errType})` : ''}${requestId ? ` [${requestId}]` : ''}: ${msg}`);
|
|
29
|
+
}
|
|
15
30
|
const data = await res.json();
|
|
16
31
|
let usedCount = 0, limitCount = 0;
|
|
17
32
|
if (Array.isArray(data.usageBreakdownList)) {
|
package/package.json
CHANGED
package/dist/plugin/cli.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare function promptAddAnotherAccount(currentCount: number): Promise<boolean>;
|
|
2
|
-
export declare function promptDeleteAccount(accounts: ExistingAccountInfo[]): Promise<number[] | null>;
|
|
3
|
-
export type LoginMode = 'add' | 'fresh' | 'delete';
|
|
4
|
-
export interface ExistingAccountInfo {
|
|
5
|
-
email?: string;
|
|
6
|
-
index: number;
|
|
7
|
-
}
|
|
8
|
-
export declare function promptLoginMode(existingAccounts: ExistingAccountInfo[]): Promise<LoginMode>;
|