@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.
@@ -15,6 +15,7 @@ export class AccountRepository {
15
15
  email: r.email,
16
16
  authMethod: r.auth_method,
17
17
  region: r.region,
18
+ oidcRegion: r.oidc_region || undefined,
18
19
  clientId: r.client_id,
19
20
  clientSecret: r.client_secret,
20
21
  profileArn: r.profile_arn,
@@ -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 tokenData = await tokenResponse.json();
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
- if (tokenData.accessToken && tokenData.refreshToken) {
120
- const expiresInSeconds = tokenData.expiresIn || 3600;
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: tokenData.refreshToken,
124
- accessToken: tokenData.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 &&
@@ -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
- writeToKiroCli(acc).catch(() => { });
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),
@@ -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.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}\n`;
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) { }
@@ -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.replace('{{region}}', auth.region),
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(refresh_token) DO UPDATE SET
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 region = data.region || 'us-east-1';
27
- const profileArn = data.profile_arn || data.profileArn;
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
- region
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, region, clientId, profileArn);
107
+ email = makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
84
108
  }
85
109
  }
86
- const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId, profileArn);
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, region, clientId, profileArn);
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 || 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,
@@ -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.${auth.region}.amazonaws.com/token`
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
@@ -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;
@@ -1,7 +1,12 @@
1
1
  export async function fetchUsageLimits(auth) {
2
- const url = `https://q.${auth.region}.amazonaws.com/getUsageLimits?isEmailRequired=true&origin=AI_EDITOR&resourceType=AGENTIC_REQUEST`;
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
- throw new Error(`Status: ${res.status}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhafron/opencode-kiro-auth",
3
- "version": "1.6.6",
3
+ "version": "1.8.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",
@@ -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>;