@kervnet/opencode-kiro-auth 1.5.5 → 1.6.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 AWS SSO authentication.
11
+ - **Multiple Auth Methods**: Supports AWS Builder ID (IDC), Kiro Desktop (CLI-based), and IAM authentication.
12
12
  - **Auto-Sync Kiro CLI**: Automatically imports and synchronizes active sessions from your local `kiro-cli` SQLite database.
13
- - **Auto-Sync AWS SSO**: Automatically imports credentials from `~/.aws/sso/cache` for seamless integration with AWS profiles.
13
+ - **IAM Profile Support**: Automatically detects and uses IAM profiles configured via `kiro-cli login` with IAM Identity Center.
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,15 +74,11 @@ 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. **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**:
77
+ 2. **Direct Authentication**:
82
78
  - Run `opencode auth login`.
83
79
  - Select `Other`, type `kiro`, and press enter.
84
80
  - Follow the instructions for **AWS Builder ID (IDC)**.
85
- 4. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
81
+ 3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
86
82
 
87
83
  ## Troubleshooting
88
84
 
@@ -103,7 +99,6 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
103
99
  ```json
104
100
  {
105
101
  "auto_sync_kiro_cli": true,
106
- "auto_sync_aws_sso": true,
107
102
  "account_selection_strategy": "lowest-usage",
108
103
  "default_region": "us-east-1",
109
104
  "rate_limit_retry_delay_ms": 5000,
@@ -122,7 +117,6 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
122
117
  ### Configuration Options
123
118
 
124
119
  - `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`).
126
120
  - `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
127
121
  - `default_region`: AWS region (`us-east-1`, `us-west-2`).
128
122
  - `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
@@ -9,7 +9,9 @@ export class AuthHandler {
9
9
  }
10
10
  async initialize() {
11
11
  const { syncFromKiroCli } = await import('../../plugin/sync/kiro-cli.js');
12
- await syncFromKiroCli();
12
+ if (this.config.auto_sync_kiro_cli) {
13
+ await syncFromKiroCli();
14
+ }
13
15
  }
14
16
  setAccountManager(am) {
15
17
  this.accountManager = am;
@@ -1,3 +1,4 @@
1
+ import { signRequestWithIAM } from '../../kiro/iam';
1
2
  import { isPermanentError } from '../../plugin/health';
2
3
  import * as logger from '../../plugin/logger';
3
4
  import { transformToCodeWhisperer } from '../../plugin/request';
@@ -75,7 +76,15 @@ export class RequestHandler {
75
76
  this.logRequest(prep, acc, apiTimestamp);
76
77
  }
77
78
  try {
78
- const res = await fetch(prep.url, prep.init);
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);
79
88
  if (apiTimestamp) {
80
89
  this.logResponse(res, prep, apiTimestamp);
81
90
  }
package/dist/kiro/auth.js CHANGED
@@ -6,6 +6,8 @@ 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' };
9
11
  if (authMethod === 'desktop')
10
12
  return { refreshToken, authMethod: 'desktop' };
11
13
  return { refreshToken, authMethod: 'desktop' };
@@ -21,5 +23,8 @@ export function encodeRefreshToken(parts) {
21
23
  throw new Error('Missing credentials');
22
24
  return `${parts.refreshToken}|${parts.clientId}|${parts.clientSecret}|idc`;
23
25
  }
26
+ if (parts.authMethod === 'iam') {
27
+ return `${parts.refreshToken}|${parts.profileArn || ''}|iam`;
28
+ }
24
29
  return `${parts.refreshToken}|desktop`;
25
30
  }
@@ -0,0 +1,4 @@
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;
@@ -0,0 +1,34 @@
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
+ }
@@ -30,6 +30,7 @@ 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,
33
34
  refreshToken: r.refresh_token,
34
35
  accessToken: r.access_token,
35
36
  expiresAt: r.expires_at,
@@ -229,7 +230,8 @@ export class AccountManager {
229
230
  profileArn: a.profileArn,
230
231
  clientId: a.clientId,
231
232
  clientSecret: a.clientSecret,
232
- email: a.email
233
+ email: a.email,
234
+ awsProfile: a.awsProfile
233
235
  };
234
236
  }
235
237
  }
@@ -17,7 +17,6 @@ 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>;
21
20
  enable_log_api_request: z.ZodDefault<z.ZodBoolean>;
22
21
  }, "strip", z.ZodTypeAny, {
23
22
  account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
@@ -32,7 +31,6 @@ export declare const KiroConfigSchema: z.ZodObject<{
32
31
  auth_server_port_range: number;
33
32
  usage_tracking_enabled: boolean;
34
33
  auto_sync_kiro_cli: boolean;
35
- auto_sync_aws_sso: boolean;
36
34
  enable_log_api_request: boolean;
37
35
  $schema?: string | undefined;
38
36
  }, {
@@ -49,7 +47,6 @@ export declare const KiroConfigSchema: z.ZodObject<{
49
47
  auth_server_port_range?: number | undefined;
50
48
  usage_tracking_enabled?: boolean | undefined;
51
49
  auto_sync_kiro_cli?: boolean | undefined;
52
- auto_sync_aws_sso?: boolean | undefined;
53
50
  enable_log_api_request?: boolean | undefined;
54
51
  }>;
55
52
  export type KiroConfig = z.infer<typeof KiroConfigSchema>;
@@ -15,7 +15,6 @@ 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),
19
18
  enable_log_api_request: z.boolean().default(false)
20
19
  });
21
20
  export const DEFAULT_CONFIG = {
@@ -31,6 +30,5 @@ export const DEFAULT_CONFIG = {
31
30
  auth_server_port_range: 10,
32
31
  usage_tracking_enabled: true,
33
32
  auto_sync_kiro_cli: true,
34
- auto_sync_aws_sso: true,
35
33
  enable_log_api_request: false
36
34
  };
@@ -215,21 +215,24 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
215
215
  const osP = os.platform(), osR = os.release(), nodeV = process.version.replace('v', ''), kiroV = KIRO_CONSTANTS.KIRO_VERSION;
216
216
  const osN = osP === 'win32' ? `windows#${osR}` : osP === 'darwin' ? `macos#${osR}` : `${osP}#${osR}`;
