@zhafron/opencode-kiro-auth 1.5.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,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
- - Follow the instructions for **AWS Builder ID (IDC)**.
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).
@@ -20,5 +20,6 @@ export declare const KIRO_AUTH_SERVICE: {
20
20
  ENDPOINT: string;
21
21
  SSO_OIDC_ENDPOINT: string;
22
22
  BUILDER_ID_START_URL: string;
23
+ USER_INFO_URL: string;
23
24
  SCOPES: string[];
24
25
  };
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': 'CLAUDE_OPUS_4_6_V1',
43
- 'claude-opus-4-6-thinking': 'CLAUDE_OPUS_4_6_V1',
44
- 'claude-opus-4-6-1m': 'CLAUDE_OPUS_4_6_LONG_V1',
45
- 'claude-opus-4-6-1m-thinking': 'CLAUDE_OPUS_4_6_LONG_V1',
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(): Array<{
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
- id: 'idc',
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?: any): Promise<{
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 { startIDCAuthServer } from '../../plugin/server.js';
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 authData = await authorizeKiroIDC(region);
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 authData = await authorizeKiroIDC(region);
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,
@@ -18,6 +18,7 @@ export class AccountRepository {
18
18
  clientId: r.client_id,
19
19
  clientSecret: r.client_secret,
20
20
  profileArn: r.profile_arn,
21
+ startUrl: r.start_url || undefined,
21
22
  refreshToken: r.refresh_token,
22
23
  accessToken: r.access_token,
23
24
  expiresAt: r.expires_at,
@@ -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>;
@@ -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: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL
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) {
@@ -30,6 +30,7 @@ export class AccountManager {
30
30
  clientId: r.client_id,
31
31
  clientSecret: r.client_secret,
32
32
  profileArn: r.profile_arn,
33
+ startUrl: r.start_url || undefined,
33
34
  refreshToken: r.refresh_token,
34
35
  accessToken: r.access_token,
35
36
  expiresAt: r.expires_at,
@@ -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;
@@ -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),
@@ -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
+ }>;
@@ -1,5 +1,6 @@
1
1
  import { createServer } from 'node:http';
2
- import { getErrorHtml, getIDCAuthHtml, getSuccessHtml } from './auth-page';
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
- const infoRes = await fetch('https://view.awsapps.com/api/user/info', {
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, access_token=excluded.access_token, expires_at=excluded.expires_at,
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,
@@ -26,6 +26,7 @@ export interface ManagedAccount {
26
26
  clientId?: string;
27
27
  clientSecret?: string;
28
28
  profileArn?: string;
29
+ startUrl?: string;
29
30
  refreshToken: string;
30
31
  accessToken: string;
31
32
  expiresAt: number;
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
- authorize: (inputs?: any) => Promise<any>;
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
- authorize: (inputs?: any) => Promise<any>;
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.5.3",
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": "^0.15.30",
34
+ "@opencode-ai/plugin": "^1.2.6",
35
35
  "proper-lockfile": "^4.1.2",
36
36
  "zod": "^3.24.0"
37
37
  },