@zhafron/opencode-kiro-auth 1.6.5 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,14 +146,56 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
146
146
  2. **Direct Authentication**:
147
147
  - Run `opencode auth login`.
148
148
  - Select `Other`, type `kiro`, and press enter.
149
- - A browser page will open asking for your **IAM Identity Center Start URL**.
149
+ - You'll be prompted for your **IAM Identity Center Start URL** and **IAM Identity Center region** (`sso_region`).
150
150
  - Leave it blank to sign in with **AWS Builder ID**.
151
151
  - Enter your company's Start URL (e.g. `https://your-company.awsapps.com/start`) to use **IAM Identity Center (SSO)**.
152
- - You can also pre-configure the Start URL in `~/.config/opencode/kiro.json` via `idc_start_url` to skip the prompt.
152
+ - Note: the TUI `/connect` flow currently does **not** run plugin OAuth prompts (Start URL / region), so Identity Center logins may fall back to Builder ID unless you use `opencode auth login` (or preconfigure defaults in `~/.config/opencode/kiro.json`).
153
+ - For **IAM Identity Center**, you may also need a **profile ARN** (`profileArn`).
154
+ - If `kiro-cli` is installed and you've selected a profile once (`kiro-cli profile`), the plugin auto-detects it.
155
+ - Otherwise, set `idc_profile_arn` in `~/.config/opencode/kiro.json`.
156
+ - A browser window will open directly to AWS' verification URL (no local auth server). If it doesn't, copy/paste the URL and enter the code printed by OpenCode.
157
+ - You can also pre-configure defaults in `~/.config/opencode/kiro.json` via `idc_start_url` and `idc_region`.
153
158
  3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
154
159
 
160
+ ## Local plugin development
161
+
162
+ OpenCode installs plugins into a cache directory (typically `~/.cache/opencode/node_modules`).
163
+
164
+ The simplest way to test local changes (without publishing to npm) is to build this repo and hot-swap the cached plugin `dist/` folder:
165
+
166
+ 1. Build this repo: `bun run build` (or `npm run build`)
167
+ 2. Hot-swap `dist/` (creates a timestamped backup):
168
+
169
+ ```bash
170
+ PLUGIN_DIR="$HOME/.cache/opencode/node_modules/@zhafron/opencode-kiro-auth"
171
+ TS=$(date +%Y%m%d-%H%M%S)
172
+ cp -a "$PLUGIN_DIR/dist" "$PLUGIN_DIR/dist.bak.$TS"
173
+ rm -rf "$PLUGIN_DIR/dist"
174
+ cp -a "/absolute/path/to/opencode-kiro-auth/dist" "$PLUGIN_DIR/dist"
175
+ echo "Backup at: $PLUGIN_DIR/dist.bak.$TS"
176
+ ```
177
+
178
+ Revert:
179
+
180
+ ```bash
181
+ PLUGIN_DIR="$HOME/.cache/opencode/node_modules/@zhafron/opencode-kiro-auth"
182
+ rm -rf "$PLUGIN_DIR/dist"
183
+ mv "$PLUGIN_DIR/dist.bak.YYYYMMDD-HHMMSS" "$PLUGIN_DIR/dist"
184
+ ```
185
+
155
186
  ## Troubleshooting
156
187
 
188
+ ### Error: Status: 403 (AccessDeniedException / User is not authorized)
189
+
190
+ If you're using **IAM Identity Center** (a custom Start URL), the Q Developer / CodeWhisperer APIs typically require a **profile ARN**.
191
+
192
+ This plugin reads the active profile ARN from your local `kiro-cli` database (`state.key = api.codewhisperer.profile`) and sends it as `profileArn`.
193
+
194
+ Fix:
195
+
196
+ 1. Run `kiro-cli profile` and select a profile (e.g. `QDevProfile-us-east-1`).
197
+ 2. Retry `opencode auth login` (or restart OpenCode so it re-syncs).
198
+
157
199
  ### Error: No accounts
