@kervnet/opencode-kiro-auth 1.5.4 → 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 +4 -10
- package/dist/core/auth/auth-handler.js +3 -1
- package/dist/core/request/request-handler.js +10 -1
- package/dist/kiro/auth.js +5 -0
- package/dist/kiro/iam.d.ts +4 -0
- package/dist/kiro/iam.js +34 -0
- package/dist/plugin/accounts.js +3 -1
- package/dist/plugin/config/schema.d.ts +0 -3
- package/dist/plugin/config/schema.js +0 -2
- package/dist/plugin/request.js +14 -11
- package/dist/plugin/storage/migrations.js +8 -0
- package/dist/plugin/storage/sqlite.js +5 -4
- package/dist/plugin/sync/iam-cli.d.ts +1 -0
- package/dist/plugin/sync/iam-cli.js +56 -0
- package/dist/plugin/sync/kiro-cli.js +19 -44
- package/dist/plugin/token.js +11 -5
- package/dist/plugin/types.d.ts +3 -1
- package/dist/plugin.js +2 -8
- package/package.json +9 -4
- package/dist/plugin/sync/aws-sso.d.ts +0 -2
- package/dist/plugin/sync/aws-sso.js +0 -50
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
|
|
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
|
-
- **
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/kiro/iam.js
ADDED
|
@@ -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
|
+
}
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -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
|
};
|
package/dist/plugin/request.js
CHANGED
|
@@ -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 =
|
|
55
|
-
const
|
|
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:
|
|
49
|
+
expires: cliExpiresAt,
|
|
75
50
|
authMethod,
|
|
76
51
|
region,
|
|
77
|
-
profileArn
|
|
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 (
|
|
101
|
-
existing = all.find((a) => a.auth_method === authMethod && a.profile_arn ===
|
|
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,
|
|
85
|
+
email = makePlaceholderEmail(authMethod, region, clientId, profileArn);
|
|
111
86
|
}
|
|
112
87
|
}
|
|
113
|
-
const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId,
|
|
114
|
-
const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId,
|
|
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 >=
|
|
93
|
+
existingById.expires_at >= cliExpiresAt)
|
|
119
94
|
continue;
|
|
120
95
|
if (usageOk) {
|
|
121
|
-
const placeholderEmail = makePlaceholderEmail(authMethod, region, clientId,
|
|
122
|
-
const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId,
|
|
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
|
|
108
|
+
profileArn,
|
|
134
109
|
refreshToken: placeholderRow.refresh_token || refreshToken,
|
|
135
110
|
accessToken: placeholderRow.access_token || accessToken,
|
|
136
|
-
expiresAt: placeholderRow.expires_at ||
|
|
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
|
|
131
|
+
profileArn,
|
|
157
132
|
refreshToken,
|
|
158
133
|
accessToken,
|
|
159
|
-
expiresAt:
|
|
134
|
+
expiresAt: cliExpiresAt,
|
|
160
135
|
rateLimitResetTime: 0,
|
|
161
136
|
isHealthy: true,
|
|
162
137
|
failCount: 0,
|
package/dist/plugin/token.js
CHANGED
|
@@ -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
|
|
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 (
|
|
17
|
+
if (isIdc && (!p.clientId || !p.clientSecret)) {
|
|
12
18
|
throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
|
|
13
19
|
}
|
|
14
|
-
const requestBody = isIdc
|
|
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
|
|
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',
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KiroAuthMethod = 'idc' | 'desktop' | '
|
|
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
|
@@ -16,22 +16,16 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
16
16
|
const authHandler = new AuthHandler(config, repository);
|
|
17
17
|
const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
18
18
|
authHandler.setAccountManager(accountManager);
|
|
19
|
-
// Sync AWS SSO before OpenCode checks for accounts
|
|
20
|
-
await authHandler.initialize();
|
|
21
19
|
const requestHandler = new RequestHandler(accountManager, config, repository);
|
|
22
20
|
return {
|
|
23
21
|
auth: {
|
|
24
22
|
provider: id,
|
|
25
23
|
loader: async (getAuth) => {
|
|
26
24
|
await getAuth();
|
|
27
|
-
|
|
28
|
-
const baseTemplate = KIRO_CONSTANTS?.BASE_URL || 'https://q.{{region}}.amazonaws.com/generateAssistantResponse';
|
|
29
|
-
const baseURL = baseTemplate
|
|
30
|
-
.replace('/generateAssistantResponse', '')
|
|
31
|
-
.replace('{{region}}', region);
|
|
25
|
+
await authHandler.initialize();
|
|
32
26
|
return {
|
|
33
27
|
apiKey: '',
|
|
34
|
-
baseURL,
|
|
28
|
+
baseURL: KIRO_CONSTANTS.BASE_URL.replace('/generateAssistantResponse', '').replace('{{region}}', config.default_region || 'us-east-1'),
|
|
35
29
|
fetch: (input, init) => requestHandler.handle(input, init, showToast)
|
|
36
30
|
};
|
|
37
31
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kervnet/opencode-kiro-auth",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "OpenCode plugin for AWS Kiro (CodeWhisperer) with IAM
|
|
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": "
|
|
24
|
+
"author": "kervnet",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/
|
|
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,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
|
-
}
|