@zhafron/opencode-kiro-auth 1.1.1 → 1.1.3

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.
@@ -2,64 +2,74 @@ import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS, buildUrl, normalizeRegion } from '..
2
2
  export async function authorizeKiroIDC(region) {
3
3
  const effectiveRegion = normalizeRegion(region);
4
4
  const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
5
- const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
6
- method: 'POST',
7
- headers: {
8
- 'Content-Type': 'application/json',
9
- 'User-Agent': KIRO_CONSTANTS.USER_AGENT
10
- },
11
- body: JSON.stringify({
12
- clientName: 'Kiro IDE',
13
- clientType: 'public',
14
- scopes: KIRO_AUTH_SERVICE.SCOPES,
15
- grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
16
- })
17
- });
18
- if (!registerResponse.ok) {
19
- const errorText = await registerResponse.text().catch(() => '');
20
- throw new Error(`Client registration failed: ${registerResponse.status} ${errorText}`);
21
- }
22
- const registerData = await registerResponse.json();
23
- const { clientId, clientSecret } = registerData;
24
- if (!clientId || !clientSecret) {
25
- throw new Error('Client registration response missing clientId or clientSecret');
26
- }
27
- const deviceAuthResponse = await fetch(`${ssoOIDCEndpoint}/device_authorization`, {
28
- method: 'POST',
29
- headers: {
30
- 'Content-Type': 'application/json',
31
- 'User-Agent': KIRO_CONSTANTS.USER_AGENT
32
- },
33
- body: JSON.stringify({
5
+ try {
6
+ const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
11
+ },
12
+ body: JSON.stringify({
13
+ clientName: 'Kiro IDE',
14
+ clientType: 'public',
15
+ scopes: KIRO_AUTH_SERVICE.SCOPES,
16
+ grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
17
+ })
18
+ });
19
+ if (!registerResponse.ok) {
20
+ const errorText = await registerResponse.text().catch(() => '');
21
+ const error = new Error(`Client registration failed: ${registerResponse.status} ${errorText}`);
22
+ throw error;
23
+ }
24
+ const registerData = await registerResponse.json();
25
+ const { clientId, clientSecret } = registerData;
26
+ if (!clientId || !clientSecret) {
27
+ const error = new Error('Client registration response missing clientId or clientSecret');
28
+ throw error;
29
+ }
30
+ const deviceAuthResponse = await fetch(`${ssoOIDCEndpoint}/device_authorization`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'User-Agent': KIRO_CONSTANTS.USER_AGENT
35
+ },
36
+ body: JSON.stringify({
37
+ clientId,
38
+ clientSecret,
39
+ startUrl: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL
40
+ })
41
+ });
42
+ if (!deviceAuthResponse.ok) {
43
+ const errorText = await deviceAuthResponse.text().catch(() => '');
44
+ const error = new Error(`Device authorization failed: ${deviceAuthResponse.status} ${errorText}`);
45
+ throw error;
46
+ }
47
+ const deviceAuthData = await deviceAuthResponse.json();
48
+ const { verificationUri, verificationUriComplete, userCode, deviceCode, interval = 5, expiresIn = 600 } = deviceAuthData;
49
+ if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
50
+ const error = new Error('Device authorization response missing required fields');
51
+ throw error;
52
+ }
53
+ return {
54
+ verificationUrl: verificationUri,
55
+ verificationUriComplete,
56
+ userCode,
57
+ deviceCode,
34
58
  clientId,
35
59
  clientSecret,
36
- startUrl: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL
37
- })
38
- });
39
- if (!deviceAuthResponse.ok) {
40
- const errorText = await deviceAuthResponse.text().catch(() => '');
41
- throw new Error(`Device authorization failed: ${deviceAuthResponse.status} ${errorText}`);
60
+ interval,
61
+ expiresIn,
62
+ region: effectiveRegion
63
+ };
42
64
  }
