@zhafron/opencode-kiro-auth 1.3.0 → 1.4.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,14 +1,13 @@
1
- import type { AccountSelectionStrategy, KiroAuthDetails, ManagedAccount, UsageMetadata } from './types';
1
+ import type { AccountSelectionStrategy, KiroAuthDetails, ManagedAccount } from './types';
2
2
  export declare function generateAccountId(): string;
3
3
  export declare function createDeterministicAccountId(email: string, method: string, clientId?: string, profileArn?: string): string;
4
4
  export declare class AccountManager {
5
5
  private accounts;
6
- private usage;
7
6
  private cursor;
8
7
  private strategy;
9
8
  private lastToastTime;
10
9
  private lastUsageToastTime;
11
- constructor(accounts: ManagedAccount[], usage: Record<string, UsageMetadata>, strategy?: AccountSelectionStrategy);
10
+ constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
12
11
  static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
13
12
  getAccountCount(): number;
14
13
  getAccounts(): ManagedAccount[];
@@ -19,7 +18,7 @@ export declare class AccountManager {
19
18
  updateUsage(id: string, meta: {
20
19
  usedCount: number;
21
20
  limitCount: number;
22
- realEmail?: string;
21
+ email?: string;
23
22
  }): void;
24
23
  addAccount(a: ManagedAccount): void;
25
24
  removeAccount(a: ManagedAccount): void;
@@ -12,32 +12,20 @@ export function createDeterministicAccountId(email, method, clientId, profileArn
12
12
  }
13
13
  export class AccountManager {
14
14
  accounts;
15
- usage;
16
15
  cursor;
17
16
  strategy;
18
17
  lastToastTime = 0;
19
18
  lastUsageToastTime = 0;
20
- constructor(accounts, usage, strategy = 'sticky') {
19
+ constructor(accounts, strategy = 'sticky') {
21
20
  this.accounts = accounts;
22
- this.usage = usage;
23
21
  this.cursor = 0;
24
22
  this.strategy = strategy;
25
- for (const a of this.accounts) {
26
- const m = this.usage[a.id];
27
- if (m) {
28
- a.usedCount = m.usedCount;
29
- a.limitCount = m.limitCount;
30
- a.realEmail = m.realEmail;
31
- }
32
- }
33
23
  }
34
24
  static async loadFromDisk(strategy) {
35
25
  const rows = kiroDb.getAccounts();
36
- const usage = kiroDb.getUsage();
37
26
  const accounts = rows.map((r) => ({
38
27
  id: r.id,
39
28
  email: r.email,
40
- realEmail: r.real_email,
41
29
  authMethod: r.auth_method,
42
30
  region: r.region,
43
31
  clientId: r.client_id,
@@ -50,9 +38,12 @@ export class AccountManager {
50
38
  isHealthy: r.is_healthy === 1,
51
39
  unhealthyReason: r.unhealthy_reason,
52
40
  recoveryTime: r.recovery_time,
53
- lastUsed: r.last_used
41
+ failCount: r.fail_count || 0,
42
+ lastUsed: r.last_used,
43
+ usedCount: r.used_count,
44
+ limitCount: r.limit_count
54
45
  }));
55
- return new AccountManager(accounts, usage, strategy || 'sticky');
46
+ return new AccountManager(accounts, strategy || 'sticky');
56
47
  }
57
48
  getAccountCount() {
58
49
  return this.accounts.length;
@@ -81,7 +72,7 @@ export class AccountManager {
81
72
  const now = Date.now();
82
73
  const available = this.accounts.filter((a) => {
83
74
  if (!a.isHealthy) {
84
- if (a.recoveryTime && now >= a.recoveryTime) {
75
+ if (a.failCount < 3 && a.recoveryTime && now >= a.recoveryTime) {
85
76
  a.isHealthy = true;
86
77
  delete a.unhealthyReason;
87
78
  delete a.recoveryTime;
@@ -91,18 +82,29 @@ export class AccountManager {
91
82
  }
92
83
  return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
93
84
  });
94
- if (available.length === 0)
95
- return null;
96
85
  let selected;
97
- if (this.strategy === 'sticky') {
98
- selected = available.find((_, i) => i === this.cursor) || available[0];
99
- }
100
- else if (this.strategy === 'round-robin') {
101
- selected = available[this.cursor % available.length];
102
- this.cursor = (this.cursor + 1) % available.length;
86
+ if (available.length > 0) {
87
+ if (this.strategy === 'sticky') {
88
+ selected = available.find((_, i) => i === this.cursor) || available[0];
89
+ }
90
+ else if (this.strategy === 'round-robin') {
91
+ selected = available[this.cursor % available.length];
92
+ this.cursor = (this.cursor + 1) % available.length;
93
+ }
94
+ else if (this.strategy === 'lowest-usage') {
95
+ selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
96
+ }
103
97
  }
104
- else if (this.strategy === 'lowest-usage') {
105
- selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
98
+ if (!selected) {
99
+ const fallback = this.accounts
100
+ .filter((a) => !a.isHealthy && a.failCount < 3)
101
+ .sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
102
+ if (fallback) {
103
+ fallback.isHealthy = true;
104
+ delete fallback.unhealthyReason;
105
+ delete fallback.recoveryTime;
106
+ selected = fallback;
107
+ }
106
108
  }
107
109
  if (selected) {
108
110
  selected.lastUsed = now;
@@ -117,11 +119,11 @@ export class AccountManager {
117
119
  if (a) {
118
120
  a.usedCount = meta.usedCount;
119
121
  a.limitCount = meta.limitCount;
120
- if (meta.realEmail)
121
- a.realEmail = meta.realEmail;
122
+ if (meta.email)
123
+ a.email = meta.email;
124
+ a.failCount = 0;
125
+ kiroDb.upsertAccount(a);
122
126
  }
123
- this.usage[id] = { ...meta, lastSync: Date.now() };
124
- kiroDb.upsertUsage(id, this.usage[id]);
125
127
  }
126
128
  addAccount(a) {
127
129
  const i = this.accounts.findIndex((x) => x.id === a.id);
@@ -136,7 +138,6 @@ export class AccountManager {
136
138
  if (removedIndex === -1)
137
139
  return;
138
140
  this.accounts = this.accounts.filter((x) => x.id !== a.id);
139
- delete this.usage[a.id];
140
141
  kiroDb.deleteAccount(a.id);
141
142
  if (this.accounts.length === 0)
142
143
  this.cursor = 0;
@@ -151,14 +152,15 @@ export class AccountManager {
151
152
  acc.accessToken = auth.access;
152
153
  acc.expiresAt = auth.expires;
153
154
  acc.lastUsed = Date.now();
154
- if (auth.email && auth.email !== 'builder-id@aws.amazon.com')
155
- acc.realEmail = auth.email;
155
+ if (auth.email)
156
+ acc.email = auth.email;
156
157
  const p = decodeRefreshToken(auth.refresh);
157
158
  acc.refreshToken = p.refreshToken;
158
159
  if (p.profileArn)
159
160
  acc.profileArn = p.profileArn;
160
161
  if (p.clientId)
161
162
  acc.clientId = p.clientId;
163
+ acc.failCount = 0;
162
164
  kiroDb.upsertAccount(acc);
163
165
  writeToKiroCli(acc).catch(() => { });
164
166
  }
@@ -175,7 +177,8 @@ export class AccountManager {
175
177
  if (acc) {
176
178
  acc.isHealthy = false;
177
179
  acc.unhealthyReason = reason;
178
- acc.recoveryTime = recovery;
180
+ acc.failCount = (acc.failCount || 0) + 1;
181
+ acc.recoveryTime = acc.failCount >= 3 ? undefined : recovery || Date.now() + 3600000;
179
182
  kiroDb.upsertAccount(acc);
180
183
  }
181
184
  }
@@ -4,10 +4,9 @@ import { join } from 'node:path';
4
4
  import * as logger from '../logger';
5
5
  import { kiroDb } from './sqlite';
6
6
  function getBaseDir() {
7
- const platform = process.platform;
8
- if (platform === 'win32') {
7
+ const p = process.platform;
8
+ if (p === 'win32')
9
9
  return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
10
- }
11
10
  return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
12
11
  }
13
12
  export async function migrateJsonToSqlite() {
@@ -19,32 +18,32 @@ export async function migrateJsonToSqlite() {
19
18
  .access(accPath)
20
19
  .then(() => true)
21
20
  .catch(() => false);
21
+ const useExists = await fs
22
+ .access(usePath)
23
+ .then(() => true)
24
+ .catch(() => false);
22
25
  if (accExists) {
23
- const data = JSON.parse(await fs.readFile(accPath, 'utf-8'));
24
- if (data.accounts && Array.isArray(data.accounts)) {
25
- for (const acc of data.accounts) {
26
+ const accData = JSON.parse(await fs.readFile(accPath, 'utf-8'));
27
+ const useData = useExists ? JSON.parse(await fs.readFile(usePath, 'utf-8')) : { usage: {} };
28
+ if (accData.accounts && Array.isArray(accData.accounts)) {
29
+ for (const acc of accData.accounts) {
30
+ const usage = useData.usage[acc.id] || {};
26
31
  kiroDb.upsertAccount({
27
32
  ...acc,
33
+ email: acc.realEmail || acc.email,
28
34
  rateLimitResetTime: acc.rateLimitResetTime || 0,
29
35
  isHealthy: acc.isHealthy !== false,
30
- lastUsed: acc.lastUsed || 0
36
+ failCount: 0,
37
+ lastUsed: acc.lastUsed || 0,
38
+ usedCount: usage.usedCount || 0,
39
+ limitCount: usage.limitCount || 0,
40
+ lastSync: usage.lastSync || 0
31
41
  });
32
42
  }
33
43
  }
34
44
  await fs.rename(accPath, accPath + '.bak');
35
- }
36
- const useExists = await fs
37
- .access(usePath)
38
- .then(() => true)
39
- .catch(() => false);
40
- if (useExists) {
41
- const data = JSON.parse(await fs.readFile(usePath, 'utf-8'));
42
- if (data.usage && typeof data.usage === 'object') {
43
- for (const [id, meta] of Object.entries(data.usage)) {
44
- kiroDb.upsertUsage(id, meta);
45
- }
46
- }
47
- await fs.rename(usePath, usePath + '.bak');
45
+ if (useExists)
46
+ await fs.rename(usePath, usePath + '.bak');
48
47
  }
49
48
  }
50
49
  catch (e) {
@@ -1,4 +1,3 @@
1
- import type { UsageMetadata } from '../types';
2
1
  export declare const DB_PATH: string;
3
2
  export declare class KiroDatabase {
4
3
  private db;
@@ -7,10 +6,6 @@ export declare class KiroDatabase {
7
6
  getAccounts(): any[];
8
7
  upsertAccount(acc: any): void;
9
8
  deleteAccount(id: string): void;
10
- getUsage(): Record<string, UsageMetadata>;
11
- upsertUsage(id: string, meta: UsageMetadata): void;
12
- getSetting(key: string): string | null;
13
- setSetting(key: string, value: string): void;
14
9
  close(): void;
15
10
  }
16
11
  export declare const kiroDb: KiroDatabase;
@@ -23,21 +23,66 @@ export class KiroDatabase {
23
23
  this.db.run('PRAGMA journal_mode = WAL');
24
24
  this.db.run(`
25
25
  CREATE TABLE IF NOT EXISTS accounts (
26
- id TEXT PRIMARY KEY, email TEXT NOT NULL, real_email TEXT, auth_method TEXT NOT NULL,
26
+ id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
27
27
  region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
28
28
  refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
29
29
  rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
30
- recovery_time INTEGER, last_used INTEGER DEFAULT 0
30
+ recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
31
+ used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
31
32
  )
32
33
  `);
33
- this.db.run(`
34
- CREATE TABLE IF NOT EXISTS usage (
35
- account_id TEXT PRIMARY KEY, used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0,
36
- real_email TEXT, last_sync INTEGER,
37
- FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
38
- )
39
- `);
40
- this.db.run('CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)');
34
+ const columns = this.db.prepare('PRAGMA table_info(accounts)').all();
35
+ const names = new Set(columns.map((c) => c.name));
36
+ if (names.has('real_email')) {
37
+ this.db.run('BEGIN TRANSACTION');
38
+ try {
39
+ this.db.run("UPDATE accounts SET email = real_email WHERE real_email IS NOT NULL AND real_email != '' AND email LIKE 'builder-id@aws.amazon.com%'");
40
+ this.db.run(`
41
+ CREATE TABLE accounts_new (
42
+ id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
43
+ region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
44
+ refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
45
+ rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
46
+ recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
47
+ used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0
48
+ )
49
+ `);
50
+ this.db.run(`
51
+ 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)
52
+ 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
53
+ `);
54
+ this.db.run('DROP TABLE accounts');
55
+ this.db.run('ALTER TABLE accounts_new RENAME TO accounts');
56
+ this.db.run('COMMIT');
57
+ }
58
+ catch (e) {
59
+ this.db.run('ROLLBACK');
60
+ }
61
+ }
62
+ else {
63
+ const needed = {
64
+ fail_count: 'INTEGER DEFAULT 0',
65
+ used_count: 'INTEGER DEFAULT 0',
66
+ limit_count: 'INTEGER DEFAULT 0',
67
+ last_sync: 'INTEGER DEFAULT 0'
68
+ };
69
+ for (const [n, d] of Object.entries(needed)) {
70
+ if (!names.has(n))
71
+ this.db.run(`ALTER TABLE accounts ADD COLUMN ${n} ${d}`);
72
+ }
73
+ }
74
+ const hasUsageTable = this.db
75
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='usage'")
76
+ .get();
77
+ if (hasUsageTable) {
78
+ this.db.run(`
79
+ UPDATE accounts SET
80
+ used_count = COALESCE((SELECT used_count FROM usage WHERE usage.account_id = accounts.id), used_count),
81
+ limit_count = COALESCE((SELECT limit_count FROM usage WHERE usage.account_id = accounts.id), limit_count),
82
+ last_sync = COALESCE((SELECT last_sync FROM usage WHERE usage.account_id = accounts.id), last_sync)
83
+ `);
84
+ this.db.run('DROP TABLE usage');
85
+ }
41
86
  }
42
87
  getAccounts() {
43
88
  return this.db.prepare('SELECT * FROM accounts').all();
@@ -46,57 +91,26 @@ export class KiroDatabase {
46
91
  this.db
47
92
  .prepare(`
48
93
  INSERT INTO accounts (
49
- id, email, real_email, auth_method, region, client_id, client_secret,
94
+ id, email, auth_method, region, client_id, client_secret,
50
95
  profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
51
- is_healthy, unhealthy_reason, recovery_time, last_used
52
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
96
+ is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
97
+ used_count, limit_count, last_sync
98
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
99
  ON CONFLICT(id) DO UPDATE SET
54
- email=excluded.email, real_email=excluded.real_email, auth_method=excluded.auth_method,
100
+ email=excluded.email, auth_method=excluded.auth_method,
55
101
  region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
56
102
  profile_arn=excluded.profile_arn, refresh_token=excluded.refresh_token,
57
103
  access_token=excluded.access_token, expires_at=excluded.expires_at,
58
104
  rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
59
105
  unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
60
- last_used=excluded.last_used
106
+ fail_count=excluded.fail_count, last_used=excluded.last_used,
107
+ used_count=excluded.used_count, limit_count=excluded.limit_count, last_sync=excluded.last_sync
61
108
  `)
62
- .run(acc.id, acc.email, acc.realEmail || null, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.lastUsed || 0);
109
+ .run(acc.id, acc.email, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || 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
110
  }
64
111
  deleteAccount(id) {
65
112
  this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
66
113
  }
67
- getUsage() {
68
- const rows = this.db.prepare('SELECT * FROM usage').all();
69
- const usage = {};
70
- for (const r of rows) {
71
- usage[r.account_id] = {
72
- usedCount: r.used_count,
73
- limitCount: r.limit_count,
74
- realEmail: r.real_email,
75
- lastSync: r.last_sync
76
- };
77
- }
78
- return usage;
79
- }
80
- upsertUsage(id, meta) {
81
- this.db
82
- .prepare(`
83
- INSERT INTO usage (account_id, used_count, limit_count, real_email, last_sync)
84
- VALUES (?, ?, ?, ?, ?)
85
- ON CONFLICT(account_id) DO UPDATE SET
86
- used_count=excluded.used_count, limit_count=excluded.limit_count,
87
- real_email=excluded.real_email, last_sync=excluded.last_sync
88
- `)
89
- .run(id, meta.usedCount, meta.limitCount, meta.realEmail || null, meta.lastSync);
90
- }
91
- getSetting(key) {
92
- const row = this.db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
93
- return row ? row.value : null;
94
- }
95
- setSetting(key, value) {
96
- this.db
97
- .prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value')
98
- .run(key, value);
99
- }
100
114
  close() {
101
115
  this.db.close();
102
116
  }
@@ -5,6 +5,7 @@ import { join } from 'node:path';
5
5
  import { createDeterministicAccountId } from '../accounts';
6
6
  import * as logger from '../logger';
7
7
  import { kiroDb } from '../storage/sqlite';
8
+ import { fetchUsageLimits } from '../usage';
8
9
  function getCliDbPath() {
9
10
  const p = platform();
10
11
  if (p === 'win32')
@@ -32,8 +33,8 @@ export async function syncFromKiroCli() {
32
33
  }
33
34
  if (!data.access_token)
34
35
  continue;
35
- const email = data.email || 'cli-account@kiro.dev';
36
36
  const authMethod = row.key.includes('odic') ? 'idc' : 'desktop';
37
+ const region = data.region || 'us-east-1';
37
38
  const clientId = data.client_id ||
38
39
  (authMethod === 'idc'
39
40
  ? JSON.parse(rows.find((r) => r.key.includes('device-registration'))?.value || '{}')
@@ -44,25 +45,43 @@ export async function syncFromKiroCli() {
44
45
  ? JSON.parse(rows.find((r) => r.key.includes('device-registration'))?.value || '{}')
45
46
  .client_secret
46
47
  : undefined);
47
- const id = createDeterministicAccountId(email, authMethod, clientId, data.profile_arn);
48
- const existing = kiroDb.getAccounts().find((a) => a.id === id);
49
- const cliExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() : 0;
50
- if (existing && existing.expires_at >= cliExpiresAt)
51
- continue;
52
- kiroDb.upsertAccount({
53
- id,
54
- email,
55
- realEmail: data.real_email || email,
56
- authMethod,
57
- region: data.region || 'us-east-1',
58
- clientId,
59
- clientSecret,
60
- profileArn: data.profile_arn,
61
- refreshToken: data.refresh_token,
62
- accessToken: data.access_token,
63
- expiresAt: cliExpiresAt || Date.now() + 3600000,
64
- isHealthy: 1
65
- });
48
+ try {
49
+ const u = await fetchUsageLimits({
50
+ refresh: '',
51
+ access: data.access_token,
52
+ expires: 0,
53
+ authMethod,
54
+ region,
55
+ clientId,
56
+ clientSecret
57
+ });
58
+ const email = u.email;
59
+ if (!email)
60
+ continue;
61
+ const id = createDeterministicAccountId(email, authMethod, clientId, data.profile_arn);
62
+ const existing = kiroDb.getAccounts().find((a) => a.id === id);
63
+ const cliExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() : 0;
64
+ if (existing && existing.is_healthy === 1 && existing.expires_at >= cliExpiresAt)
65
+ continue;
66
+ kiroDb.upsertAccount({
67
+ id,
68
+ email,
69
+ authMethod,
70
+ region,
71
+ clientId,
72
+ clientSecret,
73
+ profileArn: data.profile_arn,
74
+ refreshToken: data.refresh_token,
75
+ accessToken: data.access_token,
76
+ expiresAt: cliExpiresAt || Date.now() + 3600000,
77
+ isHealthy: 1,
78
+ failCount: 0,
79
+ usedCount: u.usedCount,
80
+ limitCount: u.limitCount,
81
+ lastSync: Date.now()
82
+ });
83
+ }
84
+ catch { }
66
85
  }
67
86
  }
68
87
  cliDb.close();
@@ -77,7 +96,7 @@ export async function writeToKiroCli(acc) {
77
96
  return;
78
97
  try {
79
98
  const cliDb = new Database(dbPath);
80
- cliDb.exec('PRAGMA busy_timeout = 5000');
99
+ cliDb.run('PRAGMA busy_timeout = 5000');
81
100
  const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
82
101
  const targetKey = acc.authMethod === 'idc' ? 'kirocli:odic:token' : 'kirocli:social:token';
83
102
  const row = rows.find((r) => r.key === targetKey || r.key.endsWith(targetKey));
@@ -21,7 +21,6 @@ export interface RefreshParts {
21
21
  export interface ManagedAccount {
22
22
  id: string;
23
23
  email: string;
24
- realEmail?: string;
25
24
  authMethod: KiroAuthMethod;
26
25
  region: KiroRegion;
27
26
  clientId?: string;
@@ -34,14 +33,15 @@ export interface ManagedAccount {
34
33
  isHealthy: boolean;
35
34
  unhealthyReason?: string;
36
35
  recoveryTime?: number;
36
+ failCount: number;
37
37
  usedCount?: number;
38
38
  limitCount?: number;
39
+ lastSync?: number;
39
40
  lastUsed?: number;
40
41
  }
41
42
  export interface UsageMetadata {
42
43
  usedCount: number;
43
44
  limitCount: number;
44
- realEmail?: string;
45
45
  lastSync: number;
46
46
  }
47
47
  export interface CodeWhispererMessage {
@@ -34,12 +34,12 @@ export function updateAccountQuota(account, usage, accountManager) {
34
34
  const meta = {
35
35
  usedCount: usage.usedCount || 0,
36
36
  limitCount: usage.limitCount || 0,
37
- realEmail: usage.email
37
+ email: usage.email
38
38
  };
39
39
  account.usedCount = meta.usedCount;
40
40
  account.limitCount = meta.limitCount;
41
- if (meta.realEmail)
42
- account.realEmail = meta.realEmail;
41
+ if (usage.email)
42
+ account.email = usage.email;
43
43
  if (accountManager)
44
44
  accountManager.updateUsage(account.id, meta);
45
45
  }
package/dist/plugin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { exec } from 'node:child_process';
2
2
  import { KIRO_CONSTANTS } from './constants';
3
- import { accessTokenExpired, encodeRefreshToken } from './kiro/auth';
3
+ import { accessTokenExpired } from './kiro/auth';
4
4
  import { authorizeKiroIDC } from './kiro/oauth-idc';
5
- import { AccountManager, generateAccountId } from './plugin/accounts';
5
+ import { AccountManager, createDeterministicAccountId } from './plugin/accounts';
6
6
  import { promptAddAnotherAccount, promptLoginMode } from './plugin/cli';
7
7
  import { loadConfig } from './plugin/config';
8
8
  import { KiroTokenRefreshError } from './plugin/errors';
@@ -54,6 +54,22 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
54
54
  if (config.auto_sync_kiro_cli)
55
55
  await syncFromKiroCli();
56
56
  const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
57
+ const allAccs = am.getAccounts();
58
+ for (const acc of allAccs) {
59
+ if (acc.isHealthy && (!acc.lastSync || Date.now() - acc.lastSync > 3600000)) {
60
+ try {
61
+ const auth = am.toAuthDetails(acc);
62
+ const u = await fetchUsageLimits(auth);
63
+ am.updateUsage(acc.id, {
64
+ usedCount: u.usedCount,
65
+ limitCount: u.limitCount,
66
+ email: u.email
67
+ });
68
+ }
69
+ catch { }
70
+ }
71
+ }
72
+ await am.saveToDisk();
57
73
  return {
58
74
  apiKey: '',
59
75
  baseURL: KIRO_CONSTANTS.BASE_URL.replace('/generateAssistantResponse', '').replace('{{region}}', config.default_region || 'us-east-1'),
@@ -76,7 +92,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
76
92
  const count = am.getAccountCount();
77
93
  if (count === 0)
78
94
  throw new Error('No accounts');
79
- const acc = am.getCurrentOrNext();
95
+ let acc = am.getCurrentOrNext();
80
96
  if (!acc) {
81
97
  const wait = am.getMinWaitTime();
82
98
  if (wait > 0 && wait < 30000) {
@@ -87,6 +103,14 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
87
103
  }
88
104
  throw new Error('All accounts are unhealthy or rate-limited');
89
105
  }
106
+ if (count > 1 && am.shouldShowToast())
107
+ showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
108
+ if (am.shouldShowUsageToast() &&
109
+ acc.usedCount !== undefined &&
110
+ acc.limitCount !== undefined) {
111
+ const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
112
+ showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
113
+ }
90
114
  const auth = am.toAuthDetails(acc);
91
115
  if (accessTokenExpired(auth, config.token_expiry_buffer_ms)) {
92
116
  try {
@@ -102,6 +126,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
102
126
  if (stillAcc &&
103
127
  !accessTokenExpired(refreshedAm.toAuthDetails(stillAcc), config.token_expiry_buffer_ms)) {
104
128
  showToast('Credentials recovered from Kiro CLI sync.', 'info');
129
+ acc = stillAcc;
105
130
  continue;
106
131
  }
107
132
  if (e instanceof KiroTokenRefreshError &&
@@ -120,19 +145,19 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
120
145
  let prep = prepRequest(reductionFactor);
121
146
  const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
122
147
  if (config.enable_log_api_request && apiTimestamp) {
123
- let parsedBody = null;
148
+ let b = null;
124
149
  try {
125
- parsedBody = prep.init.body ? JSON.parse(prep.init.body) : null;
150
+ b = prep.init.body ? JSON.parse(prep.init.body) : null;
126
151
  }
127
152
  catch { }
128
153
  logger.logApiRequest({
129
154
  url: prep.url,
130
155
  method: prep.init.method,
131
156
  headers: prep.init.headers,
132
- body: parsedBody,
157
+ body: b,
133
158
  conversationId: prep.conversationId,
134
159
  model: prep.effectiveModel,
135
- email: acc.realEmail || acc.email
160
+ email: acc.email
136
161
  }, apiTimestamp);
137
162
  }
138
163
  try {
@@ -252,9 +277,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
252
277
  conversationId: prep.conversationId,
253
278
  model: prep.effectiveModel
254
279
  };
255
- let lastBody = null;
280
+ let lastB = null;
256
281
  try {
257
- lastBody = prep.init.body ? JSON.parse(prep.init.body) : null;
282
+ lastB = prep.init.body ? JSON.parse(prep.init.body) : null;
258
283
  }
259
284
  catch { }
260
285
  if (!config.enable_log_api_request)
@@ -262,10 +287,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
262
287
  url: prep.url,
263
288
  method: prep.init.method,
264
289
  headers: prep.init.headers,
265
- body: lastBody,
290
+ body: lastB,
266
291
  conversationId: prep.conversationId,
267
292
  model: prep.effectiveModel,
268
- email: acc.realEmail || acc.email
293
+ email: acc.email
269
294
  }, rData, logger.getTimestamp());
270
295
  throw new Error(`Kiro Error: ${res.status}`);
271
296
  }
@@ -294,11 +319,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
294
319
  const accounts = [];
295
320
  let startFresh = true;
296
321
  const existingAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
297
- const allAccounts = existingAm.getAccounts();
298
- const idcAccounts = allAccounts.filter((a) => a.authMethod === 'idc');
299
- if (idcAccounts.length > 0) {
300
- const existingAccounts = idcAccounts.map((acc, idx) => ({
301
- email: acc.realEmail || acc.email,
322
+ const idcAccs = existingAm.getAccounts().filter((a) => a.authMethod === 'idc');
323
+ if (idcAccs.length > 0) {
324
+ const existingAccounts = idcAccs.map((acc, idx) => ({
325
+ email: acc.email,
302
326
  index: idx
303
327
  }));
304
328
  startFresh = (await promptLoginMode(existingAccounts)) === 'fresh';
@@ -309,15 +333,29 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
309
333
  const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
310
334
  openBrowser(url);
311
335
  const res = await waitForAuth();
336
+ const u = await fetchUsageLimits({
337
+ refresh: '',
338
+ access: res.accessToken,
339
+ expires: res.expiresAt,
340
+ authMethod: 'idc',
341
+ region,
342
+ clientId: res.clientId,
343
+ clientSecret: res.clientSecret
344
+ });
345
+ if (!u.email) {
346
+ console.log('\n[Error] Failed to fetch account email. Skipping...\n');
347
+ continue;
348
+ }
312
349
  accounts.push(res);
313
350
  const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
314
351
  if (accounts.length === 1 && startFresh)
315
352
  am.getAccounts()
316
353
  .filter((a) => a.authMethod === 'idc')
317
354
  .forEach((a) => am.removeAccount(a));
355
+ const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
318
356
  const acc = {
319
- id: generateAccountId(),
320
- email: res.email,
357
+ id,
358
+ email: u.email,
321
359
  authMethod: 'idc',
322
360
  region,
323
361
  clientId: res.clientId,
@@ -326,39 +364,18 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
326
364
  accessToken: res.accessToken,
327
365
  expiresAt: res.expiresAt,
328
366
  rateLimitResetTime: 0,
329
- isHealthy: true
367
+ isHealthy: true,
368
+ failCount: 0
330
369
  };
331
- try {
332
- const u = await fetchUsageLimits({
333
- refresh: encodeRefreshToken({
334
- refreshToken: res.refreshToken,
335
- clientId: res.clientId,
336
- clientSecret: res.clientSecret,
337
- authMethod: 'idc'
338
- }),
339
- access: res.accessToken,
340
- expires: res.expiresAt,
341
- authMethod: 'idc',
342
- region,
343
- clientId: res.clientId,
344
- clientSecret: res.clientSecret,
345
- email: res.email
346
- });
347
- am.updateUsage(acc.id, {
348
- usedCount: u.usedCount,
349
- limitCount: u.limitCount,
350
- realEmail: u.email
351
- });
352
- }
353
- catch { }
354
370
  am.addAccount(acc);
371
+ am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
355
372
  await am.saveToDisk();
356
- showToast(`Account authenticated (${res.email})`, 'success');
373
+ console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
357
374
  if (!(await promptAddAnotherAccount(am.getAccountCount())))
358
375
  break;
359
376
  }
360
377
  catch (e) {
361
- showToast(`Failed: ${e.message}`, 'error');
378
+ console.log(`\n[Error] Login failed: ${e.message}\n`);
362
379
  break;
363
380
  }
364
381
  }
@@ -384,9 +401,21 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
384
401
  callback: async () => {
385
402
  try {
386
403
  const res = await waitForAuth(), am = await AccountManager.loadFromDisk(config.account_selection_strategy);
404
+ const u = await fetchUsageLimits({
405
+ refresh: '',
406
+ access: res.accessToken,
407
+ expires: res.expiresAt,
408
+ authMethod: 'idc',
409
+ region,
410
+ clientId: res.clientId,
411
+ clientSecret: res.clientSecret
412
+ });
413
+ if (!u.email)
414
+ throw new Error('No email');
415
+ const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
387
416
  const acc = {
388
- id: generateAccountId(),
389
- email: res.email,
417
+ id,
418
+ email: u.email,
390
419
  authMethod: 'idc',
391
420
  region,
392
421
  clientId: res.clientId,
@@ -395,9 +424,12 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
395
424
  accessToken: res.accessToken,
396
425
  expiresAt: res.expiresAt,
397
426
  rateLimitResetTime: 0,
398
- isHealthy: true
427
+ isHealthy: true,
428
+ failCount: 0
399
429
  };
400
430
  am.addAccount(acc);
431
+ if (u.email)
432
+ am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
401
433
  await am.saveToDisk();
402
434
  return { type: 'success', key: res.accessToken };
403
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhafron/opencode-kiro-auth",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",