217
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}`;
218
+ const baseHeaders = {
219
+ 'Content-Type': 'application/json',
220
+ Accept: 'application/json',
221
+ 'amz-sdk-invocation-id': crypto.randomUUID(),
222
+ 'amz-sdk-request': 'attempt=1; max=1',
223
+ 'x-amzn-kiro-agent-mode': 'vibe',
224
+ 'x-amz-user-agent': `aws-sdk-js/1.0.0 KiroIDE-${kiroV}-${machineId}`,
225
+ 'user-agent': ua,
226
+ Connection: 'close'
227
+ };
228
+ if (auth.authMethod !== 'iam') {
229
+ baseHeaders.Authorization = `Bearer ${auth.access}`;
230
+ }
218
231
  return {
219
232
  url: KIRO_CONSTANTS.BASE_URL.replace('{{region}}', auth.region),
220
233
  init: {
221
234
  method: 'POST',
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
- },
235
+ headers: baseHeaders,
233
236
  body: JSON.stringify(request)
234
237
  },
235
238
  streaming: true,
@@ -2,6 +2,7 @@ export function runMigrations(db) {
2
2
  migrateToUniqueRefreshToken(db);
3
3
  migrateRealEmailColumn(db);
4
4
  migrateUsageTable(db);
5
+ migrateAwsProfileColumn(db);
5
6
  }
6
7
  function migrateToUniqueRefreshToken(db) {
7
8
  const hasIndex = db
@@ -107,3 +108,10 @@ function migrateUsageTable(db) {
107
108
  db.run('DROP TABLE usage');
108
109
  }
109
110
  }
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, refresh_token, access_token, expires_at, rate_limit_reset,
48
+ profile_arn, aws_profile, 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, access_token=excluded.access_token, expires_at=excluded.expires_at,
55
+ profile_arn=excluded.profile_arn, aws_profile=excluded.aws_profile, 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.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.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);
62
62
  }
63
63
  async upsertAccount(acc) {
64
64
  await withDatabaseLock(this.path, async () => {
@@ -110,6 +110,7 @@ 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,
113
114
  refreshToken: row.refresh_token,
114
115
  accessToken: row.access_token,
115
116
  expiresAt: row.expires_at,
@@ -0,0 +1 @@
1
+ export declare function syncIAMFromKiroCli(): Promise<void>;
@@ -0,0 +1,56 @@
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
+ const output = execSync('kiro-cli whoami', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
8
+ const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
9
+ let profileName;
10
+ let profileArn;
11
+ let region = 'us-east-1';
12
+ for (const line of lines) {
13
+ if (line.startsWith('arn:aws:codewhisperer:')) {
14
+ profileArn = line;
15
+ const match = line.match(/:([a-z]+-[a-z]+-\d+):/);
16
+ if (match && match[1])
17
+ region = match[1];
18
+ }
19
+ else if (!line.includes('Logged in') && !line.includes('Profile:') && !line.startsWith('arn:') && !line.startsWith('http')) {
20
+ profileName = line;
21
+ }
22
+ }
23
+ if (!profileArn || !profileName) {
24
+ logger.debug('IAM sync: No IAM profile detected from kiro-cli whoami');
25
+ return;
26
+ }
27
+ const email = `iam-${profileName}@aws.amazon.com`;
28
+ const id = createDeterministicAccountId(email, 'iam', undefined, profileArn);
29
+ const existing = kiroDb.getAccounts().find((a) => a.id === id);
30
+ if (existing && existing.is_healthy === 1) {
31
+ logger.debug('IAM sync: Profile already synced and healthy');
32
+ return;
33
+ }
34
+ await kiroDb.upsertAccount({
35
+ id,
36
+ email,
37
+ authMethod: 'iam',
38
+ region: region,
39
+ profileArn,
40
+ awsProfile: profileName,
41
+ refreshToken: profileArn,
42
+ accessToken: 'iam-signed',
43
+ expiresAt: Date.now() + 3600000,
44
+ rateLimitResetTime: 0,
45
+ isHealthy: true,
46
+ failCount: 0,
47
+ usedCount: 0,
48
+ limitCount: 0,
49
+ lastSync: Date.now()
50
+ });
51
+ logger.log('IAM sync: Successfully synced IAM profile', { profileName, profileArn, region });
52
+ }
53
+ catch (e) {
54
+ logger.debug('IAM sync: Failed to sync from kiro-cli', e);
55
+ }
56
+ }
@@ -4,8 +4,10 @@ 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';
7
8
  import { findClientCredsRecursive, getCliDbPath, makePlaceholderEmail, normalizeExpiresAt, safeJsonParse } from './kiro-cli-parser';
8
9
  export async function syncFromKiroCli() {
10
+ await syncIAMFromKiroCli();
9
11
  const dbPath = getCliDbPath();
10
12
  if (!existsSync(dbPath))
11
13
  return;
@@ -13,28 +15,6 @@ export async function syncFromKiroCli() {
13
15
  const cliDb = new Database(dbPath, { readonly: true });
14
16
  cliDb.run('PRAGMA busy_timeout = 5000');
15
17
  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
- }
38
18
  const deviceRegRow = rows.find((r) => typeof r?.key === 'string' && r.key.includes('device-registration'));
39
19
  const deviceReg = safeJsonParse(deviceRegRow?.value);
40
20
  const regCreds = deviceReg ? findClientCredsRecursive(deviceReg) : {};
@@ -43,16 +23,10 @@ export async function syncFromKiroCli() {
43
23
  const data = safeJsonParse(row.value);
44
24
  if (!data)
45
25
  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
- }
52
26
  const isIdc = row.key.includes('odic');
53
27
  const authMethod = isIdc ? 'idc' : 'desktop';
54
- const region = profileRegion || data.region || 'us-east-1';
55
- const tokenProfileArn = data.profile_arn || data.profileArn || profileArn;
28
+ const region = data.region || 'us-east-1';
29
+ const profileArn = data.profile_arn || data.profileArn;
56
30
  const accessToken = data.access_token || data.accessToken || '';
57
31
  const refreshToken = data.refresh_token || data.refreshToken;
58
32
  if (!refreshToken)
@@ -63,6 +37,7 @@ export async function syncFromKiroCli() {
63
37
  logger.warn('Kiro CLI sync: missing IDC device credentials; skipping token import');
64
38
  continue;
65
39
  }
40
+ const cliExpiresAt = normalizeExpiresAt(data.expires_at ?? data.expiresAt) || Date.now() + 3600000;
66
41
  let usedCount = 0;
67
42
  let limitCount = 0;
68
43
  let email;
@@ -71,10 +46,10 @@ export async function syncFromKiroCli() {
71
46
  const authForUsage = {
72
47
  refresh: '',
73
48
  access: accessToken,
74
- expires: tokenExpiresAt,
49
+ expires: cliExpiresAt,
75
50
  authMethod,
76
51
  region,
77
- profileArn: tokenProfileArn,
52
+ profileArn,
78
53
  clientId,
79
54
  clientSecret,
80
55
  email: ''
@@ -97,8 +72,8 @@ export async function syncFromKiroCli() {
97
72
  const all = kiroDb.getAccounts();
98
73
  if (!email) {
99
74
  let existing;
100
- if (tokenProfileArn) {
101
- existing = all.find((a) => a.auth_method === authMethod && a.profile_arn === tokenProfileArn);
75
+ if (profileArn) {
76
+ existing = all.find((a) => a.auth_method === authMethod && a.profile_arn === profileArn);
102
77
  }
103
78
  if (!existing && authMethod === 'idc' && clientId) {
104
79
  existing = all.find((a) => a.auth_method === 'idc' && a.client_id === clientId);
@@ -107,19 +82,19 @@ export async function syncFromKiroCli() {
107
82
  email = existing.email;
108
83
  }
109
84
  else {
110
- email = makePlaceholderEmail(authMethod, region, clientId, tokenProfileArn);
85
+ email = makePlaceholderEmail(authMethod, region, clientId, profileArn);
111
86
  }
112
87
  }
113
- const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId, tokenProfileArn);
114
- const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, tokenProfileArn);
88
+ const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId, profileArn);
89
+ const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, profileArn);
115
90
  const existingById = all.find((a) => a.id === id);
116
91
  if (existingById &&
117
92
  existingById.is_healthy === 1 &&
118
- existingById.expires_at >= tokenExpiresAt)
93
+ existingById.expires_at >= cliExpiresAt)
119
94
  continue;
120
95
  if (usageOk) {
121
- const placeholderEmail = makePlaceholderEmail(authMethod, region, clientId, tokenProfileArn);
122
- const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, tokenProfileArn);
96
+ const placeholderEmail = makePlaceholderEmail(authMethod, region, clientId, profileArn);
97
+ const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, profileArn);
123
98
  if (placeholderId !== id) {
124
99
  const placeholderRow = all.find((a) => a.id === placeholderId);
125
100
  if (placeholderRow) {
@@ -130,10 +105,10 @@ export async function syncFromKiroCli() {
130
105
  region: placeholderRow.region || region,
131
106
  clientId,
132
107
  clientSecret,
133
- profileArn: tokenProfileArn,
108
+ profileArn,
134
109
  refreshToken: placeholderRow.refresh_token || refreshToken,
135
110
  accessToken: placeholderRow.access_token || accessToken,
136
- expiresAt: placeholderRow.expires_at || tokenExpiresAt,
111
+ expiresAt: placeholderRow.expires_at || cliExpiresAt,
137
112
  rateLimitResetTime: 0,
138
113
  isHealthy: false,
139
114
  failCount: 10,
@@ -153,10 +128,10 @@ export async function syncFromKiroCli() {
153
128
  region,
154
129
  clientId,
155
130
  clientSecret,
156
- profileArn: tokenProfileArn,
131
+ profileArn,
157
132
  refreshToken,
158
133
  accessToken,
159
- expiresAt: tokenExpiresAt,
134
+ expiresAt: cliExpiresAt,
160
135
  rateLimitResetTime: 0,
161
136
  isHealthy: true,
162
137
  failCount: 0,
@@ -2,16 +2,22 @@ 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
+ }
5
12
  const p = decodeRefreshToken(auth.refresh);
6
13
  const isIdc = auth.authMethod === 'idc';
7
- const isAwsSso = auth.authMethod === 'aws-sso';
8
- const url = isIdc || isAwsSso
14
+ const url = isIdc
9
15
  ? `https://oidc.${auth.region}.amazonaws.com/token`