43
- const deviceAuthData = await deviceAuthResponse.json();
44
- const { verificationUri, verificationUriComplete, userCode, deviceCode, interval = 5, expiresIn = 600 } = deviceAuthData;
45
- if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
46
- throw new Error('Device authorization response missing required fields');
65
+ catch (error) {
66
+ throw error;
47
67
  }
48
- return {
49
- verificationUrl: verificationUri,
50
- verificationUriComplete,
51
- userCode,
52
- deviceCode,
53
- clientId,
54
- clientSecret,
55
- interval,
56
- expiresIn,
57
- region: effectiveRegion
58
- };
59
68
  }
60
69
  export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, interval, expiresIn, region) {
61
70
  if (!clientId || !clientSecret || !deviceCode) {
62
- throw new Error('Missing required parameters for token polling');
71
+ const error = new Error('Missing required parameters for token polling');
72
+ throw error;
63
73
  }
64
74
  const effectiveRegion = normalizeRegion(region);
65
75
  const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
@@ -94,12 +104,15 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
94
104
  continue;
95
105
  }
96
106
  if (errorType === 'expired_token') {
97
- throw new Error('Device code has expired. Please restart the authorization process.');
107
+ const error = new Error('Device code has expired. Please restart the authorization process.');
108
+ throw error;
98
109
  }
99
110
  if (errorType === 'access_denied') {
100
- throw new Error('Authorization was denied by the user.');
111
+ const error = new Error('Authorization was denied by the user.');
112
+ throw error;
101
113
  }
102
- throw new Error(`Token polling failed: ${errorType} - ${tokenData.error_description || ''}`);
114
+ const error = new Error(`Token polling failed: ${errorType} - ${tokenData.error_description || ''}`);
115
+ throw error;
103
116
  }
104
117
  if (tokenData.accessToken && tokenData.refreshToken) {
105
118
  const expiresInSeconds = tokenData.expiresIn || 3600;
@@ -116,7 +129,8 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
116
129
  };
117
130
  }
118
131
  if (!tokenResponse.ok) {
119
- throw new Error(`Token request failed with status: ${tokenResponse.status}`);
132
+ const error = new Error(`Token request failed with status: ${tokenResponse.status}`);
133
+ throw error;
120
134
  }
121
135
  }
122
136
  catch (error) {
@@ -127,9 +141,11 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
127
141
  throw error;
128
142
  }
129
143
  if (attempts >= maxAttempts) {
130
- throw new Error(`Token polling failed after ${attempts} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
144
+ const finalError = new Error(`Token polling failed after ${attempts} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
145
+ throw finalError;
131
146
  }
132
147
  }
133
148
  }
134
- throw new Error('Token polling timed out. Authorization may have expired.');
149
+ const timeoutError = new Error('Token polling timed out. Authorization may have expired.');
150
+ throw timeoutError;
135
151
  }
