@zhafron/opencode-kiro-auth 1.5.2 → 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 +52 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +9 -0
- 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/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/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.
|
|
@@ -61,6 +61,36 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
|
|
|
61
61
|
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
+
"claude-opus-4-6": {
|
|
65
|
+
"name": "Claude Opus 4.6",
|
|
66
|
+
"limit": { "context": 200000, "output": 64000 },
|
|
67
|
+
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
|
|
68
|
+
},
|
|
69
|
+
"claude-opus-4-6-thinking": {
|
|
70
|
+
"name": "Claude Opus 4.6 Thinking",
|
|
71
|
+
"limit": { "context": 200000, "output": 64000 },
|
|
72
|
+
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
|
|
73
|
+
"variants": {
|
|
74
|
+
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
|
|
75
|
+
"medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
|
|
76
|
+
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"claude-opus-4-6-1m": {
|
|
80
|
+
"name": "Claude Opus 4.6 (1M Context)",
|
|
81
|
+
"limit": { "context": 1000000, "output": 64000 },
|
|
82
|
+
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
|
|
83
|
+
},
|
|
84
|
+
"claude-opus-4-6-1m-thinking": {
|
|
85
|
+
"name": "Claude Opus 4.6 (1M Context) Thinking",
|
|
86
|
+
"limit": { "context": 1000000, "output": 64000 },
|
|
87
|
+
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
|
|
88
|
+
"variants": {
|
|
89
|
+
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
|
|
90
|
+
"medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
|
|
91
|
+
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
64
94
|
"claude-sonnet-4-5-1m": {
|
|
65
95
|
"name": "Claude Sonnet 4.5 1M",
|
|
66
96
|
"limit": { "context": 1000000, "output": 64000 },
|
|
@@ -86,7 +116,10 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
|
|
|
86
116
|
2. **Direct Authentication**:
|
|
87
117
|
- Run `opencode auth login`.
|
|
88
118
|
- Select `Other`, type `kiro`, and press enter.
|
|
89
|
-
-
|
|
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.
|
|
90
123
|
3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
|
|
91
124
|
|
|
92
125
|
## Troubleshooting
|
|
@@ -101,6 +134,21 @@ This happens when the plugin has no records in `~/.config/opencode/kiro.db`.
|
|
|
101
134
|
|
|
102
135
|
Note for IDC/SSO (ODIC): the plugin may temporarily create an account with a placeholder email if it cannot fetch the real email during sync (e.g. offline). It will replace it with the real email once usage/email lookup succeeds.
|
|
103
136
|
|
|
137
|
+
### Error: ERR_INVALID_URL
|
|
138
|
+
|
|
139
|
+
`TypeError [ERR_INVALID_URL]: "undefined/chat/completions" cannot be parsed as a URL`
|
|
140
|
+
|
|
141
|
+
If this happens, check your auth.json in .local/share/opencode. example:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"kiro": {
|
|
146
|
+
"type": "api",
|
|
147
|
+
"key": "whatever"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
104
152
|
## Configuration
|
|
105
153
|
|
|
106
154
|
The plugin supports extensive configuration options. Edit `~/.config/opencode/kiro.json`:
|
|
@@ -110,6 +158,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
|
|
|
110
158
|
"auto_sync_kiro_cli": true,
|
|
111
159
|
"account_selection_strategy": "lowest-usage",
|
|
112
160
|
"default_region": "us-east-1",
|
|
161
|
+
"idc_start_url": "https://your-company.awsapps.com/start",
|
|
113
162
|
"rate_limit_retry_delay_ms": 5000,
|
|
114
163
|
"rate_limit_max_retries": 3,
|
|
115
164
|
"max_request_iterations": 20,
|
|
@@ -128,6 +177,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
|
|
|
128
177
|
- `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
|
|
129
178
|
- `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
|
|
130
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.
|
|
131
181
|
- `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
|
|
132
182
|
- `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10).
|
|
133
183
|
- `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000).
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -37,8 +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',
|
|
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',
|
|
42
50
|
'claude-sonnet-4': 'CLAUDE_SONNET_4_20250514_V1_0',
|
|
43
51
|
'claude-3-7-sonnet': 'CLAUDE_3_7_SONNET_20250219_V1_0',
|
|
44
52
|
'nova-swe': 'AGI_NOVA_SWE_V1_5',
|
|
@@ -52,6 +60,7 @@ export const KIRO_AUTH_SERVICE = {
|
|
|
52
60
|
ENDPOINT: 'https://prod.{{region}}.auth.desktop.kiro.dev',
|
|
53
61
|
SSO_OIDC_ENDPOINT: 'https://oidc.{{region}}.amazonaws.com',
|
|
54
62
|
BUILDER_ID_START_URL: 'https://view.awsapps.com/start',
|
|
63
|
+
USER_INFO_URL: 'https://view.awsapps.com/api/user/info',
|
|
55
64
|
SCOPES: [
|
|
56
65
|
'codewhisperer:completions',
|
|
57
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,
|
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/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.0",
|
|
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
|
},
|