@zhafron/opencode-kiro-auth 1.6.6 → 1.8.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 +53 -8
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +13 -0
- package/dist/core/account/account-selector.js +0 -9
- package/dist/core/auth/auth-handler.js +25 -1
- package/dist/core/auth/idc-auth-method.d.ts +2 -3
- package/dist/core/auth/idc-auth-method.js +106 -167
- package/dist/core/auth/token-refresher.js +11 -0
- package/dist/core/request/error-handler.js +39 -21
- package/dist/infrastructure/database/account-repository.js +1 -0
- package/dist/kiro/oauth-idc.js +20 -6
- package/dist/plugin/accounts.js +37 -7
- package/dist/plugin/config/schema.d.ts +6 -0
- package/dist/plugin/config/schema.js +2 -0
- package/dist/plugin/logger.js +16 -1
- package/dist/plugin/request.js +4 -2
- package/dist/plugin/storage/migrations.js +41 -3
- package/dist/plugin/storage/sqlite.js +8 -7
- package/dist/plugin/sync/kiro-cli-profile.d.ts +1 -0
- package/dist/plugin/sync/kiro-cli-profile.js +30 -0
- package/dist/plugin/sync/kiro-cli.js +36 -9
- package/dist/plugin/token.js +4 -1
- package/dist/plugin/types.d.ts +2 -0
- package/dist/plugin/usage.js +19 -4
- package/package.json +1 -1
- package/dist/plugin/cli.d.ts +0 -8
- package/dist/plugin/cli.js +0 -103
- package/dist/plugin/server.d.ts +0 -34
- package/dist/plugin/server.js +0 -362
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
|
-
-
|
|
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
|
-
-
|
|
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`:
|
|
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`:
|
|
219
|
-
- `auth_server_port_range`:
|
|
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
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
155
|
-
const startUrl = defaultStartUrl;
|
|
156
|
-
const u = await fetchUsageLimits({
|
|
91
|
+
usage = await fetchUsageLimits({
|
|
157
92
|
refresh: '',
|
|
158
|
-
access:
|
|
159
|
-
expires:
|
|
93
|
+
access: token.accessToken,
|
|
94
|
+
expires: token.expiresAt,
|
|
160
95
|
authMethod: 'idc',
|
|
161
|
-
region,
|
|
162
|
-
clientId:
|
|
163
|
-
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
|
-
|
|
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
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessTokenExpired } from '../../kiro/auth';
|
|
2
2
|
import { KiroTokenRefreshError } from '../../plugin/errors';
|
|
3
|
+
import * as logger from '../../plugin/logger';
|
|
3
4
|
import { refreshAccessToken } from '../../plugin/token';
|
|
4
5
|
export class TokenRefresher {
|
|
5
6
|
config;
|
|
@@ -27,6 +28,11 @@ export class TokenRefresher {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
async handleRefreshError(error, account, showToast) {
|
|
31
|
+
logger.error('Token refresh failed', {
|
|
32
|
+
email: account.email,
|
|
33
|
+
code: error instanceof KiroTokenRefreshError ? error.code : undefined,
|
|
34
|
+
message: error instanceof Error ? error.message : String(error)
|
|
35
|
+
});
|
|
30
36
|
if (this.config.auto_sync_kiro_cli) {
|
|
31
37
|
await this.syncFromKiroCli();
|
|
32
38
|
}
|
|
@@ -48,6 +54,11 @@ export class TokenRefresher {
|
|
|
48
54
|
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
49
55
|
return { account, shouldContinue: true };
|
|
50
56
|
}
|
|
57
|
+
logger.error('Token refresh unrecoverable', {
|
|
58
|
+
email: account.email,
|
|
59
|
+
code: error instanceof KiroTokenRefreshError ? error.code : undefined,
|
|
60
|
+
message: error instanceof Error ? error.message : String(error)
|
|
61
|
+
});
|
|
51
62
|
throw error;
|
|
52
63
|
}
|
|
53
64
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as logger from '../../plugin/logger';
|
|
1
2
|
export class ErrorHandler {
|
|
2
3
|
config;
|
|
3
4
|
accountManager;
|
|
@@ -8,13 +9,32 @@ export class ErrorHandler {
|
|
|
8
9
|
this.repository = repository;
|
|
9
10
|
}
|
|
10
11
|
async handle(error, response, account, context, showToast) {
|
|
11
|
-
if (response.status === 400
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
if (response.status === 400) {
|
|
13
|
+
const body = await response.text();
|
|
14
|
+
const errorData = (() => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(body);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
22
|
+
if (errorData?.reason === 'INVALID_MODEL_ID') {
|
|
23
|
+
throw new Error(`Invalid model: ${errorData.message}`);
|
|
24
|
+
}
|
|
25
|
+
logger.warn('HTTP 400 response body', {
|
|
26
|
+
body,
|
|
27
|
+
reductionFactor: context.reductionFactor,
|
|
28
|
+
email: account.email
|
|
29
|
+
});
|
|
30
|
+
if (context.reductionFactor > 0.4) {
|
|
31
|
+
const newFactor = context.reductionFactor - 0.2;
|
|
32
|
+
showToast(`Context too long. Retrying with ${Math.round(newFactor * 100)}%...`, 'warning');
|
|
33
|
+
return {
|
|
34
|
+
shouldRetry: true,
|
|
35
|
+
newContext: { ...context, reductionFactor: newFactor }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
18
38
|
}
|
|
19
39
|
if (response.status === 401 && context.retry < this.config.rate_limit_max_retries) {
|
|
20
40
|
return {
|
|
@@ -55,7 +75,6 @@ export class ErrorHandler {
|
|
|
55
75
|
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
56
76
|
const count = this.accountManager.getAccountCount();
|
|
57
77
|
if (count > 1) {
|
|
58
|
-
showToast(`Rate limited (${account.email}). Switching account...`, 'warning');
|
|
59
78
|
return { shouldRetry: true, switchAccount: true };
|
|
60
79
|
}
|
|
61
80
|
showToast(`Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
|
|
@@ -66,28 +85,27 @@ export class ErrorHandler {
|
|
|
66
85
|
this.accountManager.getAccountCount() > 1) {
|
|
67
86
|
let errorReason = response.status === 402 ? 'Quota' : 'Forbidden';
|
|
68
87
|
let isPermanent = false;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
throw new Error(`Invalid model: ${errorData.message}`);
|
|
88
|
+
const errorBody = await response.text();
|
|
89
|
+
const errorData = (() => {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(errorBody);
|
|
74
92
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
isPermanent = true;
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
78
95
|
}
|
|
96
|
+
})();
|
|
97
|
+
if (errorData?.reason === 'INVALID_MODEL_ID') {
|
|
98
|
+
throw new Error(`Invalid model: ${errorData.message}`);
|
|
79
99
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
100
|
+
if (errorData?.reason === 'TEMPORARILY_SUSPENDED') {
|
|
101
|
+
errorReason = 'Account Suspended';
|
|
102
|
+
isPermanent = true;
|
|
84
103
|
}
|
|
85
104
|
if (isPermanent) {
|
|
86
105
|
account.failCount = 10;
|
|
87
106
|
}
|
|
88
107
|
this.accountManager.markUnhealthy(account, errorReason);
|
|
89
108
|
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
90
|
-
showToast(`${errorReason} (${account.email}). Switching account...`, 'warning');
|
|
91
109
|
return { shouldRetry: true, switchAccount: true };
|
|
92
110
|
}
|
|
93
111
|
return { shouldRetry: false };
|