158
200
 
159
201
  This happens when the plugin has no records in `~/.config/opencode/kiro.db`.
@@ -162,6 +204,10 @@ This happens when the plugin has no records in `~/.config/opencode/kiro.db`.
162
204
  2. Ensure `auto_sync_kiro_cli` is `true` in `~/.config/opencode/kiro.json`.
163
205
  3. Retry the request; the plugin will attempt a Kiro CLI sync when it detects zero accounts.
164
206
 
207
+ ### Note: `/connect` vs `opencode auth login`
208
+
209
+ If you need to enter provider-specific values for an OAuth login (like IAM Identity Center Start URL / region), use `opencode auth login`. The current TUI `/connect` flow may not display plugin OAuth prompts, so it can’t collect those inputs.
210
+
165
211
  Note for IDC/SSO (ODIC): the plugin may temporarily create an account with a placeholder email if it cannot fetch the real email during sync (e.g. offline). It will replace it with the real email once usage/email lookup succeeds.
166
212
 
167
213
  ### Error: ERR_INVALID_URL
@@ -189,14 +235,13 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
189
235
  "account_selection_strategy": "lowest-usage",
190
236
  "default_region": "us-east-1",
191
237
  "idc_start_url": "https://your-company.awsapps.com/start",
238
+ "idc_region": "us-east-1",
192
239
  "rate_limit_retry_delay_ms": 5000,
193
240
  "rate_limit_max_retries": 3,
194
241
  "max_request_iterations": 20,
195
242
  "request_timeout_ms": 120000,
196
243
  "token_expiry_buffer_ms": 120000,
197
244
  "usage_sync_max_retries": 3,
198
- "auth_server_port_start": 19847,
199
- "auth_server_port_range": 10,
200
245
  "usage_tracking_enabled": true,
201
246
  "enable_log_api_request": false
202
247
  }
@@ -207,15 +252,16 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
207
252
  - `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
208
253
  - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
209
254
  - `default_region`: AWS region (`us-east-1`, `us-west-2`).
210
- - `idc_start_url`: Pre-configure your IAM Identity Center Start URL (e.g. `https://your-company.awsapps.com/start`). If set, the browser auth page will pre-fill this value. Leave unset to default to AWS Builder ID.
255
+ - `idc_start_url`: Default IAM Identity Center Start URL (e.g. `https://your-company.awsapps.com/start`). Leave unset/blank to default to AWS Builder ID.
256
+ - `idc_region`: IAM Identity Center (SSO OIDC) region (`sso_region`). Defaults to `us-east-1`.
211
257
  - `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
212
258
  - `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10).