10
16
  : `https://prod.${auth.region}.auth.desktop.kiro.dev/refreshToken`;
11
- if ((isIdc || isAwsSso) && (!p.clientId || !p.clientSecret)) {
17
+ if (isIdc && (!p.clientId || !p.clientSecret)) {
12
18
  throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
13
19
  }
14
- const requestBody = isIdc || isAwsSso
20
+ const requestBody = isIdc
15
21
  ? {
16
22
  refreshToken: p.refreshToken,
17
23
  clientId: p.clientId,
@@ -25,7 +31,7 @@ export async function refreshAccessToken(auth) {
25
31
  .createHash('sha256')
26
32
  .update(auth.profileArn || auth.clientId || 'KIRO_DEFAULT_MACHINE')
27
33
  .digest('hex');
28
- const ua = isIdc || isAwsSso ? 'aws-sdk-js/1.0.0' : `KiroIDE-0.7.45-${machineId}`;
34
+ const ua = isIdc ? 'aws-sdk-js/1.0.0' : `KiroIDE-0.7.45-${machineId}`;
29
35
  try {
30
36
  const res = await fetch(url, {
31
37
  method: 'POST',
@@ -1,4 +1,4 @@
1
- export type KiroAuthMethod = 'idc' | 'desktop' | 'aws-sso';
1
+ export type KiroAuthMethod = 'idc' | 'desktop' | 'iam';
2
2
  export type KiroRegion = 'us-east-1' | 'us-west-2';
3
3
  export interface KiroAuthDetails {
4
4
  refresh: string;
@@ -10,6 +10,7 @@ export interface KiroAuthDetails {
10
10
  clientSecret?: string;
11
11
  email?: string;
12
12
  profileArn?: string;
13
+ awsProfile?: string;
13
14
  }
14
15
  export interface RefreshParts {
15
16
  refreshToken: string;
@@ -26,6 +27,7 @@ export interface ManagedAccount {
26
27
  clientId?: string;
27
28
  clientSecret?: string;
28
29
  profileArn?: string;
30
+ awsProfile?: string;
29
31
  refreshToken: string;
30
32
  accessToken: string;
31
33
  expiresAt: number;
package/dist/plugin.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { KIRO_CONSTANTS } from './constants.js';
1
2
  import { AuthHandler } from './core/auth/auth-handler.js';
2
3
  import { RequestHandler } from './core/request/request-handler.js';
3
4
  import { AccountCache } from './infrastructure/database/account-cache.js';
@@ -15,17 +16,16 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
15
16
  const authHandler = new AuthHandler(config, repository);
16
17
  const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy);
17
18
  authHandler.setAccountManager(accountManager);
18
- // Sync AWS SSO before OpenCode checks for accounts
19
- await authHandler.initialize();
20
19
  const requestHandler = new RequestHandler(accountManager, config, repository);
21
20
  return {
22
21
  auth: {
23
22
  provider: id,
24
23
  loader: async (getAuth) => {
25
24
  await getAuth();
25
+ await authHandler.initialize();
26
26
  return {
27
27
  apiKey: '',
28
- baseURL: 'https://q.us-east-1.amazonaws.com',
28
+ baseURL: KIRO_CONSTANTS.BASE_URL.replace('/generateAssistantResponse', '').replace('{{region}}', config.default_region || 'us-east-1'),
29
29
  fetch: (input, init) => requestHandler.handle(input, init, showToast)
30
30
  };
31
31
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kervnet/opencode-kiro-auth",
3
- "version": "1.5.5",
4
- "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) with IAM Identity Center profile support",
3
+ "version": "1.6.0",
4
+ "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models with IAM auth support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -21,17 +21,22 @@
21
21
  "ai",
22
22
  "auth"
23
23
  ],
24
- "author": "tickernelz",
24
+ "author": "kervnet",
25
25
  "license": "MIT",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "git+https://github.com/tickernelz/opencode-kiro-auth.git"
28
+ "url": "git+https://github.com/kervnet/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",
34
37
  "@opencode-ai/plugin": "^0.15.30",
38
+ "@smithy/protocol-http": "^5.3.8",
39
+ "@smithy/signature-v4": "^5.3.8",
35
40
  "proper-lockfile": "^4.1.2",
36
41
  "zod": "^3.24.0"
37
42
  },
@@ -1,2 +0,0 @@
1
- import type { ManagedAccount } from '../types';
2
- export declare function syncFromAwsSso(): Promise<ManagedAccount[]>;
@@ -1,50 +0,0 @@
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
- }