@kervnet/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
@@ -8,9 +8,9 @@ OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude Sonnet a
8
8
 
9
9
  ## Features
10
10
 
11
- - **Multiple Auth Methods**: Supports AWS Builder ID (IDC), Kiro Desktop (CLI-based), and IAM authentication.
11
+ - **Multiple Auth Methods**: Supports AWS Builder ID (IDC), Kiro Desktop (CLI-based), and AWS SSO authentication.
12
12
  - **Auto-Sync Kiro CLI**: Automatically imports and synchronizes active sessions from your local `kiro-cli` SQLite database.
13
- - **IAM Profile Support**: Automatically detects and uses IAM profiles configured via `kiro-cli login` with IAM Identity Center.
13
+ - **Auto-Sync AWS SSO**: Automatically imports credentials from `~/.aws/sso/cache` for seamless integration with AWS profiles.
14
14
  - **Gradual Context Truncation**: Intelligently prevents error 400 by reducing context size dynamically during retries.
15
15
  - **Intelligent Account Rotation**: Prioritizes multi-account usage based on lowest available quota.
16
16
  - **High-Performance Storage**: Efficient account and usage management using native Bun SQLite.
@@ -74,11 +74,15 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
74
74
  - Perform login directly in your terminal using `kiro-cli login`.
75
75
  - The plugin will automatically detect and import your session on startup.
76
76
  - For AWS IAM Identity Center (SSO/IDC), the plugin imports both the token and device registration (OIDC client credentials) from the `kiro-cli` database.
77
- 2. **Direct Authentication**:
77
+ 2. **Authentication via AWS SSO**:
78
+ - Ensure you have AWS SSO configured in `~/.aws/config` with active sessions.
79
+ - The plugin automatically imports credentials from `~/.aws/sso/cache` on startup.
80
+ - No additional configuration needed - just use your existing AWS SSO profiles.
81
+ 3. **Direct Authentication**:
78
82
  - Run `opencode auth login`.
79
83
  - Select `Other`, type `kiro`, and press enter.
80
84
  - Follow the instructions for **AWS Builder ID (IDC)**.
81
- 3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
85
+ 4. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
82
86
 
83
87
  ## Troubleshooting
84
88
 
@@ -99,6 +103,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
99
103
  ```json
100
104
  {
101
105
  "auto_sync_kiro_cli": true,
106
+ "auto_sync_aws_sso": true,
102
107
  "account_selection_strategy": "lowest-usage",
103
108
  "default_region": "us-east-1",
104
109
  "rate_limit_retry_delay_ms": 5000,
@@ -117,6 +122,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
117
122
  ### Configuration Options
118
123
 
119
124
  - `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
125
+ - `auto_sync_aws_sso`: Automatically sync credentials from AWS SSO cache (default: `true`).
120
126
  - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
121
127
  - `default_region`: AWS region (`us-east-1`, `us-west-2`).
