@zhafron/opencode-kiro-auth 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -60,13 +51,13 @@ export class AccountManager {
60
51
  getAccounts() {
61
52
  return [...this.accounts];
62
53
  }
63
- shouldShowToast(debounce = 30000) {
54
+ shouldShowToast(debounce = 10000) {
64
55
  if (Date.now() - this.lastToastTime < debounce)
65
56
  return false;
66
57
  this.lastToastTime = Date.now();
67
58
  return true;
68
59
  }
69
- shouldShowUsageToast(debounce = 30000) {
60
+ shouldShowUsageToast(debounce = 10000) {
70
61
  if (Date.now() - this.lastUsageToastTime < debounce)
71
62
  return false;
72
63
  this.lastUsageToastTime = Date.now();
@@ -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;
@@ -106,7 +97,7 @@ export class AccountManager {
106
97
  }
107
98
  if (!selected) {
108
99
  const fallback = this.accounts
109
- .filter((a) => !a.isHealthy)
100
+ .filter((a) => !a.isHealthy && a.failCount < 3)
110
101
  .sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
111
102
  if (fallback) {
112
103
  fallback.isHealthy = true;
@@ -128,11 +119,11 @@ export class AccountManager {
128
119
  if (a) {
129
120
  a.usedCount = meta.usedCount;
130
121
  a.limitCount = meta.limitCount;
131
- if (meta.realEmail)
132
- a.realEmail = meta.realEmail;
122
+ if (meta.email)
123
+ a.email = meta.email;
124
+ a.failCount = 0;
125
+ kiroDb.upsertAccount(a);
133
126
  }
134
- this.usage[id] = { ...meta, lastSync: Date.now() };
135
- kiroDb.upsertUsage(id, this.usage[id]);
136
127
  }
137
128
  addAccount(a) {
138
129
  const i = this.accounts.findIndex((x) => x.id === a.id);
@@ -147,7 +138,6 @@ export class AccountManager {
147
138
  if (removedIndex === -1)
148
139
  return;
149
140
  this.accounts = this.accounts.filter((x) => x.id !== a.id);
150
- delete this.usage[a.id];
151
141
  kiroDb.deleteAccount(a.id);
152
142
  if (this.accounts.length === 0)
153
143
  this.cursor = 0;
@@ -162,14 +152,15 @@ export class AccountManager {
162
152
  acc.accessToken = auth.access;
163
153
  acc.expiresAt = auth.expires;
164
154
  acc.lastUsed = Date.now();
165
- if (auth.email && auth.email !== 'builder-id@aws.amazon.com')
166
- acc.realEmail = auth.email;
155
+ if (auth.email)
156
+ acc.email = auth.email;
167
157
  const p = decodeRefreshToken(auth.refresh);
168
158
  acc.refreshToken = p.refreshToken;
169
159
  if (p.profileArn)
170
160
  acc.profileArn = p.profileArn;
171
161
  if (p.clientId)
172
162
  acc.clientId = p.clientId;
163
+ acc.failCount = 0;
173
164
  kiroDb.upsertAccount(acc);
174
165
  writeToKiroCli(acc).catch(() => { });
175
166
  }
@@ -184,9 +175,13 @@ export class AccountManager {
184
175
  markUnhealthy(a, reason, recovery) {
185
176
  const acc = this.accounts.find((x) => x.id === a.id);
186
177
  if (acc) {
187
- acc.isHealthy = false;
178
+ acc.failCount = (acc.failCount || 0) + 1;
188
179
  acc.unhealthyReason = reason;
189
- acc.recoveryTime = recovery || Date.now() + 3600000;
180
+ acc.lastUsed = Date.now();
181
+ if (acc.failCount >= 3) {
182
+ acc.isHealthy = false;
183
+ acc.recoveryTime = recovery || Date.now() + 3600000;
184
+ }
190
185
  kiroDb.upsertAccount(acc);
191
186
  }
192
187
  }
@@ -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,8 +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
9
  close(): void;
13
10
  }
14
11
  export declare const kiroDb: KiroDatabase;
@@ -23,20 +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
31
- )
32
- `);
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
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
38
32
  )
39
33
  `);
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
+ }
40
86
  }
41
87
  getAccounts() {
42
88
  return this.db.prepare('SELECT * FROM accounts').all();
@@ -45,48 +91,26 @@ export class KiroDatabase {
45
91
  this.db
46
92
  .prepare(`
47
93
  INSERT INTO accounts (
48
- id, email, real_email, auth_method, region, client_id, client_secret,
94
+ id, email, auth_method, region, client_id, client_secret,
49
95
  profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
50
- is_healthy, unhealthy_reason, recovery_time, last_used
51
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
96
+ is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
97
+ used_count, limit_count, last_sync
98
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
52
99
  ON CONFLICT(id) DO UPDATE SET
53
- email=excluded.email, real_email=excluded.real_email, auth_method=excluded.auth_method,
100
+ email=excluded.email, auth_method=excluded.auth_method,
54
101
  region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
55
102
  profile_arn=excluded.profile_arn, refresh_token=excluded.refresh_token,
56
103
  access_token=excluded.access_token, expires_at=excluded.expires_at,
57
104
  rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
58
105
  unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
59
- 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
60
108
  `)
61
- .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);
62
110
  }
63
111
  deleteAccount(id) {
64
112
  this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
65
113
  }
66
- getUsage() {
67
- const rows = this.db.prepare('SELECT * FROM usage').all();
68
- const usage = {};
69
- for (const r of rows) {
70
- usage[r.account_id] = {
71
- usedCount: r.used_count,
72
- limitCount: r.limit_count,
73
- realEmail: r.real_email,
74
- lastSync: r.last_sync
75
- };
76
- }
77
- return usage;
78
- }
79
- upsertUsage(id, meta) {
80
- this.db
81
- .prepare(`
82
- INSERT INTO usage (account_id, used_count, limit_count, real_email, last_sync)
83
- VALUES (?, ?, ?, ?, ?)
84
- ON CONFLICT(account_id) DO UPDATE SET
85
- used_count=excluded.used_count, limit_count=excluded.limit_count,
86
- real_email=excluded.real_email, last_sync=excluded.last_sync
87
- `)
88
- .run(id, meta.usedCount, meta.limitCount, meta.realEmail || null, meta.lastSync);
89
- }
90
114
  close() {
91
115
  this.db.close();
92
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.is_healthy === 1 && 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();
@@ -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,13 +103,13 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
87
103
  }
88
104
  throw new Error('All accounts are unhealthy or rate-limited');
89
105
  }
90
- if (count > 1 && am.shouldShowToast())
91
- showToast(`Using ${acc.realEmail || acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
106
+ if (am.shouldShowToast())
107
+ showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
92
108
  if (am.shouldShowUsageToast() &&
93
109
  acc.usedCount !== undefined &&
94
110
  acc.limitCount !== undefined) {
95
111
  const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
96
- showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.realEmail || acc.email), p >= 80 ? 'warning' : 'info');
112
+ showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
97
113
  }
98
114
  const auth = am.toAuthDetails(acc);
99
115
  if (accessTokenExpired(auth, config.token_expiry_buffer_ms)) {
@@ -110,6 +126,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
110
126
  if (stillAcc &&
111
127
  !accessTokenExpired(refreshedAm.toAuthDetails(stillAcc), config.token_expiry_buffer_ms)) {
112
128
  showToast('Credentials recovered from Kiro CLI sync.', 'info');
129
+ acc = stillAcc;
113
130
  continue;
114
131
  }
115
132
  if (e instanceof KiroTokenRefreshError &&
@@ -128,19 +145,19 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
128
145
  let prep = prepRequest(reductionFactor);
129
146
  const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
130
147
  if (config.enable_log_api_request && apiTimestamp) {
131
- let parsedBody = null;
148
+ let b = null;
132
149
  try {
133
- parsedBody = prep.init.body ? JSON.parse(prep.init.body) : null;
150
+ b = prep.init.body ? JSON.parse(prep.init.body) : null;
134
151
  }
135
152
  catch { }
136
153
  logger.logApiRequest({
137
154
  url: prep.url,
138
155
  method: prep.init.method,
139
156
  headers: prep.init.headers,
140
- body: parsedBody,
157
+ body: b,
141
158
  conversationId: prep.conversationId,
142
159
  model: prep.effectiveModel,
143
- email: acc.realEmail || acc.email
160
+ email: acc.email
144
161
  }, apiTimestamp);
145
162
  }
146
163
  try {
@@ -260,9 +277,9 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
260
277
  conversationId: prep.conversationId,
261
278
  model: prep.effectiveModel
262
279
  };
263
- let lastBody = null;
280
+ let lastB = null;
264
281
  try {
265
- lastBody = prep.init.body ? JSON.parse(prep.init.body) : null;
282
+ lastB = prep.init.body ? JSON.parse(prep.init.body) : null;
266
283
  }
267
284
  catch { }
268
285
  if (!config.enable_log_api_request)
@@ -270,10 +287,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
270
287
  url: prep.url,
271
288
  method: prep.init.method,
272
289
  headers: prep.init.headers,
273
- body: lastBody,
290
+ body: lastB,
274
291
  conversationId: prep.conversationId,
275
292
  model: prep.effectiveModel,
276
- email: acc.realEmail || acc.email
293
+ email: acc.email
277
294
  }, rData, logger.getTimestamp());
278
295
  throw new Error(`Kiro Error: ${res.status}`);
279
296
  }
@@ -305,7 +322,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
305
322
  const idcAccs = existingAm.getAccounts().filter((a) => a.authMethod === 'idc');
306
323
  if (idcAccs.length > 0) {
307
324
  const existingAccounts = idcAccs.map((acc, idx) => ({
308
- email: acc.realEmail || acc.email,
325
+ email: acc.email,
309
326
  index: idx
310
327
  }));
311
328
  startFresh = (await promptLoginMode(existingAccounts)) === 'fresh';
@@ -316,15 +333,29 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
316
333
  const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
317
334
  openBrowser(url);
318
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
+ }
319
349
  accounts.push(res);
320
350
  const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
321
351
  if (accounts.length === 1 && startFresh)
322
352
  am.getAccounts()
323
353
  .filter((a) => a.authMethod === 'idc')
324
354
  .forEach((a) => am.removeAccount(a));
355
+ const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
325
356
  const acc = {
326
- id: generateAccountId(),
327
- email: res.email,
357
+ id,
358
+ email: u.email,
328
359
  authMethod: 'idc',
329
360
  region,
330
361
  clientId: res.clientId,
@@ -333,39 +364,18 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
333
364
  accessToken: res.accessToken,
334
365
  expiresAt: res.expiresAt,
335
366
  rateLimitResetTime: 0,
336
- isHealthy: true
367
+ isHealthy: true,
368
+ failCount: 0
337
369
  };
338
- try {
339
- const u = await fetchUsageLimits({
340
- refresh: encodeRefreshToken({
341
- refreshToken: res.refreshToken,
342
- clientId: res.clientId,
343
- clientSecret: res.clientSecret,
344
- authMethod: 'idc'
345
- }),
346
- access: res.accessToken,
347
- expires: res.expiresAt,
348
- authMethod: 'idc',
349
- region,
350
- clientId: res.clientId,
351
- clientSecret: res.clientSecret,
352
- email: res.email
353
- });
354
- am.updateUsage(acc.id, {
355
- usedCount: u.usedCount,
356
- limitCount: u.limitCount,
357
- realEmail: u.email
358
- });
359
- }
360
- catch { }
361
370
  am.addAccount(acc);
371
+ am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
362
372
  await am.saveToDisk();
363
- showToast(`Account authenticated (${res.email})`, 'success');
373
+ console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
364
374
  if (!(await promptAddAnotherAccount(am.getAccountCount())))
365
375
  break;
366
376
  }
367
377
  catch (e) {
368
- showToast(`Failed: ${e.message}`, 'error');
378
+ console.log(`\n[Error] Login failed: ${e.message}\n`);
369
379
  break;
370
380
  }
371
381
  }
@@ -391,9 +401,21 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
391
401
  callback: async () => {
392
402
  try {
393
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);
394
416
  const acc = {
395
- id: generateAccountId(),
396
- email: res.email,
417
+ id,
418
+ email: u.email,
397
419
  authMethod: 'idc',
398
420
  region,
399
421
  clientId: res.clientId,
@@ -402,9 +424,12 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
402
424
  accessToken: res.accessToken,
403
425
  expiresAt: res.expiresAt,
404
426
  rateLimitResetTime: 0,
405
- isHealthy: true
427
+ isHealthy: true,
428
+ failCount: 0
406
429
  };
407
430
  am.addAccount(acc);
431
+ if (u.email)
432
+ am.updateUsage(id, { usedCount: u.usedCount, limitCount: u.limitCount });
408
433
  await am.saveToDisk();
409
434
  return { type: 'success', key: res.accessToken };
410
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhafron/opencode-kiro-auth",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",