@zhafron/opencode-kiro-auth 1.5.3 → 1.6.1
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 +7 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +9 -4
- package/dist/core/auth/auth-handler.d.ts +2 -6
- package/dist/core/auth/auth-handler.js +20 -2
- package/dist/core/auth/idc-auth-method.d.ts +2 -9
- package/dist/core/auth/idc-auth-method.js +13 -10
- package/dist/infrastructure/database/account-repository.js +1 -0
- package/dist/infrastructure/transformers/history-builder.d.ts +2 -1
- package/dist/infrastructure/transformers/history-builder.js +20 -31
- package/dist/kiro/oauth-idc.d.ts +2 -1
- package/dist/kiro/oauth-idc.js +5 -3
- package/dist/plugin/accounts.js +1 -0
- package/dist/plugin/auth-page.d.ts +1 -0
- package/dist/plugin/auth-page.js +148 -0
- package/dist/plugin/config/schema.d.ts +3 -0
- package/dist/plugin/config/schema.js +1 -0
- package/dist/plugin/request.js +10 -3
- package/dist/plugin/server.d.ts +10 -0
- package/dist/plugin/server.js +198 -2
- package/dist/plugin/storage/migrations.js +8 -0
- package/dist/plugin/storage/sqlite.js +7 -4
- package/dist/plugin/types.d.ts +1 -0
- package/dist/plugin.d.ts +100 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ 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) and Kiro Desktop (CLI-based) authentication.
|
|
11
|
+
- **Multiple Auth Methods**: Supports AWS Builder ID (IDC), IAM Identity Center (custom Start URL), and Kiro Desktop (CLI-based) authentication.
|
|
12
12
|
- **Auto-Sync Kiro CLI**: Automatically imports and synchronizes active sessions from your local `kiro-cli` SQLite database.
|
|
13
13
|
- **Gradual Context Truncation**: Intelligently prevents error 400 by reducing context size dynamically during retries.
|
|
14
14
|
- **Intelligent Account Rotation**: Prioritizes multi-account usage based on lowest available quota.
|
|
@@ -116,7 +116,10 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
|
|
|
116
116
|
2. **Direct Authentication**:
|
|
117
117
|
- Run `opencode auth login`.
|
|
118
118
|
- Select `Other`, type `kiro`, and press enter.
|
|
119
|
-
-
|
|
119
|
+
- A browser page will open asking for your **IAM Identity Center Start URL**.
|
|
120
|
+
- Leave it blank to sign in with **AWS Builder ID**.
|
|
121
|
+
- Enter your company's Start URL (e.g. `https://your-company.awsapps.com/start`) to use **IAM Identity Center (SSO)**.
|
|
122
|
+
- You can also pre-configure the Start URL in `~/.config/opencode/kiro.json` via `idc_start_url` to skip the prompt.
|
|
120
123
|
3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
|
|
121
124
|
|
|
122
125
|
## Troubleshooting
|
|
@@ -155,6 +158,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
|
|
|
155
158
|
"auto_sync_kiro_cli": true,
|
|
156
159
|
"account_selection_strategy": "lowest-usage",
|
|
157
160
|
"default_region": "us-east-1",
|
|
161
|
+
"idc_start_url": "https://your-company.awsapps.com/start",
|
|
158
162
|
"rate_limit_retry_delay_ms": 5000,
|
|
159
163
|
"rate_limit_max_retries": 3,
|
|
160
164
|
"max_request_iterations": 20,
|
|
@@ -173,6 +177,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
|
|
|
173
177
|
- `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
|
|
174
178
|
- `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
|
|
175
179
|
- `default_region`: AWS region (`us-east-1`, `us-west-2`).
|
|
180
|
+
- `idc_start_url`: Pre-configure your IAM Identity Center Start URL (e.g. `https://your-company.awsapps.com/start`). If set, the browser auth page will pre-fill this value. Leave unset to default to AWS Builder ID.
|
|
176
181
|
- `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
|
|
177
182
|
- `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10).
|
|
178
183
|
- `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000).
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -37,12 +37,16 @@ export const MODEL_MAPPING = {
|
|
|
37
37
|
'claude-sonnet-4-5-thinking': 'CLAUDE_SONNET_4_5_20250929_V1_0',
|
|
38
38
|
'claude-sonnet-4-5-1m': 'CLAUDE_SONNET_4_5_20250929_LONG_V1_0',
|
|
39
39
|
'claude-sonnet-4-5-1m-thinking': 'CLAUDE_SONNET_4_5_20250929_LONG_V1_0',
|
|
40
|
+
'claude-sonnet-4-6': 'claude-sonnet-4.6',
|
|
41
|
+
'claude-sonnet-4-6-thinking': 'claude-sonnet-4.6',
|
|
42
|
+
'claude-sonnet-4-6-1m': 'claude-sonnet-4.6',
|
|
43
|
+
'claude-sonnet-4-6-1m-thinking': 'claude-sonnet-4.6',
|
|
40
44
|
'claude-opus-4-5': 'CLAUDE_OPUS_4_5_20251101_V1_0',
|
|
41
45
|
'claude-opus-4-5-thinking': 'CLAUDE_OPUS_4_5_20251101_V1_0',
|
|
42
|
-
'claude-opus-4-6': '
|
|
43
|
-
'claude-opus-4-6-thinking': '
|
|
44
|
-
'claude-opus-4-6-1m': '
|
|
45
|
-
'claude-opus-4-6-1m-thinking': '
|
|
46
|
+
'claude-opus-4-6': 'claude-opus-4.6',
|
|
47
|
+
'claude-opus-4-6-thinking': 'claude-opus-4.6',
|
|
48
|
+
'claude-opus-4-6-1m': 'claude-opus-4.6',
|
|
49
|
+
'claude-opus-4-6-1m-thinking': 'claude-opus-4.6',
|
|
46
50
|
'claude-sonnet-4': 'CLAUDE_SONNET_4_20250514_V1_0',
|
|
47
51
|
'claude-3-7-sonnet': 'CLAUDE_3_7_SONNET_20250219_V1_0',
|
|
48
52
|
'nova-swe': 'AGI_NOVA_SWE_V1_5',
|
|
@@ -56,6 +60,7 @@ export const KIRO_AUTH_SERVICE = {
|
|
|
56
60
|
ENDPOINT: 'https://prod.{{region}}.auth.desktop.kiro.dev',
|
|
57
61
|
SSO_OIDC_ENDPOINT: 'https://oidc.{{region}}.amazonaws.com',
|
|
58
62
|
BUILDER_ID_START_URL: 'https://view.awsapps.com/start',
|
|
63
|
+
USER_INFO_URL: 'https://view.awsapps.com/api/user/info',
|
|
59
64
|
SCOPES: [
|
|
60
65
|
'codewhisperer:completions',
|
|
61
66
|
'codewhisperer:analysis',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuthHook } from '@opencode-ai/plugin';
|
|
1
2
|
import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
|
|
2
3
|
export declare class AuthHandler {
|
|
3
4
|
private config;
|
|
@@ -6,10 +7,5 @@ export declare class AuthHandler {
|
|
|
6
7
|
constructor(config: any, repository: AccountRepository);
|
|
7
8
|
initialize(): Promise<void>;
|
|
8
9
|
setAccountManager(am: any): void;
|
|
9
|
-
getMethods():
|
|
10
|
-
id: string;
|
|
11
|
-
label: string;
|
|
12
|
-
type: 'oauth';
|
|
13
|
-
authorize: (inputs?: any) => Promise<any>;
|
|
14
|
-
}>;
|
|
10
|
+
getMethods(): AuthHook['methods'];
|
|
15
11
|
}
|
|
@@ -23,9 +23,27 @@ export class AuthHandler {
|
|
|
23
23
|
const idcMethod = new IdcAuthMethod(this.config, this.repository);
|
|
24
24
|
return [
|
|
25
25
|
{
|
|
26
|
-
|
|
27
|
-
label: 'AWS Builder ID (IDC)',
|
|
26
|
+
label: 'AWS Builder ID / IAM Identity Center',
|
|
28
27
|
type: 'oauth',
|
|
28
|
+
prompts: [
|
|
29
|
+
{
|
|
30
|
+
type: 'text',
|
|
31
|
+
key: 'start_url',
|
|
32
|
+
message: 'IAM Identity Center Start URL (leave blank for AWS Builder ID)',
|
|
33
|
+
placeholder: 'https://your-company.awsapps.com/start',
|
|
34
|
+
validate: (value) => {
|
|
35
|
+
if (!value)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
new URL(value);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 'Please enter a valid URL';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
29
47
|
authorize: (inputs) => idcMethod.authorize(inputs)
|
|
30
48
|
}
|
|
31
49
|
];
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
+
import type { AuthOuathResult } from '@opencode-ai/plugin';
|
|
1
2
|
import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
|
|
2
3
|
export declare class IdcAuthMethod {
|
|
3
4
|
private config;
|
|
4
5
|
private repository;
|
|
5
6
|
constructor(config: any, repository: AccountRepository);
|
|
6
|
-
authorize(inputs?:
|
|
7
|
-
url: string;
|
|
8
|
-
instructions: string;
|
|
9
|
-
method: 'auto';
|
|
10
|
-
callback: () => Promise<{
|
|
11
|
-
type: 'success' | 'failed';
|
|
12
|
-
key?: string;
|
|
13
|
-
}>;
|
|
14
|
-
}>;
|
|
7
|
+
authorize(inputs?: Record<string, string>): Promise<AuthOuathResult>;
|
|
15
8
|
private handleMultipleLogin;
|
|
16
9
|
private handleSingleLogin;
|
|
17
10
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { exec } from 'node:child_process';
|
|
2
|
-
import { authorizeKiroIDC } from '../../kiro/oauth-idc.js';
|
|
3
2
|
import { createDeterministicAccountId } from '../../plugin/accounts.js';
|
|
4
3
|
import { promptAddAnotherAccount, promptDeleteAccount, promptLoginMode } from '../../plugin/cli.js';
|
|
5
4
|
import * as logger from '../../plugin/logger.js';
|
|
6
|
-
import {
|
|
5
|
+
import { startIDCAuthServerWithInput } from '../../plugin/server.js';
|
|
7
6
|
import { fetchUsageLimits } from '../../plugin/usage.js';
|
|
8
7
|
const openBrowser = (url) => {
|
|
9
8
|
const escapedUrl = url.replace(/"/g, '\\"');
|
|
@@ -28,15 +27,17 @@ export class IdcAuthMethod {
|
|
|
28
27
|
async authorize(inputs) {
|
|
29
28
|
return new Promise(async (resolve) => {
|
|
30
29
|
const region = this.config.default_region;
|
|
30
|
+
// inputs.start_url takes priority over config; browser input page will also allow override
|
|
31
|
+
const defaultStartUrl = inputs?.start_url || this.config.idc_start_url;
|
|
31
32
|
if (inputs) {
|
|
32
|
-
await this.handleMultipleLogin(region, resolve);
|
|
33
|
+
await this.handleMultipleLogin(region, defaultStartUrl, resolve);
|
|
33
34
|
}
|
|
34
35
|
else {
|
|
35
|
-
await this.handleSingleLogin(region, resolve);
|
|
36
|
+
await this.handleSingleLogin(region, defaultStartUrl, resolve);
|
|
36
37
|
}
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
|
-
async handleMultipleLogin(region, resolve) {
|
|
40
|
+
async handleMultipleLogin(region, defaultStartUrl, resolve) {
|
|
40
41
|
const accounts = [];
|
|
41
42
|
let startFresh = true;
|
|
42
43
|
while (true) {
|
|
@@ -75,10 +76,10 @@ export class IdcAuthMethod {
|
|
|
75
76
|
}
|
|
76
77
|
while (true) {
|
|
77
78
|
try {
|
|
78
|
-
const
|
|
79
|
-
const { url, waitForAuth } = await startIDCAuthServer(authData, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
79
|
+
const { url, waitForAuth } = await startIDCAuthServerWithInput(region, defaultStartUrl, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
80
80
|
openBrowser(url);
|
|
81
81
|
const res = await waitForAuth();
|
|
82
|
+
const startUrl = defaultStartUrl;
|
|
82
83
|
const u = await fetchUsageLimits({
|
|
83
84
|
refresh: '',
|
|
84
85
|
access: res.accessToken,
|
|
@@ -108,6 +109,7 @@ export class IdcAuthMethod {
|
|
|
108
109
|
region,
|
|
109
110
|
clientId: res.clientId,
|
|
110
111
|
clientSecret: res.clientSecret,
|
|
112
|
+
startUrl: startUrl || undefined,
|
|
111
113
|
refreshToken: res.refreshToken,
|
|
112
114
|
accessToken: res.accessToken,
|
|
113
115
|
expiresAt: res.expiresAt,
|
|
@@ -139,10 +141,9 @@ export class IdcAuthMethod {
|
|
|
139
141
|
})
|
|
140
142
|
});
|
|
141
143
|
}
|
|
142
|
-
async handleSingleLogin(region, resolve) {
|
|
144
|
+
async handleSingleLogin(region, defaultStartUrl, resolve) {
|
|
143
145
|
try {
|
|
144
|
-
const
|
|
145
|
-
const { url, waitForAuth } = await startIDCAuthServer(authData, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
146
|
+
const { url, waitForAuth } = await startIDCAuthServerWithInput(region, defaultStartUrl, this.config.auth_server_port_start, this.config.auth_server_port_range);
|
|
146
147
|
openBrowser(url);
|
|
147
148
|
resolve({
|
|
148
149
|
url,
|
|
@@ -151,6 +152,7 @@ export class IdcAuthMethod {
|
|
|
151
152
|
callback: async () => {
|
|
152
153
|
try {
|
|
153
154
|
const res = await waitForAuth();
|
|
155
|
+
const startUrl = defaultStartUrl;
|
|
154
156
|
const u = await fetchUsageLimits({
|
|
155
157
|
refresh: '',
|
|
156
158
|
access: res.accessToken,
|
|
@@ -170,6 +172,7 @@ export class IdcAuthMethod {
|
|
|
170
172
|
region,
|
|
171
173
|
clientId: res.clientId,
|
|
172
174
|
clientSecret: res.clientSecret,
|
|
175
|
+
startUrl: startUrl || undefined,
|
|
173
176
|
refreshToken: res.refreshToken,
|
|
174
177
|
accessToken: res.accessToken,
|
|
175
178
|
expiresAt: res.expiresAt,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CodeWhispererMessage } from '../../plugin/types';
|
|
2
|
-
export declare function buildHistory(msgs: any[], resolved: string,
|
|
2
|
+
export declare function buildHistory(msgs: any[], resolved: string, toolResultLimit: number): CodeWhispererMessage[];
|
|
3
|
+
export declare function injectSystemPrompt(history: CodeWhispererMessage[], system: string | undefined, resolved: string): CodeWhispererMessage[];
|
|
3
4
|
export declare function truncateHistory(history: CodeWhispererMessage[], historyLimit: number): CodeWhispererMessage[];
|
|
4
5
|
export declare function historyHasToolCalling(history: CodeWhispererMessage[]): boolean;
|
|
5
6
|
export declare function extractToolNamesFromHistory(history: CodeWhispererMessage[]): Set<string>;
|
|
@@ -2,38 +2,8 @@ import { KIRO_CONSTANTS } from '../../constants.js';
|
|
|
2
2
|
import { convertImagesToKiroFormat, extractAllImages, extractTextFromParts } from '../../plugin/image-handler.js';
|
|
3
3
|
import { getContentText, sanitizeHistory, truncate } from './message-transformer.js';
|
|
4
4
|
import { deduplicateToolResults } from './tool-transformer.js';
|
|
5
|
-
export function buildHistory(msgs, resolved,
|
|
5
|
+
export function buildHistory(msgs, resolved, toolResultLimit) {
|
|
6
6
|
let history = [];
|
|
7
|
-
let firstUserIndex = -1;
|
|
8
|
-
for (let i = 0; i < msgs.length; i++) {
|
|
9
|
-
if (msgs[i].role === 'user') {
|
|
10
|
-
firstUserIndex = i;
|
|
11
|
-
break;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
if (system) {
|
|
15
|
-
if (firstUserIndex !== -1) {
|
|
16
|
-
const m = msgs[firstUserIndex];
|
|
17
|
-
const oldContent = getContentText(m);
|
|
18
|
-
if (Array.isArray(m.content)) {
|
|
19
|
-
m.content = [
|
|
20
|
-
{ type: 'text', text: `${system}\n\n${oldContent}` },
|
|
21
|
-
...m.content.filter((p) => p.type !== 'text')
|
|
22
|
-
];
|
|
23
|
-
}
|
|
24
|
-
else
|
|
25
|
-
m.content = `${system}\n\n${oldContent}`;
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
history.push({
|
|
29
|
-
userInputMessage: {
|
|
30
|
-
content: system,
|
|
31
|
-
modelId: resolved,
|
|
32
|
-
origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
7
|
for (let i = 0; i < msgs.length - 1; i++) {
|
|
38
8
|
const m = msgs[i];
|
|
39
9
|
if (!m)
|
|
@@ -137,6 +107,25 @@ export function buildHistory(msgs, resolved, system, toolResultLimit) {
|
|
|
137
107
|
}
|
|
138
108
|
return history;
|
|
139
109
|
}
|
|
110
|
+
export function injectSystemPrompt(history, system, resolved) {
|
|
111
|
+
if (!system)
|
|
112
|
+
return history;
|
|
113
|
+
const firstUserMsg = history.find((h) => !!h.userInputMessage);
|
|
114
|
+
if (firstUserMsg && firstUserMsg.userInputMessage) {
|
|
115
|
+
const oldContent = firstUserMsg.userInputMessage.content || '';
|
|
116
|
+
firstUserMsg.userInputMessage.content = `${system}\n\n${oldContent}`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
history.unshift({
|
|
120
|
+
userInputMessage: {
|
|
121
|
+
content: system,
|
|
122
|
+
modelId: resolved,
|
|
123
|
+
origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return history;
|
|
128
|
+
}
|
|
140
129
|
export function truncateHistory(history, historyLimit) {
|
|
141
130
|
let sanitized = sanitizeHistory(history);
|
|
142
131
|
let historySize = JSON.stringify(sanitized).length;
|
package/dist/kiro/oauth-idc.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface KiroIDCAuthorization {
|
|
|
9
9
|
interval: number;
|
|
10
10
|
expiresIn: number;
|
|
11
11
|
region: KiroRegion;
|
|
12
|
+
startUrl: string;
|
|
12
13
|
}
|
|
13
14
|
export interface KiroIDCTokenResult {
|
|
14
15
|
refreshToken: string;
|
|
@@ -20,5 +21,5 @@ export interface KiroIDCTokenResult {
|
|
|
20
21
|
region: KiroRegion;
|
|
21
22
|
authMethod: 'idc';
|
|
22
23
|
}
|
|
23
|
-
export declare function authorizeKiroIDC(region?: KiroRegion): Promise<KiroIDCAuthorization>;
|
|
24
|
+
export declare function authorizeKiroIDC(region?: KiroRegion, startUrl?: string): Promise<KiroIDCAuthorization>;
|
|
24
25
|
export declare function pollKiroIDCToken(clientId: string, clientSecret: string, deviceCode: string, interval: number, expiresIn: number, region: KiroRegion): Promise<KiroIDCTokenResult>;
|
package/dist/kiro/oauth-idc.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS, buildUrl, normalizeRegion } from '../constants';
|
|
2
|
-
export async function authorizeKiroIDC(region) {
|
|
2
|
+
export async function authorizeKiroIDC(region, startUrl) {
|
|
3
3
|
const effectiveRegion = normalizeRegion(region);
|
|
4
4
|
const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
|
|
5
|
+
const effectiveStartUrl = startUrl || KIRO_AUTH_SERVICE.BUILDER_ID_START_URL;
|
|
5
6
|
try {
|
|
6
7
|
const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
|
|
7
8
|
method: 'POST',
|
|
@@ -36,7 +37,7 @@ export async function authorizeKiroIDC(region) {
|
|
|
36
37
|
body: JSON.stringify({
|
|
37
38
|
clientId,
|
|
38
39
|
clientSecret,
|
|
39
|
-
startUrl:
|
|
40
|
+
startUrl: effectiveStartUrl
|
|
40
41
|
})
|
|
41
42
|
});
|
|
42
43
|
if (!deviceAuthResponse.ok) {
|
|
@@ -59,7 +60,8 @@ export async function authorizeKiroIDC(region) {
|
|
|
59
60
|
clientSecret,
|
|
60
61
|
interval,
|
|
61
62
|
expiresIn,
|
|
62
|
-
region: effectiveRegion
|
|
63
|
+
region: effectiveRegion,
|
|
64
|
+
startUrl: effectiveStartUrl
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
catch (error) {
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function getIDCAuthHtml(verificationUrl: string, userCode: string, statusUrl: string): string;
|
|
2
2
|
export declare function getSuccessHtml(): string;
|
|
3
3
|
export declare function getErrorHtml(message: string): string;
|
|
4
|
+
export declare function getStartUrlInputHtml(defaultStartUrl: string, submitUrl: string): string;
|
package/dist/plugin/auth-page.js
CHANGED
|
@@ -571,3 +571,151 @@ export function getErrorHtml(message) {
|
|
|
571
571
|
</body>
|
|
572
572
|
</html>`;
|
|
573
573
|
}
|
|
574
|
+
export function getStartUrlInputHtml(defaultStartUrl, submitUrl) {
|
|
575
|
+
const escapedDefault = escapeHtml(defaultStartUrl);
|
|
576
|
+
const escapedSubmit = escapeHtml(submitUrl);
|
|
577
|
+
return `<!DOCTYPE html>
|
|
578
|
+
<html lang="en">
|
|
579
|
+
<head>
|
|
580
|
+
<meta charset="UTF-8">
|
|
581
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
582
|
+
<title>AWS Authentication Setup</title>
|
|
583
|
+
<style>
|
|
584
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
585
|
+
body {
|
|
586
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
587
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
588
|
+
min-height: 100vh;
|
|
589
|
+
display: flex;
|
|
590
|
+
align-items: center;
|
|
591
|
+
justify-content: center;
|
|
592
|
+
padding: 20px;
|
|
593
|
+
}
|
|
594
|
+
.container {
|
|
595
|
+
background: white;
|
|
596
|
+
border-radius: 16px;
|
|
597
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
598
|
+
max-width: 500px;
|
|
599
|
+
width: 100%;
|
|
600
|
+
padding: 48px 40px;
|
|
601
|
+
animation: slideIn 0.4s ease-out;
|
|
602
|
+
}
|
|
603
|
+
@keyframes slideIn {
|
|
604
|
+
from { opacity: 0; transform: translateY(-20px); }
|
|
605
|
+
to { opacity: 1; transform: translateY(0); }
|
|
606
|
+
}
|
|
607
|
+
h1 { color: #1a202c; font-size: 26px; font-weight: 700; margin-bottom: 10px; }
|
|
608
|
+
.subtitle { color: #718096; font-size: 15px; margin-bottom: 32px; line-height: 1.5; }
|
|
609
|
+
label { display: block; color: #4a5568; font-size: 14px; font-weight: 600; margin-bottom: 8px; }
|
|
610
|
+
input[type="url"], input[type="text"] {
|
|
611
|
+
width: 100%;
|
|
612
|
+
padding: 12px 16px;
|
|
613
|
+
border: 2px solid #e2e8f0;
|
|
614
|
+
border-radius: 8px;
|
|
615
|
+
font-size: 15px;
|
|
616
|
+
color: #2d3748;
|
|
617
|
+
outline: none;
|
|
618
|
+
transition: border-color 0.2s;
|
|
619
|
+
}
|
|
620
|
+
input:focus { border-color: #667eea; }
|
|
621
|
+
.hint { color: #a0aec0; font-size: 13px; margin-top: 8px; margin-bottom: 24px; }
|
|
622
|
+
.hint a { color: #667eea; text-decoration: none; }
|
|
623
|
+
button {
|
|
624
|
+
width: 100%;
|
|
625
|
+
padding: 14px;
|
|
626
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
627
|
+
color: white;
|
|
628
|
+
border: none;
|
|
629
|
+
border-radius: 8px;
|
|
630
|
+
font-size: 16px;
|
|
631
|
+
font-weight: 600;
|
|
632
|
+
cursor: pointer;
|
|
633
|
+
transition: opacity 0.2s, transform 0.1s;
|
|
634
|
+
}
|
|
635
|
+
button:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
636
|
+
button:active { transform: translateY(0); }
|
|
637
|
+
button:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
|
638
|
+
.error { color: #e53e3e; font-size: 13px; margin-top: 8px; display: none; }
|
|
639
|
+
.loading { display: none; text-align: center; color: #718096; margin-top: 16px; font-size: 14px; }
|
|
640
|
+
.spinner {
|
|
641
|
+
display: inline-block; width: 16px; height: 16px;
|
|
642
|
+
border: 2px solid #e2e8f0; border-top-color: #667eea;
|
|
643
|
+
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
644
|
+
vertical-align: middle; margin-right: 8px;
|
|
645
|
+
}
|
|
646
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
647
|
+
</style>
|
|
648
|
+
</head>
|
|
649
|
+
<body>
|
|
650
|
+
<div class="container">
|
|
651
|
+
<h1>AWS Authentication</h1>
|
|
652
|
+
<p class="subtitle">Enter your IAM Identity Center Start URL, or leave blank to use AWS Builder ID.</p>
|
|
653
|
+
|
|
654
|
+
<form id="form" onsubmit="handleSubmit(event)">
|
|
655
|
+
<label for="startUrl">Start URL</label>
|
|
656
|
+
<input
|
|
657
|
+
type="text"
|
|
658
|
+
id="startUrl"
|
|
659
|
+
name="startUrl"
|
|
660
|
+
placeholder="https://your-company.awsapps.com/start"
|
|
661
|
+
value="${escapedDefault}"
|
|
662
|
+
autocomplete="url"
|
|
663
|
+
spellcheck="false"
|
|
664
|
+
/>
|
|
665
|
+
<div class="hint">Leave blank to sign in with <strong>AWS Builder ID</strong></div>
|
|
666
|
+
<div class="error" id="error"></div>
|
|
667
|
+
<button type="submit" id="btn">Continue</button>
|
|
668
|
+
</form>
|
|
669
|
+
|
|
670
|
+
<div class="loading" id="loading">
|
|
671
|
+
<span class="spinner"></span>Initializing authentication...
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
<script>
|
|
676
|
+
async function handleSubmit(e) {
|
|
677
|
+
e.preventDefault();
|
|
678
|
+
const input = document.getElementById('startUrl');
|
|
679
|
+
const errorEl = document.getElementById('error');
|
|
680
|
+
const btn = document.getElementById('btn');
|
|
681
|
+
const loading = document.getElementById('loading');
|
|
682
|
+
const val = input.value.trim();
|
|
683
|
+
|
|
684
|
+
if (val) {
|
|
685
|
+
try { new URL(val); } catch {
|
|
686
|
+
errorEl.textContent = 'Please enter a valid URL';
|
|
687
|
+
errorEl.style.display = 'block';
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
errorEl.style.display = 'none';
|
|
693
|
+
btn.disabled = true;
|
|
694
|
+
loading.style.display = 'block';
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
const res = await fetch('${escapedSubmit}', {
|
|
698
|
+
method: 'POST',
|
|
699
|
+
headers: { 'Content-Type': 'application/json' },
|
|
700
|
+
body: JSON.stringify({ startUrl: val || '' })
|
|
701
|
+
});
|
|
702
|
+
const data = await res.json();
|
|
703
|
+
if (data.error) {
|
|
704
|
+
errorEl.textContent = data.error;
|
|
705
|
+
errorEl.style.display = 'block';
|
|
706
|
+
btn.disabled = false;
|
|
707
|
+
loading.style.display = 'none';
|
|
708
|
+
} else {
|
|
709
|
+
window.location.href = '/auth?code=' + encodeURIComponent(data.userCode) + '&url=' + encodeURIComponent(data.verificationUriComplete);
|
|
710
|
+
}
|
|
711
|
+
} catch (err) {
|
|
712
|
+
errorEl.textContent = 'Failed to connect. Please try again.';
|
|
713
|
+
errorEl.style.display = 'block';
|
|
714
|
+
btn.disabled = false;
|
|
715
|
+
loading.style.display = 'none';
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
</script>
|
|
719
|
+
</body>
|
|
720
|
+
</html>`;
|
|
721
|
+
}
|
|
@@ -5,6 +5,7 @@ export declare const RegionSchema: z.ZodEnum<["us-east-1", "us-west-2"]>;
|
|
|
5
5
|
export type Region = z.infer<typeof RegionSchema>;
|
|
6
6
|
export declare const KiroConfigSchema: z.ZodObject<{
|
|
7
7
|
$schema: z.ZodOptional<z.ZodString>;
|
|
8
|
+
idc_start_url: z.ZodOptional<z.ZodString>;
|
|
8
9
|
account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin", "lowest-usage"]>>;
|
|
9
10
|
default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-west-2"]>>;
|
|
10
11
|
rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
@@ -33,8 +34,10 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
33
34
|
auto_sync_kiro_cli: boolean;
|
|
34
35
|
enable_log_api_request: boolean;
|
|
35
36
|
$schema?: string | undefined;
|
|
37
|
+
idc_start_url?: string | undefined;
|
|
36
38
|
}, {
|
|
37
39
|
$schema?: string | undefined;
|
|
40
|
+
idc_start_url?: string | undefined;
|
|
38
41
|
account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
|
|
39
42
|
default_region?: "us-east-1" | "us-west-2" | undefined;
|
|
40
43
|
rate_limit_retry_delay_ms?: number | undefined;
|
|
@@ -3,6 +3,7 @@ export const AccountSelectionStrategySchema = z.enum(['sticky', 'round-robin', '
|
|
|
3
3
|
export const RegionSchema = z.enum(['us-east-1', 'us-west-2']);
|
|
4
4
|
export const KiroConfigSchema = z.object({
|
|
5
5
|
$schema: z.string().optional(),
|
|
6
|
+
idc_start_url: z.string().url().optional(),
|
|
6
7
|
account_selection_strategy: AccountSelectionStrategySchema.default('lowest-usage'),
|
|
7
8
|
default_region: RegionSchema.default('us-east-1'),
|
|
8
9
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
package/dist/plugin/request.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import { KIRO_CONSTANTS } from '../constants.js';
|
|
4
|
-
import { buildHistory, extractToolNamesFromHistory, historyHasToolCalling, truncateHistory } from '../infrastructure/transformers/history-builder.js';
|
|
4
|
+
import { buildHistory, extractToolNamesFromHistory, historyHasToolCalling, injectSystemPrompt, truncateHistory } from '../infrastructure/transformers/history-builder.js';
|
|
5
5
|
import { findOriginalToolCall, getContentText, mergeAdjacentMessages, truncate } from '../infrastructure/transformers/message-transformer.js';
|
|
6
6
|
import { convertToolsToCodeWhisperer, deduplicateToolResults } from '../infrastructure/transformers/tool-transformer.js';
|
|
7
7
|
import { convertImagesToKiroFormat, extractAllImages, extractTextFromParts } from './image-handler.js';
|
|
@@ -13,20 +13,27 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
|
|
|
13
13
|
if (!messages || messages.length === 0)
|
|
14
14
|
throw new Error('No messages');
|
|
15
15
|
const resolved = resolveKiroModel(model);
|
|
16
|
+
const systemMsgs = messages.filter((m) => m.role === 'system');
|
|
17
|
+
const otherMsgs = messages.filter((m) => m.role !== 'system');
|
|
16
18
|
let sys = system || '';
|
|
19
|
+
if (systemMsgs.length > 0) {
|
|
20
|
+
const extractedSystem = systemMsgs.map((m) => getContentText(m)).join('\n\n');
|
|
21
|
+
sys = sys ? `${sys}\n\n${extractedSystem}` : extractedSystem;
|
|
22
|
+
}
|
|
17
23
|
if (think) {
|
|
18
24
|
const pfx = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>`;
|
|
19
25
|
sys = sys.includes('<thinking_mode>') ? sys : sys ? `${pfx}\n${sys}` : pfx;
|
|
20
26
|
}
|
|
21
|
-
const msgs = mergeAdjacentMessages([...
|
|
27
|
+
const msgs = mergeAdjacentMessages([...otherMsgs]);
|
|
22
28
|
const lastMsg = msgs[msgs.length - 1];
|
|
23
29
|
if (lastMsg && lastMsg.role === 'assistant' && getContentText(lastMsg) === '{')
|
|
24
30
|
msgs.pop();
|
|
25
31
|
const cwTools = tools ? convertToolsToCodeWhisperer(tools) : [];
|
|
26
32
|
const toolResultLimit = Math.floor(250000 * reductionFactor);
|
|
27
|
-
let history = buildHistory(msgs, resolved,
|
|
33
|
+
let history = buildHistory(msgs, resolved, toolResultLimit);
|
|
28
34
|
const historyLimit = Math.floor(850000 * reductionFactor);
|
|
29
35
|
history = truncateHistory(history, historyLimit);
|
|
36
|
+
history = injectSystemPrompt(history, sys, resolved);
|
|
30
37
|
const curMsg = msgs[msgs.length - 1];
|
|
31
38
|
if (!curMsg)
|
|
32
39
|
throw new Error('Empty');
|
package/dist/plugin/server.d.ts
CHANGED
|
@@ -17,8 +17,18 @@ export interface IDCAuthData {
|
|
|
17
17
|
interval: number;
|
|
18
18
|
expiresIn: number;
|
|
19
19
|
region: KiroRegion;
|
|
20
|
+
startUrl: string;
|
|
20
21
|
}
|
|
21
22
|
export declare function startIDCAuthServer(authData: IDCAuthData, startPort?: number, portRange?: number): Promise<{
|
|
22
23
|
url: string;
|
|
23
24
|
waitForAuth: () => Promise<KiroIDCTokenResult>;
|
|
24
25
|
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Starts a local auth server that first shows a Start URL input page.
|
|
28
|
+
* After the user submits, it calls authorizeKiroIDC internally and transitions
|
|
29
|
+
* to the verification code page — no need to call authorizeKiroIDC beforehand.
|
|
30
|
+
*/
|
|
31
|
+
export declare function startIDCAuthServerWithInput(region: KiroRegion, defaultStartUrl: string | undefined, startPort?: number, portRange?: number): Promise<{
|
|
32
|
+
url: string;
|
|
33
|
+
waitForAuth: () => Promise<KiroIDCTokenResult>;
|
|
34
|
+
}>;
|
package/dist/plugin/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeKiroIDC } from '../kiro/oauth-idc';
|
|
3
|
+
import { getErrorHtml, getIDCAuthHtml, getStartUrlInputHtml, getSuccessHtml } from './auth-page';
|
|
3
4
|
import * as logger from './logger';
|
|
4
5
|
async function tryPort(port) {
|
|
5
6
|
return new Promise((resolve) => {
|
|
@@ -76,7 +77,9 @@ export async function startIDCAuthServer(authData, startPort = 19847, portRange
|
|
|
76
77
|
const acc = d.access_token || d.accessToken, ref = d.refresh_token || d.refreshToken, exp = Date.now() + (d.expires_in || d.expiresIn || 0) * 1000;
|
|
77
78
|
let email = 'builder-id@aws.amazon.com';
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
+
// Derive user info URL from startUrl: replace /start with /api/user/info
|
|
81
|
+
const userInfoUrl = authData.startUrl.replace(/\/start\/?$/, '/api/user/info');
|
|
82
|
+
const infoRes = await fetch(userInfoUrl, {
|
|
80
83
|
headers: { Authorization: `Bearer ${acc}` }
|
|
81
84
|
});
|
|
82
85
|
if (infoRes.ok) {
|
|
@@ -164,3 +167,196 @@ export async function startIDCAuthServer(authData, startPort = 19847, portRange
|
|
|
164
167
|
});
|
|
165
168
|
});
|
|
166
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Starts a local auth server that first shows a Start URL input page.
|
|
172
|
+
* After the user submits, it calls authorizeKiroIDC internally and transitions
|
|
173
|
+
* to the verification code page — no need to call authorizeKiroIDC beforehand.
|
|
174
|
+
*/
|
|
175
|
+
export async function startIDCAuthServerWithInput(region, defaultStartUrl, startPort = 19847, portRange = 10) {
|
|
176
|
+
return new Promise(async (resolve, reject) => {
|
|
177
|
+
let port;
|
|
178
|
+
try {
|
|
179
|
+
port = await findAvailablePort(startPort, portRange);
|
|
180
|
+
logger.log(`Auth server (with input) will use port ${port}`);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
logger.error('Failed to find available port', error);
|
|
184
|
+
reject(error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let server = null;
|
|
188
|
+
let timeoutId = null;
|
|
189
|
+
let resolver = null;
|
|
190
|
+
let rejector = null;
|
|
191
|
+
const status = { status: 'pending' };
|
|
192
|
+
// authData is populated after the user submits the start URL form
|
|
193
|
+
let authData = null;
|
|
194
|
+
const cleanup = () => {
|
|
195
|
+
if (timeoutId)
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
if (server)
|
|
198
|
+
server.close();
|
|
199
|
+
};
|
|
200
|
+
const sendHtml = (res, html) => {
|
|
201
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
202
|
+
res.end(html);
|
|
203
|
+
};
|
|
204
|
+
const sendJson = (res, code, data) => {
|
|
205
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
206
|
+
res.end(JSON.stringify(data));
|
|
207
|
+
};
|
|
208
|
+
const poll = async (data) => {
|
|
209
|
+
try {
|
|
210
|
+
const body = {
|
|
211
|
+
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
212
|
+
deviceCode: data.deviceCode,
|
|
213
|
+
clientId: data.clientId,
|
|
214
|
+
clientSecret: data.clientSecret
|
|
215
|
+
};
|
|
216
|
+
const res = await fetch(`https://oidc.${data.region}.amazonaws.com/token`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
body: JSON.stringify(body)
|
|
220
|
+
});
|
|
221
|
+
const responseText = await res.text();
|
|
222
|
+
let d = {};
|
|
223
|
+
if (responseText) {
|
|
224
|
+
try {
|
|
225
|
+
d = JSON.parse(responseText);
|
|
226
|
+
}
|
|
227
|
+
catch (parseError) {
|
|
228
|
+
logger.error(`Auth polling error: Failed to parse JSON (status ${res.status})`, parseError);
|
|
229
|
+
throw parseError;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (res.ok) {
|
|
233
|
+
const acc = d.access_token || d.accessToken, ref = d.refresh_token || d.refreshToken, exp = Date.now() + (d.expires_in || d.expiresIn || 0) * 1000;
|
|
234
|
+
let email = 'builder-id@aws.amazon.com';
|
|
235
|
+
try {
|
|
236
|
+
const userInfoUrl = data.startUrl.replace(/\/start\/?$/, '/api/user/info');
|
|
237
|
+
const infoRes = await fetch(userInfoUrl, {
|
|
238
|
+
headers: { Authorization: `Bearer ${acc}` }
|
|
239
|
+
});
|
|
240
|
+
if (infoRes.ok) {
|
|
241
|
+
const info = await infoRes.json();
|
|
242
|
+
email = info.email || info.userName || email;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
logger.warn(`User info request failed with status ${infoRes.status}; using fallback email`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (infoError) {
|
|
249
|
+
logger.warn(`Failed to fetch user info; using fallback email: ${infoError?.message || infoError}`);
|
|
250
|
+
}
|
|
251
|
+
status.status = 'success';
|
|
252
|
+
if (resolver)
|
|
253
|
+
resolver({
|
|
254
|
+
email,
|
|
255
|
+
accessToken: acc,
|
|
256
|
+
refreshToken: ref,
|
|
257
|
+
expiresAt: exp,
|
|
258
|
+
clientId: data.clientId,
|
|
259
|
+
clientSecret: data.clientSecret
|
|
260
|
+
});
|
|
261
|
+
setTimeout(cleanup, 2000);
|
|
262
|
+
}
|
|
263
|
+
else if (d.error === 'authorization_pending') {
|
|
264
|
+
setTimeout(() => poll(data), data.interval * 1000);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
status.status = 'failed';
|
|
268
|
+
status.error = d.error_description || d.error;
|
|
269
|
+
logger.error(`Auth polling failed: ${status.error}`);
|
|
270
|
+
if (rejector)
|
|
271
|
+
rejector(new Error(status.error));
|
|
272
|
+
setTimeout(cleanup, 2000);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
status.status = 'failed';
|
|
277
|
+
status.error = e.message;
|
|
278
|
+
logger.error(`Auth polling error: ${e.message}`, e);
|
|
279
|
+
if (rejector)
|
|
280
|
+
rejector(e);
|
|
281
|
+
setTimeout(cleanup, 2000);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
server = createServer(async (req, res) => {
|
|
285
|
+
const u = req.url || '';
|
|
286
|
+
// Step 1: Show start URL input page
|
|
287
|
+
if (u === '/' || u === '') {
|
|
288
|
+
sendHtml(res, getStartUrlInputHtml(defaultStartUrl || '', `http://127.0.0.1:${port}/setup`));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Step 2: Receive start URL, call authorizeKiroIDC, return auth info to browser
|
|
292
|
+
if (u === '/setup' && req.method === 'POST') {
|
|
293
|
+
let body = '';
|
|
294
|
+
req.on('data', (chunk) => {
|
|
295
|
+
body += chunk;
|
|
296
|
+
});
|
|
297
|
+
req.on('end', async () => {
|
|
298
|
+
try {
|
|
299
|
+
const { startUrl } = JSON.parse(body);
|
|
300
|
+
const effectiveStartUrl = startUrl || undefined;
|
|
301
|
+
const data = await authorizeKiroIDC(region, effectiveStartUrl);
|
|
302
|
+
authData = data;
|
|
303
|
+
// Start polling now that we have device code
|
|
304
|
+
poll(authData);
|
|
305
|
+
sendJson(res, 200, {
|
|
306
|
+
userCode: data.userCode,
|
|
307
|
+
verificationUriComplete: data.verificationUriComplete
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
logger.error('authorizeKiroIDC failed', e);
|
|
312
|
+
sendJson(res, 500, { error: e.message || 'Failed to initialize authentication' });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Step 3: Show verification code page (browser redirects here after /setup)
|
|
318
|
+
if (u.startsWith('/auth')) {
|
|
319
|
+
const params = new URL(u, `http://127.0.0.1:${port}`).searchParams;
|
|
320
|
+
const code = params.get('code') || '';
|
|
321
|
+
const verUrl = params.get('url') || '';
|
|
322
|
+
sendHtml(res, getIDCAuthHtml(verUrl, code, `http://127.0.0.1:${port}/status`));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (u === '/status') {
|
|
326
|
+
sendJson(res, 200, status);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (u === '/success') {
|
|
330
|
+
sendHtml(res, getSuccessHtml());
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (u.startsWith('/error')) {
|
|
334
|
+
sendHtml(res, getErrorHtml(status.error || 'Failed'));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
res.writeHead(404);
|
|
338
|
+
res.end();
|
|
339
|
+
});
|
|
340
|
+
server.on('error', (e) => {
|
|
341
|
+
logger.error(`Auth server error on port ${port}`, e);
|
|
342
|
+
cleanup();
|
|
343
|
+
reject(e);
|
|
344
|
+
});
|
|
345
|
+
server.listen(port, '127.0.0.1', () => {
|
|
346
|
+
timeoutId = setTimeout(() => {
|
|
347
|
+
status.status = 'timeout';
|
|
348
|
+
logger.warn('Auth timeout waiting for authorization');
|
|
349
|
+
if (rejector)
|
|
350
|
+
rejector(new Error('Timeout'));
|
|
351
|
+
cleanup();
|
|
352
|
+
}, 900000);
|
|
353
|
+
resolve({
|
|
354
|
+
url: `http://127.0.0.1:${port}`,
|
|
355
|
+
waitForAuth: () => new Promise((rv, rj) => {
|
|
356
|
+
resolver = rv;
|
|
357
|
+
rejector = rj;
|
|
358
|
+
})
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
@@ -2,6 +2,7 @@ export function runMigrations(db) {
|
|
|
2
2
|
migrateToUniqueRefreshToken(db);
|
|
3
3
|
migrateRealEmailColumn(db);
|
|
4
4
|
migrateUsageTable(db);
|
|
5
|
+
migrateStartUrlColumn(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 migrateStartUrlColumn(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('start_url')) {
|
|
115
|
+
db.run('ALTER TABLE accounts ADD COLUMN start_url TEXT');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -29,6 +29,7 @@ export class KiroDatabase {
|
|
|
29
29
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
30
30
|
id TEXT PRIMARY KEY, email TEXT NOT NULL, auth_method TEXT NOT NULL,
|
|
31
31
|
region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
|
|
32
|
+
start_url TEXT,
|
|
32
33
|
refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
33
34
|
rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
|
|
34
35
|
recovery_time INTEGER, fail_count INTEGER DEFAULT 0, last_used INTEGER DEFAULT 0,
|
|
@@ -45,20 +46,21 @@ export class KiroDatabase {
|
|
|
45
46
|
.prepare(`
|
|
46
47
|
INSERT INTO accounts (
|
|
47
48
|
id, email, auth_method, region, client_id, client_secret,
|
|
48
|
-
profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
|
|
49
|
+
profile_arn, start_url, refresh_token, access_token, expires_at, rate_limit_reset,
|
|
49
50
|
is_healthy, unhealthy_reason, recovery_time, fail_count, last_used,
|
|
50
51
|
used_count, limit_count, last_sync
|
|
51
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
52
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
52
53
|
ON CONFLICT(refresh_token) DO UPDATE SET
|
|
53
54
|
id=excluded.id, email=excluded.email, auth_method=excluded.auth_method,
|
|
54
55
|
region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
|
|
55
|
-
profile_arn=excluded.profile_arn,
|
|
56
|
+
profile_arn=excluded.profile_arn, start_url=excluded.start_url,
|
|
57
|
+
access_token=excluded.access_token, expires_at=excluded.expires_at,
|
|
56
58
|
rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
|
|
57
59
|
unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
|
|
58
60
|
fail_count=excluded.fail_count, last_used=excluded.last_used,
|
|
59
61
|
used_count=excluded.used_count, limit_count=excluded.limit_count, last_sync=excluded.last_sync
|
|
60
62
|
`)
|
|
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);
|
|
63
|
+
.run(acc.id, acc.email, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || 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
64
|
}
|
|
63
65
|
async upsertAccount(acc) {
|
|
64
66
|
await withDatabaseLock(this.path, async () => {
|
|
@@ -110,6 +112,7 @@ export class KiroDatabase {
|
|
|
110
112
|
clientId: row.client_id,
|
|
111
113
|
clientSecret: row.client_secret,
|
|
112
114
|
profileArn: row.profile_arn,
|
|
115
|
+
startUrl: row.start_url || undefined,
|
|
113
116
|
refreshToken: row.refresh_token,
|
|
114
117
|
accessToken: row.access_token,
|
|
115
118
|
expiresAt: row.expires_at,
|
package/dist/plugin/types.d.ts
CHANGED
package/dist/plugin.d.ts
CHANGED
|
@@ -6,12 +6,57 @@ export declare const createKiroPlugin: (id: string) => ({ client, directory }: a
|
|
|
6
6
|
baseURL: string;
|
|
7
7
|
fetch: (input: any, init?: any) => Promise<Response>;
|
|
8
8
|
}>;
|
|
9
|
-
methods: {
|
|
10
|
-
id: string;
|
|
11
|
-
label: string;
|
|
9
|
+
methods: ({
|
|
12
10
|
type: "oauth";
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
label: string;
|
|
12
|
+
prompts?: Array<{
|
|
13
|
+
type: "text";
|
|
14
|
+
key: string;
|
|
15
|
+
message: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
validate?: (value: string) => string | undefined;
|
|
18
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
19
|
+
} | {
|
|
20
|
+
type: "select";
|
|
21
|
+
key: string;
|
|
22
|
+
message: string;
|
|
23
|
+
options: Array<{
|
|
24
|
+
label: string;
|
|
25
|
+
value: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
}>;
|
|
28
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
29
|
+
}>;
|
|
30
|
+
authorize(inputs?: Record<string, string>): Promise<import("@opencode-ai/plugin").AuthOuathResult>;
|
|
31
|
+
} | {
|
|
32
|
+
type: "api";
|
|
33
|
+
label: string;
|
|
34
|
+
prompts?: Array<{
|
|
35
|
+
type: "text";
|
|
36
|
+
key: string;
|
|
37
|
+
message: string;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
validate?: (value: string) => string | undefined;
|
|
40
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
41
|
+
} | {
|
|
42
|
+
type: "select";
|
|
43
|
+
key: string;
|
|
44
|
+
message: string;
|
|
45
|
+
options: Array<{
|
|
46
|
+
label: string;
|
|
47
|
+
value: string;
|
|
48
|
+
hint?: string;
|
|
49
|
+
}>;
|
|
50
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
51
|
+
}>;
|
|
52
|
+
authorize?(inputs?: Record<string, string>): Promise<{
|
|
53
|
+
type: "success";
|
|
54
|
+
key: string;
|
|
55
|
+
provider?: string;
|
|
56
|
+
} | {
|
|
57
|
+
type: "failed";
|
|
58
|
+
}>;
|
|
59
|
+
})[];
|
|
15
60
|
};
|
|
16
61
|
}>;
|
|
17
62
|
export declare const KiroOAuthPlugin: ({ client, directory }: any) => Promise<{
|
|
@@ -22,11 +67,56 @@ export declare const KiroOAuthPlugin: ({ client, directory }: any) => Promise<{
|
|
|
22
67
|
baseURL: string;
|
|
23
68
|
fetch: (input: any, init?: any) => Promise<Response>;
|
|
24
69
|
}>;
|
|
25
|
-
methods: {
|
|
26
|
-
id: string;
|
|
27
|
-
label: string;
|
|
70
|
+
methods: ({
|
|
28
71
|
type: "oauth";
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
label: string;
|
|
73
|
+
prompts?: Array<{
|
|
74
|
+
type: "text";
|
|
75
|
+
key: string;
|
|
76
|
+
message: string;
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
validate?: (value: string) => string | undefined;
|
|
79
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
80
|
+
} | {
|
|
81
|
+
type: "select";
|
|
82
|
+
key: string;
|
|
83
|
+
message: string;
|
|
84
|
+
options: Array<{
|
|
85
|
+
label: string;
|
|
86
|
+
value: string;
|
|
87
|
+
hint?: string;
|
|
88
|
+
}>;
|
|
89
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
90
|
+
}>;
|
|
91
|
+
authorize(inputs?: Record<string, string>): Promise<import("@opencode-ai/plugin").AuthOuathResult>;
|
|
92
|
+
} | {
|
|
93
|
+
type: "api";
|
|
94
|
+
label: string;
|
|
95
|
+
prompts?: Array<{
|
|
96
|
+
type: "text";
|
|
97
|
+
key: string;
|
|
98
|
+
message: string;
|
|
99
|
+
placeholder?: string;
|
|
100
|
+
validate?: (value: string) => string | undefined;
|
|
101
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
102
|
+
} | {
|
|
103
|
+
type: "select";
|
|
104
|
+
key: string;
|
|
105
|
+
message: string;
|
|
106
|
+
options: Array<{
|
|
107
|
+
label: string;
|
|
108
|
+
value: string;
|
|
109
|
+
hint?: string;
|
|
110
|
+
}>;
|
|
111
|
+
condition?: (inputs: Record<string, string>) => boolean;
|
|
112
|
+
}>;
|
|
113
|
+
authorize?(inputs?: Record<string, string>): Promise<{
|
|
114
|
+
type: "success";
|
|
115
|
+
key: string;
|
|
116
|
+
provider?: string;
|
|
117
|
+
} | {
|
|
118
|
+
type: "failed";
|
|
119
|
+
}>;
|
|
120
|
+
})[];
|
|
31
121
|
};
|
|
32
122
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhafron/opencode-kiro-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@opencode-ai/plugin": "^
|
|
34
|
+
"@opencode-ai/plugin": "^1.2.6",
|
|
35
35
|
"proper-lockfile": "^4.1.2",
|
|
36
36
|
"zod": "^3.24.0"
|
|
37
37
|
},
|