@@ -6,11 +6,13 @@ export declare class AccountManager {
6
6
  private cursor;
7
7
  private strategy;
8
8
  private lastToastTime;
9
+ private lastUsageToastTime;
9
10
  constructor(accounts: ManagedAccount[], usage: Record<string, UsageMetadata>, strategy?: AccountSelectionStrategy);
10
11
  static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
11
12
  getAccountCount(): number;
12
13
  getAccounts(): ManagedAccount[];
13
14
  shouldShowToast(debounce?: number): boolean;
15
+ shouldShowUsageToast(debounce?: number): boolean;
14
16
  getMinWaitTime(): number;
15
17
  getCurrentOrNext(): ManagedAccount | null;
16
18
  updateUsage(id: string, meta: {
@@ -11,6 +11,7 @@ export class AccountManager {
11
11
  cursor;
12
12
  strategy;
13
13
  lastToastTime = 0;
14
+ lastUsageToastTime = 0;
14
15
  constructor(accounts, usage, strategy = 'sticky') {
15
16
  this.accounts = accounts;
16
17
  this.usage = usage;
@@ -46,6 +47,12 @@ export class AccountManager {
46
47
  this.lastToastTime = Date.now();
47
48
  return true;
48
49
  }
50
+ shouldShowUsageToast(debounce = 30000) {
51
+ if (Date.now() - this.lastUsageToastTime < debounce)
52
+ return false;
53
+ this.lastUsageToastTime = Date.now();
54
+ return true;
55
+ }
49
56
  getMinWaitTime() {
50
57
  const now = Date.now();
51
58
  const waits = this.accounts.map((a) => (a.rateLimitResetTime || 0) - now).filter((t) => t > 0);
@@ -1,5 +1,6 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { getIDCAuthHtml, getSuccessHtml, getErrorHtml } from './auth-page';
3
+ import * as logger from './logger';
3
4
  export function startIDCAuthServer(authData, port = 19847) {
4
5
  return new Promise((resolve, reject) => {
5
6
  let server = null;
@@ -50,11 +51,13 @@ export function startIDCAuthServer(authData, port = 19847) {
50
51
  });
51
52
  setTimeout(cleanup, 2000);
52
53
  }
53
- else if (d.error === 'authorization_pending')
54
+ else if (d.error === 'authorization_pending') {
54
55
  setTimeout(poll, authData.interval * 1000);
56
+ }
55
57
  else {
56
58
  status.status = 'failed';
57
59
  status.error = d.error_description || d.error;
60
+ logger.error(`Auth polling failed: ${status.error}`);
58
61
  if (rejector)
59
62
  rejector(new Error(status.error));
60
63
  setTimeout(cleanup, 2000);
@@ -63,6 +66,7 @@ export function startIDCAuthServer(authData, port = 19847) {
63
66
  catch (e) {
64
67
  status.status = 'failed';
65
68
  status.error = e.message;
69
+ logger.error(`Auth polling error: ${e.message}`, e);
66
70
  if (rejector)
67
71
  rejector(e);
68
72
  setTimeout(cleanup, 2000);
@@ -86,12 +90,14 @@ export function startIDCAuthServer(authData, port = 19847) {
86
90
  }
87
91
  });
88
92
  server.on('error', (e) => {
93
+ logger.error(`Auth server error on port ${port}`, e);
89
94
  cleanup();
90
95
  reject(e);
91
96
  });
92
97
  server.listen(port, '127.0.0.1', () => {
93
98
  timeoutId = setTimeout(() => {
94
99
  status.status = 'timeout';
100
+ logger.warn('Auth timeout waiting for authorization');
95
101
  if (rejector)
96
102
  rejector(new Error('Timeout'));
97
103
  cleanup();
@@ -1,15 +1,20 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { randomBytes } from 'node:crypto';
4
+ import { homedir } from 'node:os';
4
5
  import lockfile from 'proper-lockfile';
5
- import { xdgConfig } from 'xdg-basedir';
6
6
  import * as logger from './logger';
7
7
  const LOCK_OPTIONS = {
8
8
  stale: 10000,
9
9
  retries: { retries: 5, minTimeout: 100, maxTimeout: 1000, factor: 2 }
10
10
  };
11
11
  function getBaseDir() {
12
- return join(xdgConfig || join(process.env.HOME || '', '.config'), 'opencode');
12
+ const platform = process.platform;
13
+ if (platform === 'win32') {
14
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
15
+ }
16
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
17
+ return join(xdgConfig, 'opencode');
13
18
  }
14
19
  export function getStoragePath() {
15
20
  return join(getBaseDir(), 'kiro-accounts.json');
@@ -18,12 +23,24 @@ export function getUsagePath() {
18
23
  return join(getBaseDir(), 'kiro-usage.json');
19
24
  }
20
25
  async function withLock(path, fn) {
21
- await fs.mkdir(dirname(path), { recursive: true });
26
+ try {
27
+ await fs.mkdir(dirname(path), { recursive: true });
28
+ }
29
+ catch (error) {
30
+ logger.error(`Failed to create directory ${dirname(path)}`, error);
31
+ throw error;
32
+ }
22
33
  try {
23
34
  await fs.access(path);
24
35
  }
25
36
  catch {
26
- await fs.writeFile(path, '{}');
37
+ try {
38
+ await fs.writeFile(path, '{}');
39
+ }
40
+ catch (error) {
41
+ logger.error(`Failed to initialize file ${path}`, error);
42
+ throw error;
43
+ }
27
44
  }
28
45
  let release = null;
29
46
  try {
@@ -35,8 +52,14 @@ async function withLock(path, fn) {
35
52
  throw error;
36
53
  }
37
54
  finally {
38
- if (release)
39
- await release();
55
+ if (release) {
56
+ try {
57
+ await release();
58
+ }
59
+ catch (error) {
60
+ logger.warn(`Failed to release lock for ${path}`, error);
61
+ }
62
+ }
40
63
  }
41
64
  }
42
65
  export async function loadAccounts() {
@@ -50,11 +73,17 @@ export async function loadAccounts() {
50
73
  }
51
74
  export async function saveAccounts(storage) {
52
75
  const path = getStoragePath();
53
- await withLock(path, async () => {
54
- const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
55
- await fs.writeFile(tmp, JSON.stringify(storage, null, 2));
56
- await fs.rename(tmp, path);
57
- });
76
+ try {
77
+ await withLock(path, async () => {
78
+ const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
79
+ await fs.writeFile(tmp, JSON.stringify(storage, null, 2));
80
+ await fs.rename(tmp, path);
81
+ });
82
+ }
83
+ catch (error) {
84
+ logger.error(`Failed to save accounts to ${path}`, error);
85
+ throw error;
86
+ }
58
87
  }
59
88
  export async function loadUsage() {
60
89
  try {
@@ -67,9 +96,15 @@ export async function loadUsage() {
67
96
  }
68
97
  export async function saveUsage(storage) {
69
98
  const path = getUsagePath();
70
- await withLock(path, async () => {
71
- const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
72
- await fs.writeFile(tmp, JSON.stringify(storage, null, 2));
73
- await fs.rename(tmp, path);
74
- });
99
+ try {
100
+ await withLock(path, async () => {
101
+ const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
102
+ await fs.writeFile(tmp, JSON.stringify(storage, null, 2));
103
+ await fs.rename(tmp, path);
104
+ });
105
+ }
106
+ catch (error) {
107
+ logger.error(`Failed to save usage to ${path}`, error);
108
+ throw error;
109
+ }
75
110
  }
@@ -3,54 +3,64 @@ import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth';
3
3
  export async function refreshAccessToken(auth) {
4
4
  const url = `https://oidc.${auth.region}.amazonaws.com/token`;
5
5
  const p = decodeRefreshToken(auth.refresh);
6
- if (!p.clientId || !p.clientSecret)
6
+ if (!p.clientId || !p.clientSecret) {
7
7
  throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
8
+ }
8
9
  const requestBody = {
9
10
  refreshToken: p.refreshToken,
10
11
  clientId: p.clientId,
11
12
  clientSecret: p.clientSecret,
12
13
  grantType: 'refresh_token'
13
14
  };
14
- const res = await fetch(url, {
15
- method: 'POST',
16
- headers: {
17
- 'Content-Type': 'application/json',
18
- Accept: 'application/json',
19
- 'amz-sdk-request': 'attempt=1; max=1',
20
- 'x-amzn-kiro-agent-mode': 'vibe',
21
- Connection: 'close'
22
- },
23
- body: JSON.stringify(requestBody)
24
- });
25
- if (!res.ok) {
26
- const txt = await res.text();
27
- let data = {};
28
- try {
29
- data = JSON.parse(txt);
15
+ try {
16
+ const res = await fetch(url, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ Accept: 'application/json',
21
+ 'amz-sdk-request': 'attempt=1; max=1',
22
+ 'x-amzn-kiro-agent-mode': 'vibe',
23
+ Connection: 'close'
24
+ },
25
+ body: JSON.stringify(requestBody)
26
+ });
27
+ if (!res.ok) {
28
+ const txt = await res.text();
29
+ let data = {};
30
+ try {
31
+ data = JSON.parse(txt);
32
+ }
33
+ catch {
34
+ data = { message: txt };
35
+ }
36
+ throw new KiroTokenRefreshError(`Refresh failed: ${data.message || data.error_description || txt}`, data.error || `HTTP_${res.status}`);
30
37
  }
31
- catch {
32
- data = { message: txt };
38
+ const d = await res.json();
39
+ const acc = d.access_token || d.accessToken;
40
+ if (!acc) {
41
+ throw new KiroTokenRefreshError('No access token', 'INVALID_RESPONSE');
33
42
  }
34
- throw new KiroTokenRefreshError(`Refresh failed: ${data.message || data.error_description || txt}`, data.error || `HTTP_${res.status}`);
43
+ const upP = {
44
+ refreshToken: d.refresh_token || d.refreshToken || p.refreshToken,
45
+ clientId: p.clientId,
46
+ clientSecret: p.clientSecret,
47
+ authMethod: 'idc'
48
+ };
49
+ return {
50
+ refresh: encodeRefreshToken(upP),
51
+ access: acc,
52
+ expires: Date.now() + (d.expires_in || 3600) * 1000,
53
+ authMethod: 'idc',
54
+ region: auth.region,
55
+ clientId: auth.clientId,
56
+ clientSecret: auth.clientSecret,
57
+ email: auth.email
58
+ };
59
+ }
60
+ catch (error) {
61
+ if (error instanceof KiroTokenRefreshError) {
62
+ throw error;
63
+ }
64
+ throw new KiroTokenRefreshError(`Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', error instanceof Error ? error : undefined);
35
65
  }
36
- const d = await res.json();
37
- const acc = d.access_token || d.accessToken;
38
- if (!acc)
39
- throw new KiroTokenRefreshError('No access token', 'INVALID_RESPONSE');
40
- const upP = {
41
- refreshToken: d.refresh_token || d.refreshToken || p.refreshToken,
42
- clientId: p.clientId,
43
- clientSecret: p.clientSecret,
44
- authMethod: 'idc'
45
- };
46
- return {
47
- refresh: encodeRefreshToken(upP),
48
- access: acc,
49
- expires: Date.now() + (d.expires_in || 3600) * 1000,
50
- authMethod: 'idc',
51
- region: auth.region,
52
- clientId: auth.clientId,
53
- clientSecret: auth.clientSecret,
54
- email: auth.email
55
- };
56
66
  }
package/dist/plugin.js CHANGED
@@ -16,6 +16,13 @@ const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
16
16
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
17
  const isNetworkError = (e) => e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message);
18
18
  const extractModel = (url) => url.match(/models\/([^/:]+)/)?.[1] || null;
19
+ const formatUsageMessage = (usedCount, limitCount, email) => {
20
+ if (limitCount > 0) {
21
+ const percentage = Math.round((usedCount / limitCount) * 100);
22
+ return `Usage (${email}): ${usedCount}/${limitCount} (${percentage}%)`;
23
+ }
24
+ return `Usage (${email}): ${usedCount}`;
25
+ };
19
26
  export const createKiroPlugin = (id) => async ({ client, directory }) => {
20
27
  const config = loadConfig(directory);
21
28
  const showToast = (message, variant) => {
@@ -51,18 +58,25 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
51
58
  continue;
52
59
  }
53
60
  if (count > 1 && am.shouldShowToast())
54
- showToast(`Using ${acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
61
+ showToast(`Using ${acc.realEmail || acc.email} (${am.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
62
+ if (am.shouldShowUsageToast() &&
63
+ acc.usedCount !== undefined &&
64
+ acc.limitCount !== undefined) {
65
+ const percentage = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
66
+ const variant = percentage >= 80 ? 'warning' : 'info';
67
+ showToast(formatUsageMessage(acc.usedCount, acc.limitCount, acc.realEmail || acc.email), variant);
68
+ }
55
69
  let auth = am.toAuthDetails(acc);
56
70
  if (accessTokenExpired(auth)) {
57
71
  try {
58
- logger.log(`Refreshing token for ${acc.email}`);
72
+ logger.log(`Refreshing token for ${acc.realEmail || acc.email}`);
59
73
  auth = await refreshAccessToken(auth);
60
74
  am.updateFromAuth(acc, auth);
61
75
  await am.saveToDisk();
62
76
  }
63
77
  catch (e) {
64
78
  const msg = e instanceof KiroTokenRefreshError ? e.message : String(e);
65
- showToast(`Refresh failed for ${acc.email}: ${msg}`, 'error');
79
+ showToast(`Refresh failed for ${acc.realEmail || acc.email}: ${msg}`, 'error');
66
80
  if (e instanceof KiroTokenRefreshError && e.code === 'invalid_grant') {
67
81
  am.removeAccount(acc);
68
82
  await am.saveToDisk();
@@ -114,7 +128,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
114
128
  updateAccountQuota(acc, u, am);
115
129
  am.saveToDisk();
116
130
  })
117
- .catch((e) => logger.warn(`Usage sync failed for ${acc.email}: ${e.message}`));
131
+ .catch((e) => logger.warn(`Usage sync failed for ${acc.realEmail || acc.email}: ${e.message}`));
118
132
  if (prep.streaming) {
119
133
  const s = transformKiroStream(res, model, prep.conversationId);
120
134
  return new Response(new ReadableStream({
@@ -164,7 +178,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
164
178
  });
165
179
  }
166
180
  if (res.status === 401 && retry < config.rate_limit_max_retries) {
167
- logger.warn(`Unauthorized (401) on ${acc.email}, retrying...`);
181
+ logger.warn(`Unauthorized (401) on ${acc.realEmail || acc.email}, retrying...`);
168
182
  retry++;
169
183
  continue;
170
184
  }
@@ -173,7 +187,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
173
187
  am.markRateLimited(acc, wait);
174
188
  await am.saveToDisk();
175
189
  if (count > 1) {
176
- showToast(`Rate limited on ${acc.email}. Switching account...`, 'warning');
190
+ showToast(`Rate limited on ${acc.realEmail || acc.email}. Switching account...`, 'warning');
177
191
  continue;
178
192
  }
179
193
  else {
@@ -183,7 +197,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
183
197
  }
184
198
  }
185
199
  if ((res.status === 402 || res.status === 403) && count > 1) {
186
- showToast(`${res.status === 402 ? 'Quota exhausted' : 'Forbidden'} on ${acc.email}. Switching...`, 'warning');
200
+ showToast(`${res.status === 402 ? 'Quota exhausted' : 'Forbidden'} on ${acc.realEmail || acc.email}. Switching...`, 'warning');
187
201
  am.markUnhealthy(acc, res.status === 402 ? 'Quota' : 'Forbidden');
188
202
  await am.saveToDisk();
189
203
  continue;
@@ -232,65 +246,78 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
232
246
  type: 'oauth',
233
247
  authorize: async () => new Promise(async (resolve) => {
234
248
  const region = config.default_region;
235
- const authData = await authorizeKiroIDC(region);
236
- const { url, waitForAuth } = await startIDCAuthServer(authData);
237
- resolve({
238
- url,
239
- instructions: 'Opening browser...',
240
- method: 'auto',
241
- callback: async () => {
242
- try {
243
- const res = await waitForAuth();
244
- const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
245
- const acc = {
246
- id: generateAccountId(),
247
- email: res.email,
248
- authMethod: 'idc',
249
- region,
250
- clientId: res.clientId,
251
- clientSecret: res.clientSecret,
252
- refreshToken: res.refreshToken,
253
- accessToken: res.accessToken,
254
- expiresAt: res.expiresAt,
255
- rateLimitResetTime: 0,
256
- isHealthy: true
257
- };
249
+ try {
250
+ const authData = await authorizeKiroIDC(region);
251
+ const { url, waitForAuth } = await startIDCAuthServer(authData);
252
+ resolve({
253
+ url,
254
+ instructions: 'Opening browser...',
255
+ method: 'auto',
256
+ callback: async () => {
258
257
  try {
259
- const u = await fetchUsageLimits({
260
- refresh: encodeRefreshToken({
261
- refreshToken: res.refreshToken,
262
- clientId: res.clientId,
263
- clientSecret: res.clientSecret,
264
- authMethod: 'idc'
265
- }),
266
- access: res.accessToken,
267
- expires: res.expiresAt,
258
+ const res = await waitForAuth();
259
+ const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
260
+ const acc = {
261
+ id: generateAccountId(),
262
+ email: res.email,
268
263
  authMethod: 'idc',
269
264
  region,
270
265
  clientId: res.clientId,
271
266
  clientSecret: res.clientSecret,
272
- email: res.email
273
- });
274
- am.updateUsage(acc.id, {
275
- usedCount: u.usedCount,
276
- limitCount: u.limitCount,
277
- realEmail: u.email
278
- });
267
+ refreshToken: res.refreshToken,
268
+ accessToken: res.accessToken,
269
+ expiresAt: res.expiresAt,
270
+ rateLimitResetTime: 0,
271
+ isHealthy: true
272
+ };
273
+ try {
274
+ const u = await fetchUsageLimits({
275
+ refresh: encodeRefreshToken({
276
+ refreshToken: res.refreshToken,
277
+ clientId: res.clientId,
278
+ clientSecret: res.clientSecret,
279
+ authMethod: 'idc'
280
+ }),
281
+ access: res.accessToken,
282
+ expires: res.expiresAt,
283
+ authMethod: 'idc',
284
+ region,
285
+ clientId: res.clientId,
286
+ clientSecret: res.clientSecret,
287
+ email: res.email
288
+ });
289
+ am.updateUsage(acc.id, {
290
+ usedCount: u.usedCount,
291
+ limitCount: u.limitCount,
292
+ realEmail: u.email
293
+ });
294
+ }
295
+ catch (e) {
296
+ logger.warn(`Initial usage fetch failed: ${e.message}`, e);
297
+ }
298
+ am.addAccount(acc);
299
+ await am.saveToDisk();
300
+ showToast(`Successfully logged in as ${res.email}`, 'success');
301
+ return { type: 'success', key: res.accessToken };
279
302
  }
280
303
  catch (e) {
281
- logger.warn(`Initial usage fetch failed: ${e.message}`);
304
+ logger.error(`Login failed: ${e.message}`, e);
305
+ showToast(`Login failed: ${e.message}`, 'error');
306
+ return { type: 'failed' };
282
307
  }
283
- am.addAccount(acc);
284
- await am.saveToDisk();
285
- showToast(`Successfully logged in as ${res.email}`, 'success');
286
- return { type: 'success', key: res.accessToken };
287
- }
288
- catch (e) {
289
- logger.error(`Login failed: ${e.message}`);
290
- return { type: 'failed' };
291
308
  }
292
- }
293
- });
309
+ });
310
+ }
311
+ catch (e) {
312
+ logger.error(`Authorization failed: ${e.message}`, e);
313
+ showToast(`Authorization failed: ${e.message}`, 'error');
314
+ resolve({
315
+ url: '',
316
+ instructions: 'Authorization failed',
317
+ method: 'auto',
318
+ callback: async () => ({ type: 'failed' })
319
+ });
320
+ }
294
321
  })
295
322
  }
296
323
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhafron/opencode-kiro-auth",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,7 +32,6 @@
32
32
  "dependencies": {
33
33
  "@opencode-ai/plugin": "^0.15.30",
34
34
  "proper-lockfile": "^4.1.2",
35
- "xdg-basedir": "^5.1.0",
36
35
  "zod": "^3.24.0"
37
36
  },
38
37
  "devDependencies": {