213
259
  - `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000).
214
260
  - `request_timeout_ms`: Request timeout in milliseconds (60000-600000ms).
215
261
  - `token_expiry_buffer_ms`: Token refresh buffer time (30000-300000ms).
216
262
  - `usage_sync_max_retries`: Retry attempts for usage sync (0-5).
217
- - `auth_server_port_start`: Starting port for auth server (1024-65535).
218
- - `auth_server_port_range`: Number of ports to try (1-100).
263
+ - `auth_server_port_start`: Legacy/ignored (no local auth server).
264
+ - `auth_server_port_range`: Legacy/ignored (no local auth server).
219
265
  - `usage_tracking_enabled`: Enable usage tracking and toast notifications.
220
266
  - `enable_log_api_request`: Enable detailed API request logging.
221
267
 
@@ -2,6 +2,7 @@ import type { KiroRegion } from './plugin/types';
2
2
  export declare function isValidRegion(region: string): region is KiroRegion;
3
3
  export declare function normalizeRegion(region: string | undefined): KiroRegion;
4
4
  export declare function buildUrl(template: string, region: KiroRegion): string;
5
+ export declare function extractRegionFromArn(arn: string | undefined): KiroRegion | undefined;
5
6
  export declare const KIRO_CONSTANTS: {
6
7
  REFRESH_URL: string;
7
8
  REFRESH_IDC_URL: string;
package/dist/constants.js CHANGED
@@ -19,6 +19,19 @@ export function buildUrl(template, region) {
19
19
  throw new Error(`Invalid URL generated: ${url}`);
20
20
  }
21
21
  }
22
+ export function extractRegionFromArn(arn) {
23
+ if (!arn)
24
+ return undefined;
25
+ const parts = arn.split(':');
26
+ if (parts.length < 6)
27
+ return undefined;
28
+ if (parts[0] !== 'arn')
29
+ return undefined;
30
+ const region = parts[3];
31
+ if (typeof region !== 'string' || !region)
32
+ return undefined;
33
+ return isValidRegion(region) ? region : undefined;
34
+ }
22
35
  export const KIRO_CONSTANTS = {
23
36
  REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
24
37
  REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
@@ -37,15 +37,6 @@ export class AccountSelector {
37
37
  throw new Error('All accounts are unhealthy or rate-limited');
38
38
  }
39
39
  this.resetCircuitBreaker();
40
- if (this.accountManager.shouldShowToast()) {
41
- showToast(`Using ${acc.email} (${this.accountManager.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
42
- }
43
- if (this.accountManager.shouldShowUsageToast() &&
44
- acc.usedCount !== undefined &&
45
- acc.limitCount !== undefined) {
46
- const p = acc.limitCount > 0 ? (acc.usedCount / acc.limitCount) * 100 : 0;
47
- showToast(this.formatUsageMessage(acc.usedCount, acc.limitCount, acc.email), p >= 80 ? 'warning' : 'info');
48
- }
49
40
  return acc;
50
41
  }