122
128
  - `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
@@ -9,7 +9,7 @@ export declare class AuthHandler {
9
9
  getMethods(): Array<{
10
10
  id: string;
11
11
  label: string;
12
- type: 'oauth' | 'custom';
12
+ type: 'oauth';
13
13
  authorize: (inputs?: any) => Promise<any>;
14
14
  }>;
15
15
  }
@@ -10,9 +10,7 @@ export class AuthHandler {
10
10
  }
11
11
  async initialize() {
12
12
  const { syncFromKiroCli } = await import('../../plugin/sync/kiro-cli.js');
13
- if (this.config.auto_sync_kiro_cli) {
14
- await syncFromKiroCli();
15
- }
13
+ await syncFromKiroCli();
16
14
  }
17
15
  setAccountManager(am) {
18
16
  this.accountManager = am;
@@ -22,17 +20,17 @@ export class AuthHandler {
22
20
  return [];
23
21
  }
24
22
  const idcMethod = new IdcAuthMethod(this.config, this.repository);
25
- const cliMethod = new KiroCliAuthMethod(this.config, this.repository);
23
+ const kiroCliMethod = new KiroCliAuthMethod(this.config, this.repository);
26
24
  return [
27
25
  {
28
26
  id: 'kiro-cli',
29
- label: 'Use Kiro CLI Session',
30
- type: 'custom',
31
- authorize: () => cliMethod.authorize()
27
+ label: 'Kiro CLI (IAM Identity Center)',
28
+ type: 'oauth',
29
+ authorize: () => kiroCliMethod.authorize()
32
30
  },
33
31
  {
34
32
  id: 'idc',
35
- label: 'AWS Builder ID (New Login)',
33
+ label: 'AWS Builder ID (Direct)',
36
34
  type: 'oauth',
37
35
  authorize: (inputs) => idcMethod.authorize(inputs)
38
36
  }
@@ -1,9 +1,6 @@
1
- import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
2
1
  export declare class KiroCliAuthMethod {
3
2
  private config;
4
3
  private repository;
5
- constructor(config: any, repository: AccountRepository);
6
- authorize(): Promise<{
7
- success: boolean;
8
- }>;
4
+ constructor(config: any, repository: any);
5
+ authorize(): Promise<any>;
9
6
  }
@@ -1,4 +1,3 @@
1
- import { syncFromKiroCli } from '../../plugin/sync/kiro-cli.js';
2
1
  export class KiroCliAuthMethod {
3
2
  config;
4
3
  repository;
@@ -8,12 +7,17 @@ export class KiroCliAuthMethod {
8
7
  }
9
8
  async authorize() {
10
9
  // Sync from kiro-cli
10
+ const { syncFromKiroCli } = await import('../../plugin/sync/kiro-cli.js');
11
11
  await syncFromKiroCli();
12
- // Check if we have accounts
12
+ // Check if we have any accounts
13
13
  const accounts = await this.repository.findAll();
14
14
  if (accounts.length === 0) {
15
- throw new Error('No accounts found. Please run: kiro-cli login');
15
+ throw new Error('No Kiro CLI session found. Please run "kiro-cli login" first, then try again.');
16
16
  }
17
- return { success: true };
17
+ // Return success - no actual OAuth needed since we sync from kiro-cli
18
+ return {
19
+ success: true,
20
+ message: `Successfully synced ${accounts.length} account(s) from Kiro CLI`
21
+ };
18
22
  }
19
23
  }
@@ -1,4 +1,3 @@
1
- import { signRequestWithIAM } from '../../kiro/iam';
2
1
  import { isPermanentError } from '../../plugin/health';
3
2
  import * as logger from '../../plugin/logger';
4
3
  import { transformToCodeWhisperer } from '../../plugin/request';
@@ -76,15 +75,7 @@ export class RequestHandler {
76
75
  this.logRequest(prep, acc, apiTimestamp);
77
76
  }
78
77
  try {
79
- let finalInit = prep.init;
80
- if (auth.authMethod === 'iam' && auth.awsProfile) {
81
- const signedHeaders = await signRequestWithIAM(prep.url, prep.init.method || 'POST', prep.init.headers, prep.init.body, auth.awsProfile, auth.region);
82
- finalInit = {
83
- ...prep.init,
84
- headers: signedHeaders
85
- };
86
- }
87
- const res = await fetch(prep.url, finalInit);
78
+ const res = await fetch(prep.url, prep.init);
88
79
  if (apiTimestamp) {
89
80
  this.logResponse(res, prep, apiTimestamp);
90
81
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { KiroOAuthPlugin } from './plugin.js';
2
- export { KiroOAuthPlugin as default } from './plugin.js';
3
2
  export type { KiroConfig } from './plugin/config/index.js';
4
3
  export type { KiroAuthMethod, KiroRegion, ManagedAccount } from './plugin/types.js';
package/dist/index.js CHANGED
@@ -1,2 +1 @@
1
1
  export { KiroOAuthPlugin } from './plugin.js';
2
- export { KiroOAuthPlugin as default } from './plugin.js';
package/dist/kiro/auth.js CHANGED
@@ -6,8 +6,6 @@ export function decodeRefreshToken(refresh) {
6
6
  const authMethod = parts[parts.length - 1];
7
7
  if (authMethod === 'idc')
8
8
  return { refreshToken, clientId: parts[1], clientSecret: parts[2], authMethod: 'idc' };
9
- if (authMethod === 'iam')
10
- return { refreshToken, profileArn: parts[1], authMethod: 'iam' };
11
9
  if (authMethod === 'desktop')
12
10
  return { refreshToken, authMethod: 'desktop' };
13
11
  return { refreshToken, authMethod: 'desktop' };
@@ -23,8 +21,5 @@ export function encodeRefreshToken(parts) {
23
21
  throw new Error('Missing credentials');
24
22
  return `${parts.refreshToken}|${parts.clientId}|${parts.clientSecret}|idc`;
25
23
  }
26
- if (parts.authMethod === 'iam') {
27
- return `${parts.refreshToken}|${parts.profileArn || ''}|iam`;
28
- }
29
24
  return `${parts.refreshToken}|desktop`;
30
25
  }
@@ -30,7 +30,6 @@ export class AccountManager {
30
30
  clientId: r.client_id,
31
31
  clientSecret: r.client_secret,
32
32
  profileArn: r.profile_arn,
33
- awsProfile: r.aws_profile,
34
33
  refreshToken: r.refresh_token,
35
34
  accessToken: r.access_token,
36
35
  expiresAt: r.expires_at,
@@ -230,8 +229,7 @@ export class AccountManager {
230
229
  profileArn: a.profileArn,
231
230
  clientId: a.clientId,
232
231
  clientSecret: a.clientSecret,
233
- email: a.email,
234
- awsProfile: a.awsProfile
232
+ email: a.email
235
233
  };
236
234
  }
237
235
  }
@@ -17,6 +17,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
17
17
  auth_server_port_range: z.ZodDefault<z.ZodNumber>;
18
18
  usage_tracking_enabled: z.ZodDefault<z.ZodBoolean>;
19
19
  auto_sync_kiro_cli: z.ZodDefault<z.ZodBoolean>;
20
+ auto_sync_aws_sso: z.ZodDefault<z.ZodBoolean>;
20
21
  enable_log_api_request: z.ZodDefault<z.ZodBoolean>;
21
22
  }, "strip", z.ZodTypeAny, {
22
23
  account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
@@ -31,6 +32,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
31
32
  auth_server_port_range: number;
32
33
  usage_tracking_enabled: boolean;
33
34
  auto_sync_kiro_cli: boolean;
35
+ auto_sync_aws_sso: boolean;
34
36
  enable_log_api_request: boolean;
35
37
  $schema?: string | undefined;
36
38
  }, {
@@ -47,6 +49,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
47
49
  auth_server_port_range?: number | undefined;
48
50
  usage_tracking_enabled?: boolean | undefined;
49
51
  auto_sync_kiro_cli?: boolean | undefined;
52
+ auto_sync_aws_sso?: boolean | undefined;
50
53
  enable_log_api_request?: boolean | undefined;
51
54
  }>;
52
55
  export type KiroConfig = z.infer<typeof KiroConfigSchema>;
@@ -15,6 +15,7 @@ export const KiroConfigSchema = z.object({
15
15
  auth_server_port_range: z.number().min(1).max(100).default(10),
16
16
  usage_tracking_enabled: z.boolean().default(true),
17
17
  auto_sync_kiro_cli: z.boolean().default(true),
18
+ auto_sync_aws_sso: z.boolean().default(true),
18
19
  enable_log_api_request: z.boolean().default(false)
19
20
  });
20
21
  export const DEFAULT_CONFIG = {
@@ -30,5 +31,6 @@ export const DEFAULT_CONFIG = {
30
31
  auth_server_port_range: 10,
31
32
  usage_tracking_enabled: true,
32
33
  auto_sync_kiro_cli: true,
34
+ auto_sync_aws_sso: true,
33
35
  enable_log_api_request: false
34
36
  };
@@ -128,9 +128,6 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
128
128
  }
129
129
  }
130
130
  };
131
- if (auth.profileArn) {
132
- request.profileArn = auth.profileArn;
133
- }
134
131
  const toolUsesInHistory = history.flatMap((h) => h.assistantResponseMessage?.toolUses || []);
135
132
  const allToolUseIdsInHistory = new Set(toolUsesInHistory.map((tu) => tu.toolUseId));
136
133
  const finalCurTrs = [];
@@ -218,24 +215,21 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
218
215
  const osP = os.platform(), osR = os.release(), nodeV = process.version.replace('v', ''), kiroV = KIRO_CONSTANTS.KIRO_VERSION;
219
216
  const osN = osP === 'win32' ? `windows#${osR}` : osP === 'darwin' ? `macos#${osR}` : `${osP}#${osR}`;
