@zhafron/opencode-kiro-auth 1.6.6 → 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
@@ -25,7 +25,6 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
25
25
  "plugin": ["@zhafron/opencode-kiro-auth"],
26
26
  "provider": {
27
27
  "kiro": {
28
- "npm": "@zhafron/opencode-kiro-auth",
29
28
  "models": {
30
29
  "claude-sonnet-4-5": {
31
30
  "name": "Claude Sonnet 4.5",
@@ -147,14 +146,56 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
147
146
  2. **Direct Authentication**:
148
147
  - Run `opencode auth login`.
149
148
  - Select `Other`, type `kiro`, and press enter.
150
- - 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`).
151
150
  - Leave it blank to sign in with **AWS Builder ID**.
152
151
  - Enter your company's Start URL (e.g. `https://your-company.awsapps.com/start`) to use **IAM Identity Center (SSO)**.
153
- - 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`.
154
158
  3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
155
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
+
156
186
  ## Troubleshooting
157
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
+
158
199
  ### Error: No accounts
159
200
 
160
201
  This happens when the plugin has no records in `~/.config/opencode/kiro.db`.
@@ -163,6 +204,10 @@ This happens when the plugin has no records in `~/.config/opencode/kiro.db`.
163
204
  2. Ensure `auto_sync_kiro_cli` is `true` in `~/.config/opencode/kiro.json`.
164
205
  3. Retry the request; the plugin will attempt a Kiro CLI sync when it detects zero accounts.
165
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
+
166
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.
167
212
 
168
213
  ### Error: ERR_INVALID_URL
@@ -190,14 +235,13 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
190
235
  "account_selection_strategy": "lowest-usage",
191
236
  "default_region": "us-east-1",
192
237
  "idc_start_url": "https://your-company.awsapps.com/start",
238
+ "idc_region": "us-east-1",
193
239
  "rate_limit_retry_delay_ms": 5000,
194
240
  "rate_limit_max_retries": 3,
195
241
  "max_request_iterations": 20,
196
242
  "request_timeout_ms": 120000,
197
243
  "token_expiry_buffer_ms": 120000,
198
244
  "usage_sync_max_retries": 3,
199
- "auth_server_port_start": 19847,
200
- "auth_server_port_range": 10,
201
245
  "usage_tracking_enabled": true,
202
246
  "enable_log_api_request": false
203
247
  }
@@ -208,15 +252,16 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
208
252
  - `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
209
253
  - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
210
254
  - `default_region`: AWS region (`us-east-1`, `us-west-2`).
211
- - `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`.
212
257
  - `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
213
258
  - `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10).
214
259
  - `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000).
215
260
  - `request_timeout_ms`: Request timeout in milliseconds (60000-600000ms).
216
261
  - `token_expiry_buffer_ms`: Token refresh buffer time (30000-300000ms).
217
262
  - `usage_sync_max_retries`: Retry attempts for usage sync (0-5).
218
- - `auth_server_port_start`: Starting port for auth server (1024-65535).
219
- - `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).
220
265
  - `usage_tracking_enabled`: Enable usage tracking and toast notifications.
221
266
  - `enable_log_api_request`: Enable detailed API request logging.
222
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;