51
42
  async handleEmptyAccounts() {
@@ -1,3 +1,5 @@
1
+ import { RegionSchema } from '../../plugin/config/schema.js';
2
+ import * as logger from '../../plugin/logger.js';
1
3
  import { IdcAuthMethod } from './idc-auth-method.js';
2
4
  export class AuthHandler {
3
5
  config;
@@ -9,8 +11,17 @@ export class AuthHandler {
9
11
  }
10
12
  async initialize() {
11
13
  const { syncFromKiroCli } = await import('../../plugin/sync/kiro-cli.js');
14
+ logger.log('Auth init', { autoSyncKiroCli: !!this.config.auto_sync_kiro_cli });
12
15
  if (this.config.auto_sync_kiro_cli) {
16
+ logger.log('Kiro CLI sync: start');
13
17
  await syncFromKiroCli();
18
+ this.repository.invalidateCache();
19
+ const accounts = await this.repository.findAll();
20
+ if (this.accountManager) {
21
+ for (const a of accounts)
22
+ this.accountManager.addAccount(a);
23
+ }
24
+ logger.log('Kiro CLI sync: done', { importedAccounts: accounts.length });
14
25
  }
15
26
  }
16
27
  setAccountManager(am) {
@@ -20,7 +31,7 @@ export class AuthHandler {
20
31
  if (!this.accountManager) {
21
32
  return [];
22
33
  }
23
- const idcMethod = new IdcAuthMethod(this.config, this.repository);
34
+ const idcMethod = new IdcAuthMethod(this.config, this.repository, this.accountManager);
24
35
  return [
25
36
  {
26
37
  label: 'AWS Builder ID / IAM Identity Center',
@@ -42,6 +53,19 @@ export class AuthHandler {
42
53
  return 'Please enter a valid URL';
43
54
  }
44
55
  }
56
+ },
57
+ {
58
+ type: 'text',
59
+ key: 'idc_region',
60
+ message: 'IAM Identity Center region (sso_region) (leave blank for us-east-1)',
61
+ placeholder: 'us-east-1',
62
+ validate: (value) => {
63
+ if (!value)
64
+ return undefined;
65
+ return RegionSchema.safeParse(value.trim()).success
66
+ ? undefined
67
+ : 'Please enter a valid AWS region';
68
+ }
45
69
  }
46
70
  ],
47
71
  authorize: (inputs) => idcMethod.authorize(inputs)
@@ -3,8 +3,7 @@ import type { AccountRepository } from '../../infrastructure/database/account-re
3
3
  export declare class IdcAuthMethod {
4
4
  private config;
5
5
  private repository;
6
- constructor(config: any, repository: AccountRepository);
6
+ private accountManager;
7
+ constructor(config: any, repository: AccountRepository, accountManager: any);
7
8
  authorize(inputs?: Record<string, string>): Promise<AuthOuathResult>;
8
- private handleMultipleLogin;
9
- private handleSingleLogin;
10
9
  }
@@ -1,8 +1,9 @@
1
1
  import { exec } from 'node:child_process';
2
+ import { extractRegionFromArn, normalizeRegion } from '../../constants.js';
3
+ import { authorizeKiroIDC, pollKiroIDCToken } from '../../kiro/oauth-idc.js';
2
4
  import { createDeterministicAccountId } from '../../plugin/accounts.js';
3
- import { promptAddAnotherAccount, promptDeleteAccount, promptLoginMode } from '../../plugin/cli.js';
4
5
  import * as logger from '../../plugin/logger.js';
5
- import { startIDCAuthServerWithInput } from '../../plugin/server.js';
6
+ import { readActiveProfileArnFromKiroCli } from '../../plugin/sync/kiro-cli-profile.js';
6
7
  import { fetchUsageLimits } from '../../plugin/usage.js';
7
8
  const openBrowser = (url) => {
8
9
  const escapedUrl = url.replace(/"/g, '\\"');
@@ -17,187 +18,125 @@ const openBrowser = (url) => {
17
18
  logger.warn(`Browser error: ${error.message}`);
18
19
  });
19
20
  };
21
+ function normalizeStartUrl(raw) {
22
+ if (!raw)
23
+ return undefined;
24
+ const trimmed = raw.trim();
25
+ if (!trimmed)
26
+ return undefined;
27
+ const url = new URL(trimmed);
28
+ url.hash = '';
29
+ url.search = '';
30
+ // Normalize common portal URL shapes to end in `/start` (AWS Builder ID and IAM Identity Center)
31
+ if (url.pathname.endsWith('/start/'))
32
+ url.pathname = url.pathname.replace(/\/start\/$/, '/start');
33
+ if (!url.pathname.endsWith('/start'))
34
+ url.pathname = url.pathname.replace(/\/+$/, '') + '/start';
35
+ return url.toString();
36
+ }
37
+ function buildDeviceUrl(startUrl, userCode) {
38
+ const url = new URL(startUrl);
39
+ url.search = '';
40
+ // Prefer `/start/` (with trailing slash) to match AWS portal URLs like `/start/#/device?...`.
41
+ if (url.pathname.endsWith('/start'))
42
+ url.pathname = `${url.pathname}/`;
43
+ url.pathname = url.pathname.replace(/\/start\/?$/, '/start/');
44
+ url.hash = `#/device?user_code=${encodeURIComponent(userCode)}`;
45
+ return url.toString();
46
+ }
20
47
  export class IdcAuthMethod {
21
48
  config;
22
49
  repository;
23
- constructor(config, repository) {
50
+ accountManager;
51
+ constructor(config, repository, accountManager) {
24
52
  this.config = config;
25
53
  this.repository = repository;
54
+ this.accountManager = accountManager;
26
55
  }
27
56
  async authorize(inputs) {
28
- return new Promise(async (resolve) => {
29
- const region = this.config.default_region;
30
- // inputs.start_url takes priority over config; browser input page will also allow override
31
- const defaultStartUrl = inputs?.start_url || this.config.idc_start_url;
32
- if (inputs) {
33
- await this.handleMultipleLogin(region, defaultStartUrl, resolve);
34
- }
35
- else {
36
- await this.handleSingleLogin(region, defaultStartUrl, resolve);
37
- }
57
+ const configuredServiceRegion = this.config.default_region;
58
+ const invokedWithoutPrompts = !inputs || Object.keys(inputs).length === 0;
59
+ const startUrl = normalizeStartUrl(inputs?.start_url || this.config.idc_start_url) || undefined;
60
+ const oidcRegion = normalizeRegion(inputs?.idc_region || this.config.idc_region);
61
+ const configuredProfileArn = this.config.idc_profile_arn;
62
+ logger.log('IDC authorize: resolved defaults', {
63
+ hasInputs: !!inputs && Object.keys(inputs).length > 0,
64
+ invokedWithoutPrompts,
65
+ startUrlSource: inputs?.start_url ? 'inputs' : this.config.idc_start_url ? 'config' : 'none',
66
+ oidcRegion,
67
+ startUrl: startUrl ? new URL(startUrl).origin : undefined
38
68
  });
39
- }
40
- async handleMultipleLogin(region, defaultStartUrl, resolve) {
41
- const accounts = [];
42
- let startFresh = true;
43
- while (true) {
44
- const existingAccounts = await this.repository.findAll();
45
- const idcAccs = existingAccounts.filter((a) => a.authMethod === 'idc');
46
- if (idcAccs.length === 0) {
47
- break;
48
- }
49
- const existingAccountsList = idcAccs.map((acc, idx) => ({
50
- email: acc.email,
51
- index: idx
52
- }));
53
- const mode = await promptLoginMode(existingAccountsList);
54
- if (mode === 'delete') {
55
- const deleteIndices = await promptDeleteAccount(existingAccountsList);
56
- if (deleteIndices !== null && deleteIndices.length > 0) {
57
- for (const idx of deleteIndices) {
58
- const accToDelete = idcAccs[idx];
59
- if (accToDelete) {
60
- await this.repository.delete(accToDelete.id);
61
- console.log(`[Success] Deleted: ${accToDelete.email}`);
62
- }
63
- }
64
- console.log(`\n[Success] Deleted ${deleteIndices.length} account(s)\n`);
65
- }
66
- continue;
67
- }
68
- if (mode === 'add') {
69
- startFresh = false;
70
- break;
71
- }
72
- if (mode === 'fresh') {
73
- startFresh = true;
74
- break;
75
- }
76
- }
77
- while (true) {
78
- try {
79
- const { url, waitForAuth } = await startIDCAuthServerWithInput(region, defaultStartUrl, this.config.auth_server_port_start, this.config.auth_server_port_range);
80
- openBrowser(url);
81
- const res = await waitForAuth();
82
- const startUrl = defaultStartUrl;
83
- const u = await fetchUsageLimits({
84
- refresh: '',
85
- access: res.accessToken,
86
- expires: res.expiresAt,
87
- authMethod: 'idc',
88
- region,
89
- clientId: res.clientId,
90
- clientSecret: res.clientSecret
91
- });
92
- if (!u.email) {
93
- console.log('\n[Error] Failed to fetch account email. Skipping...\n');
94
- continue;
95
- }
96
- accounts.push(res);
97
- if (accounts.length === 1 && startFresh) {
98
- const allAccounts = await this.repository.findAll();
99
- const idcAccountsToRemove = allAccounts.filter((a) => a.authMethod === 'idc');
100
- for (const acc of idcAccountsToRemove) {
101
- await this.repository.delete(acc.id);
102
- }
103
- }
104
- const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
105
- const acc = {
106
- id,
107
- email: u.email,
108
- authMethod: 'idc',
109
- region,
110
- clientId: res.clientId,
111
- clientSecret: res.clientSecret,
112
- startUrl: startUrl || undefined,
113
- refreshToken: res.refreshToken,
114
- accessToken: res.accessToken,
115
- expiresAt: res.expiresAt,
116
- rateLimitResetTime: 0,
117
- isHealthy: true,
118
- failCount: 0,
119
- usedCount: u.usedCount,
120
- limitCount: u.limitCount
121
- };
122
- await this.repository.save(acc);
123
- const currentCount = (await this.repository.findAll()).length;
124
- console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`);
125
- if (!(await promptAddAnotherAccount(currentCount)))
126
- break;
127
- }
128
- catch (e) {
129
- console.log(`\n[Error] Login failed: ${e.message}\n`);
130
- break;
131
- }
132
- }
133
- const finalAccounts = await this.repository.findAll();
134
- return resolve({
135
- url: '',
136
- instructions: `Complete (${finalAccounts.length} accounts).`,
69
+ // Step 1: get device code + verification URL (fast)
70
+ const auth = await authorizeKiroIDC(oidcRegion, startUrl);
71
+ // If a custom Identity Center start URL is provided, prefer the portal device page.
72
+ // This avoids the AWS Builder ID device page (which often prompts for an email)
73
+ // and routes the user into their org's IAM Identity Center sign-in.
74
+ const verificationUrl = startUrl
75
+ ? buildDeviceUrl(startUrl, auth.userCode)
76
+ : auth.verificationUriComplete || auth.verificationUrl;
77
+ // Open the *AWS* verification page directly (no local web server).
78
+ openBrowser(verificationUrl);
79
+ return {
80
+ url: verificationUrl,
81
+ instructions: `Open the verification URL and complete sign-in.\nCode: ${auth.userCode}`,
137
82
  method: 'auto',
138
- callback: async () => ({
139
- type: 'success',
140
- key: finalAccounts[0]?.accessToken || ''
141
- })
142
- });
143
- }
144
- async handleSingleLogin(region, defaultStartUrl, resolve) {
145
- try {
146
- const { url, waitForAuth } = await startIDCAuthServerWithInput(region, defaultStartUrl, this.config.auth_server_port_start, this.config.auth_server_port_range);
147
- openBrowser(url);
148
- resolve({
149
- url,
150
- instructions: `Open: ${url}`,
151
- method: 'auto',
152
- callback: async () => {
83
+ callback: async () => {
84
+ try {
85
+ // Step 2: poll until token is issued (standard device-code flow)
86
+ const token = await pollKiroIDCToken(auth.clientId, auth.clientSecret, auth.deviceCode, auth.interval, auth.expiresIn, oidcRegion);
87
+ const profileArn = configuredProfileArn || readActiveProfileArnFromKiroCli();
88
+ const serviceRegion = extractRegionFromArn(profileArn) || configuredServiceRegion;
89
+ let usage;
153
90
  try {
154
- const res = await waitForAuth();
155
- const startUrl = defaultStartUrl;
156
- const u = await fetchUsageLimits({
91
+ usage = await fetchUsageLimits({
157
92
  refresh: '',
158
- access: res.accessToken,
159
- expires: res.expiresAt,
93
+ access: token.accessToken,
94
+ expires: token.expiresAt,
160
95
  authMethod: 'idc',
161
- region,
162
- clientId: res.clientId,
163
- clientSecret: res.clientSecret
96
+ region: serviceRegion,
97
+ clientId: token.clientId,
98
+ clientSecret: token.clientSecret,
99
+ profileArn
164
100
  });
165
- if (!u.email)
166
- throw new Error('No email');
167
- const id = createDeterministicAccountId(u.email, 'idc', res.clientId);
168
- const acc = {
169
- id,
170
- email: u.email,
171
- authMethod: 'idc',
172
- region,
173
- clientId: res.clientId,
174
- clientSecret: res.clientSecret,
175
- startUrl: startUrl || undefined,
176
- refreshToken: res.refreshToken,
177
- accessToken: res.accessToken,
178
- expiresAt: res.expiresAt,
179
- rateLimitResetTime: 0,
180
- isHealthy: true,
181
- failCount: 0,
182
- usedCount: u.usedCount,
183
- limitCount: u.limitCount
184
- };
185
- await this.repository.save(acc);
186
- return { type: 'success', key: res.accessToken };
187
101
  }
188
102
  catch (e) {
189
- return { type: 'failed' };
103
+ if (startUrl && !profileArn) {
104
+ throw new Error(`Missing profile ARN for IAM Identity Center. Set "idc_profile_arn" in ~/.config/opencode/kiro.json, or run "kiro-cli profile" once so it can be auto-detected. Original error: ${e instanceof Error ? e.message : String(e)}`);
105
+ }
106
+ throw e;
190
107
  }
108
+ if (!usage.email)
109
+ return { type: 'failed' };
110
+ const id = createDeterministicAccountId(usage.email, 'idc', token.clientId, profileArn);
111
+ const acc = {
112
+ id,
113
+ email: usage.email,
114
+ authMethod: 'idc',
115
+ region: serviceRegion,
116
+ oidcRegion,
117
+ clientId: token.clientId,
118
+ clientSecret: token.clientSecret,
119
+ profileArn,
120
+ startUrl: startUrl || undefined,
121
+ refreshToken: token.refreshToken,
122
+ accessToken: token.accessToken,
123
+ expiresAt: token.expiresAt,
124
+ rateLimitResetTime: 0,
125
+ isHealthy: true,
126
+ failCount: 0,
127
+ usedCount: usage.usedCount,
128
+ limitCount: usage.limitCount
129
+ };
130
+ await this.repository.save(acc);
131
+ this.accountManager?.addAccount?.(acc);
132
+ return { type: 'success', key: token.accessToken };
133
+ }
134
+ catch (e) {
135
+ const err = e instanceof Error ? e : new Error(String(e));
136
+ logger.error('IDC auth callback failed', err);
137
+ throw new Error(`IDC authorization failed: ${err.message}. Check ~/.config/opencode/kiro-logs/plugin.log for details. If this is an Identity Center account, ensure you have selected an AWS Q Developer/CodeWhisperer profile (try: kiro-cli profile).`);
191
138
  }
192
- });
193
- }
194
- catch (e) {
195
- resolve({
196
- url: '',
197
- instructions: 'Failed',
198
- method: 'auto',
199
- callback: async () => ({ type: 'failed' })
200
- });
201
- }
139
+ }
140
+ };
202
141
  }
203
142
  }
@@ -55,7 +55,6 @@ export class ErrorHandler {
55
55
  await this.repository.batchSave(this.accountManager.getAccounts());
56
56
  const count = this.accountManager.getAccountCount();
57
57
  if (count > 1) {
58
- showToast(`Rate limited (${account.email}). Switching account...`, 'warning');
59
58
  return { shouldRetry: true, switchAccount: true };
60
59
  }
61
60
  showToast(`Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
@@ -87,7 +86,6 @@ export class ErrorHandler {
87
86
  }
88
87
  this.accountManager.markUnhealthy(account, errorReason);
89
88
  await this.repository.batchSave(this.accountManager.getAccounts());
90
- showToast(`${errorReason} (${account.email}). Switching account...`, 'warning');
91
89
  return { shouldRetry: true, switchAccount: true };
92
90
  }
93
91
  return { shouldRetry: false };
@@ -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,
@@ -227,6 +228,7 @@ export class AccountManager {
227
228
  expires: a.expiresAt,
228
229
  authMethod: a.authMethod,
229
230
  region: a.region,
231
+ oidcRegion: a.oidcRegion,
230
232
  profileArn: a.profileArn,
231
233
  clientId: a.clientId,
232
234
  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;