220
217
  const ua = `aws-sdk-js/1.0.0 ua/2.1 os/${osN} lang/js md/nodejs#${nodeV} api/codewhispererruntime#1.0.0 m/E KiroIDE-${kiroV}-${machineId}`;
221
- const baseHeaders = {
222
- 'Content-Type': 'application/json',
223
- Accept: 'application/json',
224
- 'amz-sdk-invocation-id': crypto.randomUUID(),
225
- 'amz-sdk-request': 'attempt=1; max=1',
226
- 'x-amzn-kiro-agent-mode': 'vibe',
227
- 'x-amz-user-agent': `aws-sdk-js/1.0.0 KiroIDE-${kiroV}-${machineId}`,
228
- 'user-agent': ua,
229
- Connection: 'close'
230
- };
231
- if (auth.authMethod !== 'iam') {
232
- baseHeaders.Authorization = `Bearer ${auth.access}`;
233
- }
234
218
  return {
235
219
  url: KIRO_CONSTANTS.BASE_URL.replace('{{region}}', auth.region),
236
220
  init: {
237
221
  method: 'POST',
238
- headers: baseHeaders,
222
+ headers: {
223
+ 'Content-Type': 'application/json',
224
+ Accept: 'application/json',
225
+ Authorization: `Bearer ${auth.access}`,
226
+ 'amz-sdk-invocation-id': crypto.randomUUID(),
227
+ 'amz-sdk-request': 'attempt=1; max=1',
228
+ 'x-amzn-kiro-agent-mode': 'vibe',
229
+ 'x-amz-user-agent': `aws-sdk-js/1.0.0 KiroIDE-${kiroV}-${machineId}`,
230
+ 'user-agent': ua,
231
+ Connection: 'close'
232
+ },
239
233
  body: JSON.stringify(request)
240
234
  },
241
235
  streaming: true,
@@ -2,7 +2,6 @@ export function runMigrations(db) {
2
2
  migrateToUniqueRefreshToken(db);
3
3
  migrateRealEmailColumn(db);
4
4
  migrateUsageTable(db);
5
- migrateAwsProfileColumn(db);
6
5
  }
7
6
  function migrateToUniqueRefreshToken(db) {
8
7
  const hasIndex = db
@@ -108,10 +107,3 @@ function migrateUsageTable(db) {
108
107
  db.run('DROP TABLE usage');
109
108
  }
110
109
  }
111
- function migrateAwsProfileColumn(db) {
112
- const columns = db.prepare('PRAGMA table_info(accounts)').all();
113
- const names = new Set(columns.map((c) => c.name));
114
- if (!names.has('aws_profile')) {
115
- db.run('ALTER TABLE accounts ADD COLUMN aws_profile TEXT');
116
- }
117
- }
@@ -45,20 +45,20 @@ export class KiroDatabase {
45
45
  .prepare(`
46
46
  INSERT INTO accounts (
47
47
  id, email, auth_method, region, client_id, client_secret,
48
- profile_arn, aws_profile, refresh_token, access_token, expires_at, rate_limit_reset,
48
+ profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
49
49
  is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
50
50
  used_count, limit_count, last_sync
51
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
51
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
52
52
  ON CONFLICT(refresh_token) DO UPDATE SET
53
53
  id=excluded.id, email=excluded.email, auth_method=excluded.auth_method,
54
54
  region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
55
- profile_arn=excluded.profile_arn, aws_profile=excluded.aws_profile, access_token=excluded.access_token, expires_at=excluded.expires_at,
55
+ profile_arn=excluded.profile_arn, access_token=excluded.access_token, expires_at=excluded.expires_at,
56
56
  rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
57
57
  unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
58
58
  fail_count=excluded.fail_count, last_used=excluded.last_used,
59
59
  used_count=excluded.used_count, limit_count=excluded.limit_count, last_sync=excluded.last_sync
60
60
  `)
61
- .run(acc.id, acc.email, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.awsProfile || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
61
+ .run(acc.id, acc.email, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
62
62
  }
63
63
  async upsertAccount(acc) {
64
64
  await withDatabaseLock(this.path, async () => {
@@ -110,7 +110,6 @@ export class KiroDatabase {
110
110
  clientId: row.client_id,
111
111
  clientSecret: row.client_secret,
112
112
  profileArn: row.profile_arn,
113
- awsProfile: row.aws_profile,
114
113
  refreshToken: row.refresh_token,
115
114
  accessToken: row.access_token,
116
115
  expiresAt: row.expires_at,
@@ -0,0 +1,2 @@
1
+ import type { ManagedAccount } from '../types';
2
+ export declare function syncFromAwsSso(): Promise<ManagedAccount[]>;
@@ -0,0 +1,50 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { createDeterministicAccountId } from '../accounts';
5
+ import * as logger from '../logger';
6
+ export async function syncFromAwsSso() {
7
+ const accounts = [];
8
+ const ssoDir = join(homedir(), '.aws', 'sso', 'cache');
9
+ try {
10
+ const files = await readdir(ssoDir);
11
+ const jsonFiles = files.filter((f) => f.endsWith('.json') && !f.includes('.tmp'));
12
+ for (const file of jsonFiles) {
13
+ try {
14
+ const content = await readFile(join(ssoDir, file), 'utf-8');
15
+ const entry = JSON.parse(content);
16
+ if (!entry.accessToken || !entry.refreshToken)
17
+ continue;
18
+ const expiresAt = new Date(entry.expiresAt).getTime();
19
+ if (expiresAt < Date.now())
20
+ continue;
21
+ const id = createDeterministicAccountId(entry.startUrl, 'aws-sso', entry.clientId, undefined);
22
+ accounts.push({
23
+ id,
24
+ email: entry.startUrl,
25
+ authMethod: 'aws-sso',
26
+ region: (entry.region || 'us-east-1'),
27
+ clientId: entry.clientId,
28
+ clientSecret: entry.clientSecret,
29
+ refreshToken: entry.refreshToken,
30
+ accessToken: entry.accessToken,
31
+ expiresAt,
32
+ rateLimitResetTime: 0,
33
+ isHealthy: true,
34
+ failCount: 0,
35
+ lastUsed: Date.now(),
36
+ usedCount: 0,
37
+ limitCount: 0
38
+ });
39
+ }
40
+ catch (err) {
41
+ logger.debug('Failed to parse SSO cache file', { file, error: err });
42
+ }
43
+ }
44
+ logger.log(`Synced ${accounts.length} AWS SSO accounts`);
45
+ }
46
+ catch (err) {
47
+ logger.debug('Failed to read AWS SSO cache', { error: err });
48
+ }
49
+ return accounts;
50
+ }
@@ -4,10 +4,8 @@ import { createDeterministicAccountId } from '../accounts';
4
4
  import * as logger from '../logger';
5
5
  import { kiroDb } from '../storage/sqlite';
6
6
  import { fetchUsageLimits } from '../usage';
7
- import { syncIAMFromKiroCli } from './iam-cli';
8
7
  import { findClientCredsRecursive, getCliDbPath, makePlaceholderEmail, normalizeExpiresAt, safeJsonParse } from './kiro-cli-parser';
9
8
  export async function syncFromKiroCli() {
10
- await syncIAMFromKiroCli();
11
9
  const dbPath = getCliDbPath();
12
10
  if (!existsSync(dbPath))
13
11
  return;
@@ -15,6 +13,28 @@ export async function syncFromKiroCli() {
15
13
  const cliDb = new Database(dbPath, { readonly: true });
16
14
  cliDb.run('PRAGMA busy_timeout = 5000');
17
15
  const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
16
+ // Get profile ARN from state table
17
+ let profileArn;
18
+ let profileRegion;
19
+ try {
20
+ const stateRow = cliDb
21
+ .prepare("SELECT value FROM state WHERE key = 'api.codewhisperer.profile'")
22
+ .get();
23
+ if (stateRow?.value) {
24
+ const profileData = safeJsonParse(stateRow.value);
25
+ profileArn = profileData?.arn;
26
+ // Extract region from ARN: arn:aws:codewhisperer:REGION:...
27
+ if (profileArn) {
28
+ const arnParts = profileArn.split(':');
29
+ if (arnParts.length >= 4) {
30
+ profileRegion = arnParts[3];
31
+ }
32
+ }
33
+ }
34
+ }
35
+ catch (e) {
36
+ logger.debug('Could not read profile from state table', e);
37
+ }
18
38
  const deviceRegRow = rows.find((r) => typeof r?.key === 'string' && r.key.includes('device-registration'));
19
39
  const deviceReg = safeJsonParse(deviceRegRow?.value);
20
40
  const regCreds = deviceReg ? findClientCredsRecursive(deviceReg) : {};
@@ -23,11 +43,16 @@ export async function syncFromKiroCli() {
23
43
  const data = safeJsonParse(row.value);
24
44
  if (!data)
25
45
  continue;
46
+ const tokenExpiresAt = normalizeExpiresAt(data.expires_at ?? data.expiresAt) || Date.now() + 3600000;
47
+ // Skip expired tokens
48
+ if (tokenExpiresAt < Date.now()) {
49
+ logger.debug('Kiro CLI sync: skipping expired token', { key: row.key });
50
+ continue;
51
+ }
26
52
  const isIdc = row.key.includes('odic');
27
53
  const authMethod = isIdc ? 'idc' : 'desktop';
28
- // Force us-east-1 for Kiro - ca-central-1 not supported
29
- const region = 'us-east-1';
30
- const profileArn = data.profile_arn || data.profileArn;
54
+ const region = profileRegion || data.region || 'us-east-1';
55
+ const tokenProfileArn = data.profile_arn || data.profileArn || profileArn;
31
56
  const accessToken = data.access_token || data.accessToken || '';
32
57
  const refreshToken = data.refresh_token || data.refreshToken;
33
58
  if (!refreshToken)
@@ -38,7 +63,6 @@ export async function syncFromKiroCli() {
38
63
  logger.warn('Kiro CLI sync: missing IDC device credentials; skipping token import');
39
64
  continue;
40
65
  }
41
- const cliExpiresAt = normalizeExpiresAt(data.expires_at ?? data.expiresAt) || Date.now() + 3600000;
42
66
  let usedCount = 0;
43
67
  let limitCount = 0;
44
68
  let email;
@@ -47,10 +71,10 @@ export async function syncFromKiroCli() {
47
71
  const authForUsage = {
48
72
  refresh: '',
49
73
  access: accessToken,
50
- expires: cliExpiresAt,
74
+ expires: tokenExpiresAt,
51
75
  authMethod,
52
76
  region,
53
- profileArn,
77
+ profileArn: tokenProfileArn,
54
78
  clientId,
55
79
  clientSecret,
56
80
  email: ''
@@ -73,8 +97,8 @@ export async function syncFromKiroCli() {
73
97
  const all = kiroDb.getAccounts();
74
98
  if (!email) {
75
99
  let existing;
76
- if (profileArn) {
77
- existing = all.find((a) => a.auth_method === authMethod && a.profile_arn === profileArn);
100
+ if (tokenProfileArn) {
101
+ existing = all.find((a) => a.auth_method === authMethod && a.profile_arn === tokenProfileArn);
78
102
  }
79
103
  if (!existing && authMethod === 'idc' && clientId) {
80
104
  existing = all.find((a) => a.auth_method === 'idc' && a.client_id === clientId);
@@ -83,19 +107,19 @@ export async function syncFromKiroCli() {
83
107
  email = existing.email;
84
108
  }
85
109
  else {
86
- email = makePlaceholderEmail(authMethod, region, clientId, profileArn);
110
+ email = makePlaceholderEmail(authMethod, region, clientId, tokenProfileArn);
87
111
  }
88
112
  }
89
- const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId, profileArn);
90
- const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, profileArn);
113
+ const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId, tokenProfileArn);
114
+ const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, tokenProfileArn);
91
115
  const existingById = all.find((a) => a.id === id);
92
116
  if (existingById &&
93
117
  existingById.is_healthy === 1 &&
94
- existingById.expires_at >= cliExpiresAt)
118
+ existingById.expires_at >= tokenExpiresAt)
95
119
  continue;
96
120
  if (usageOk) {
97
- const placeholderEmail = makePlaceholderEmail(authMethod, region, clientId, profileArn);
98
- const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, profileArn);
121
+ const placeholderEmail = makePlaceholderEmail(authMethod, region, clientId, tokenProfileArn);
122
+ const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, tokenProfileArn);
99
123
  if (placeholderId !== id) {
100
124
  const placeholderRow = all.find((a) => a.id === placeholderId);
101
125
  if (placeholderRow) {
@@ -106,10 +130,10 @@ export async function syncFromKiroCli() {
106
130
  region: placeholderRow.region || region,
107
131
  clientId,
108
132
  clientSecret,
109
- profileArn,
133
+ profileArn: tokenProfileArn,
110
134
  refreshToken: placeholderRow.refresh_token || refreshToken,
111
135
  accessToken: placeholderRow.access_token || accessToken,
112
- expiresAt: placeholderRow.expires_at || cliExpiresAt,
136
+ expiresAt: placeholderRow.expires_at || tokenExpiresAt,
113
137
  rateLimitResetTime: 0,
114
138
  isHealthy: false,
115
139
  failCount: 10,
@@ -129,10 +153,10 @@ export async function syncFromKiroCli() {
129
153
  region,
130
154
  clientId,
131
155
  clientSecret,
132
- profileArn,
156
+ profileArn: tokenProfileArn,
133
157
  refreshToken,
134
158
  accessToken,
135
- expiresAt: cliExpiresAt,
159
+ expiresAt: tokenExpiresAt,
136
160
  rateLimitResetTime: 0,
137
161
  isHealthy: true,
138
162
  failCount: 0,
@@ -2,22 +2,16 @@ import crypto from 'node:crypto';
2
2
  import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth';
3
3
  import { KiroTokenRefreshError } from './errors';
4
4
  export async function refreshAccessToken(auth) {
5
- if (auth.authMethod === 'iam') {
6
- return {
7
- ...auth,
8
- access: 'iam-signed',
9
- expires: Date.now() + 3600000
10
- };
11
- }
12
5
  const p = decodeRefreshToken(auth.refresh);
13
6
  const isIdc = auth.authMethod === 'idc';
14
- const url = isIdc
7
+ const isAwsSso = auth.authMethod === 'aws-sso';
8
+ const url = isIdc || isAwsSso
15
9
  ? `https://oidc.${auth.region}.amazonaws.com/token`
16
10
  : `https://prod.${auth.region}.auth.desktop.kiro.dev/refreshToken`;
17
- if (isIdc && (!p.clientId || !p.clientSecret)) {
11
+ if ((isIdc || isAwsSso) && (!p.clientId || !p.clientSecret)) {
18
12
  throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
19
13
  }
20
- const requestBody = isIdc
14
+ const requestBody = isIdc || isAwsSso
21
15
  ? {
22
16
  refreshToken: p.refreshToken,
23
17
  clientId: p.clientId,
@@ -31,7 +25,7 @@ export async function refreshAccessToken(auth) {
31
25
  .createHash('sha256')
32
26
  .update(auth.profileArn || auth.clientId || 'KIRO_DEFAULT_MACHINE')
33
27
  .digest('hex');
34
- const ua = isIdc ? 'aws-sdk-js/1.0.0' : `KiroIDE-0.7.45-${machineId}`;
28
+ const ua = isIdc || isAwsSso ? 'aws-sdk-js/1.0.0' : `KiroIDE-0.7.45-${machineId}`;
35
29
  try {
36
30
  const res = await fetch(url, {
37
31
  method: 'POST',
@@ -1,4 +1,4 @@
1
- export type KiroAuthMethod = 'idc' | 'desktop' | 'iam';
1
+ export type KiroAuthMethod = 'idc' | 'desktop' | 'aws-sso';
2
2
  export type KiroRegion = 'us-east-1' | 'us-west-2';
3
3
  export interface KiroAuthDetails {
4
4
  refresh: string;
@@ -10,7 +10,6 @@ export interface KiroAuthDetails {
10
10
  clientSecret?: string;
11
11
  email?: string;
12
12
  profileArn?: string;
13
- awsProfile?: string;
14
13
  }
15
14
  export interface RefreshParts {
16
15
  refreshToken: string;
@@ -27,7 +26,6 @@ export interface ManagedAccount {
27
26
  clientId?: string;
28
27
  clientSecret?: string;
29
28
  profileArn?: string;
30
- awsProfile?: string;
31
29
  refreshToken: string;
32
30
  accessToken: string;
33
31
  expiresAt: number;
package/dist/plugin.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare const createKiroPlugin: (id: string) => ({ client, directory }: a
9
9
  methods: {
10
10
  id: string;
11
11
  label: string;
12
- type: "oauth" | "custom";
12
+ type: "oauth";
13
13
  authorize: (inputs?: any) => Promise<any>;
14
14
  }[];
15
15
  };
@@ -25,7 +25,7 @@ export declare const KiroOAuthPlugin: ({ client, directory }: any) => Promise<{
25
25
  methods: {
26
26
  id: string;
27
27
  label: string;
28
- type: "oauth" | "custom";
28
+ type: "oauth";
29
29
  authorize: (inputs?: any) => Promise<any>;
30
30
  }[];
31
31
  };
package/dist/plugin.js CHANGED
@@ -15,28 +15,17 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
15
15
  const authHandler = new AuthHandler(config, repository);
16
16
  const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy);
17
17
  authHandler.setAccountManager(accountManager);
18
+ // Sync AWS SSO before OpenCode checks for accounts
19
+ await authHandler.initialize();
18
20
  const requestHandler = new RequestHandler(accountManager, config, repository);
19
21
  return {
20
22
  auth: {
21
23
  provider: id,
22
24
  loader: async (getAuth) => {
23
- // Check if we already have accounts synced from kiro-cli
24
- const accounts = accountManager.getAccounts();
25
- if (accounts.length === 0) {
26
- // No accounts, need to authenticate
27
- await getAuth();
28
- }
29
- else {
30
- // We have accounts, skip auth prompt
31
- console.log('[KIRO] Using existing account:', accounts[0]?.email || 'unknown');
32
- }
33
- await authHandler.initialize();
34
- const region = config?.default_region || 'us-east-1';
35
- const baseURL = `https://q.${region}.amazonaws.com`;
36
- console.log('[KIRO] Loader called - baseURL:', baseURL, 'region:', region);
25
+ await getAuth();
37
26
  return {
38
27
  apiKey: '',
39
- baseURL,
28
+ baseURL: 'https://q.us-east-1.amazonaws.com',
40
29
  fetch: (input, init) => requestHandler.handle(input, init, showToast)
41
30
  };
42
31
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kervnet/opencode-kiro-auth",
3
- "version": "1.6.5",
4
- "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models with IAM auth support",
3
+ "version": "1.7.0",
4
+ "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) with IAM Identity Center profile support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -21,22 +21,17 @@
21
21
  "ai",
22
22
  "auth"
23
23
  ],
24
- "author": "kervnet",
24
+ "author": "tickernelz",
25
25
  "license": "MIT",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "git+https://github.com/kervnet/opencode-kiro-auth.git"
28
+ "url": "git+https://github.com/tickernelz/opencode-kiro-auth.git"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@aws-crypto/sha256-js": "^5.2.0",
35
- "@aws-sdk/credential-providers": "^3.978.0",
36
- "@aws-sdk/signature-v4": "^3.370.0",
37
34
  "@opencode-ai/plugin": "^0.15.30",
38
- "@smithy/protocol-http": "^5.3.8",
39
- "@smithy/signature-v4": "^5.3.8",
40
35
  "proper-lockfile": "^4.1.2",
41
36
  "zod": "^3.24.0"
42
37
  },
@@ -1,4 +0,0 @@
1
- import type { KiroAuthDetails } from '../plugin/types';
2
- export declare function getIAMCredentials(profileName: string, region: string): Promise<import("@smithy/types").AwsCredentialIdentity>;
3
- export declare function signRequestWithIAM(url: string, method: string, headers: Record<string, string>, body: string, profileName: string, region: string): Promise<Record<string, string>>;
4
- export declare function isIAMAuth(auth: KiroAuthDetails): boolean;
package/dist/kiro/iam.js DELETED
@@ -1,34 +0,0 @@
1
- import { fromIni } from '@aws-sdk/credential-providers';
2
- import { SignatureV4 } from '@smithy/signature-v4';
3
- import { Sha256 } from '@aws-crypto/sha256-js';
4
- import { HttpRequest } from '@smithy/protocol-http';
5
- export async function getIAMCredentials(profileName, region) {
6
- const credentials = fromIni({ profile: profileName });
7
- return await credentials();
8
- }
9
- export async function signRequestWithIAM(url, method, headers, body, profileName, region) {
10
- const credentials = await getIAMCredentials(profileName, region);
11
- const parsedUrl = new URL(url);
12
- const request = new HttpRequest({
13
- method,
14
- protocol: parsedUrl.protocol,
15
- hostname: parsedUrl.hostname,
16
- path: parsedUrl.pathname + parsedUrl.search,
17
- headers: {
18
- ...headers,
19
- host: parsedUrl.hostname,
20
- },
21
- body,
22
- });
23
- const signer = new SignatureV4({
24
- credentials,
25
- region,
26
- service: 'codewhisperer',
27
- sha256: Sha256,
28
- });
29
- const signedRequest = await signer.sign(request);
30
- return signedRequest.headers;
31
- }
32
- export function isIAMAuth(auth) {
33
- return auth.authMethod === 'iam';
34
- }
@@ -1 +0,0 @@
1
- export declare function syncIAMFromKiroCli(): Promise<void>;
@@ -1,66 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { createDeterministicAccountId } from '../accounts';
3
- import * as logger from '../logger';
4
- import { kiroDb } from '../storage/sqlite';
5
- export async function syncIAMFromKiroCli() {
6
- try {
7
- // Check if AWS credentials are configured
8
- const { existsSync } = await import('node:fs');
9
- const { homedir } = await import('node:os');
10
- const { join } = await import('node:path');
11
- const awsConfigPath = join(homedir(), '.aws', 'config');
12
- const awsCredsPath = join(homedir(), '.aws', 'credentials');
13
- if (!existsSync(awsConfigPath) && !existsSync(awsCredsPath)) {
14
- logger.debug('IAM sync: No AWS credentials configured, skipping IAM auth');
15
- return;
16
- }
17
- const output = execSync('kiro-cli whoami', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
18
- const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
19
- let profileName;
20
- let profileArn;
21
- let region = 'us-east-1';
22
- for (const line of lines) {
23
- if (line.startsWith('arn:aws:codewhisperer:')) {
24
- profileArn = line;
25
- const match = line.match(/:([a-z]+-[a-z]+-\d+):/);
26
- if (match && match[1])
27
- region = match[1];
28
- }
29
- else if (!line.includes('Logged in') && !line.includes('Profile:') && !line.startsWith('arn:') && !line.startsWith('http')) {
30
- profileName = line;
31
- }
32
- }
33
- if (!profileArn || !profileName) {
34
- logger.debug('IAM sync: No IAM profile detected from kiro-cli whoami');
35
- return;
36
- }
37
- const email = `iam-${profileName}@aws.amazon.com`;
38
- const id = createDeterministicAccountId(email, 'iam', undefined, profileArn);
39
- const existing = kiroDb.getAccounts().find((a) => a.id === id);
40
- if (existing && existing.is_healthy === 1) {
41
- logger.debug('IAM sync: Profile already synced and healthy');
42
- return;
43
- }
44
- await kiroDb.upsertAccount({
45
- id,
46
- email,
47
- authMethod: 'iam',
48
- region: region,
49
- profileArn,
50
- awsProfile: profileName,
51
- refreshToken: profileArn,
52
- accessToken: 'iam-signed',
53
- expiresAt: Date.now() + 3600000,
54
- rateLimitResetTime: 0,
55
- isHealthy: true,
56
- failCount: 0,
57
- usedCount: 0,
58
- limitCount: 0,
59
- lastSync: Date.now()
60
- });
61
- logger.log('IAM sync: Successfully synced IAM profile', { profileName, profileArn, region });
62
- }
63
- catch (e) {
64
- logger.debug('IAM sync: Failed to sync from kiro-cli', e);
65
- }
66
- }