@bogyie/opencode-kiro-plugin 0.3.6 → 0.3.8

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
@@ -34,15 +34,15 @@ Then add the plugin to your OpenCode config. See [examples/opencode.jsonc](examp
34
34
  The plugin resolves credentials in this order:
35
35
 
36
36
  1. `KIRO_API_KEY`
37
- 2. OpenCode auth input for provider `kiro`
37
+ 2. OpenCode auth input for provider `kiro`, including stored Kiro device-flow credentials
38
38
  3. Local Kiro CLI session token from the Kiro CLI SQLite store
39
39
  4. `kiro-cli whoami` diagnostics for CLI session visibility
40
40
 
41
- When no usable Kiro CLI session is available, OpenCode's startup model discovery may start the same Kiro CLI login flow automatically. The plugin runs `kiro-cli login`, waits for login to complete through `kiro-cli whoami`, and then runs the model discovery command one more time. If that login fails or times out, discovery fails for that startup attempt; it does not loop.
41
+ OpenCode startup does not start Kiro login. The provider is registered immediately with an `auto` placeholder model, then model discovery warms the in-memory cache in the background. If discovery succeeds, later model-list requests use the latest cached list; if discovery fails, the previous cache remains in use and Kiro stays visible with `auto`.
42
42
 
43
- You can also open OpenCode's provider connector, choose Kiro, and select `Kiro CLI login`. The connector flow starts `kiro-cli login`, waits until the login URL/device code is visible, returns it to the connector, and then waits until `kiro-cli whoami` succeeds. Direct fetch mode uses a usable API key/token when one is configured; otherwise it reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If credentials expire during use, direct fetch and `cli-chat` start the shared Kiro login flow, wait for it to complete, and retry the failed request once. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state. `acp` mode uses the official `kiro-cli acp` surface, but is still treated as an explicit backend while its real-world protocol behavior is validated across Kiro CLI versions.
43
+ You can open OpenCode's provider connector, choose Kiro, and select `Kiro device login`. The connector uses AWS OIDC device authorization directly, so it does not rely on a localhost callback URL. After login succeeds, the plugin stores the access token, refresh token, OIDC client credentials, region, and optional profile ARN in OpenCode auth. Direct fetch mode reuses that stored credential and refreshes the access token before expiry; it does not ask you to log in again while the refresh token remains valid. If no OpenCode device credential or API key is configured, direct fetch reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If an API/model call fails with an auth error, the selected transport starts the Kiro login flow and retries the request once. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state. `acp` mode uses the official `kiro-cli acp` surface, but is still treated as an explicit backend while its real-world protocol behavior is validated across Kiro CLI versions.
44
44
 
45
- For AWS IAM Identity Center login, configure the CLI login arguments separately from the API region:
45
+ For AWS IAM Identity Center login, configure the default device-flow Start URL separately from the API region:
46
46
 
47
47
  ```jsonc
48
48
  {
@@ -62,7 +62,7 @@ For AWS IAM Identity Center login, configure the CLI login arguments separately
62
62
  }
63
63
  ```
64
64
 
65
- This runs `kiro-cli login --license pro --identity-provider https://example.awsapps.com/start --region ap-northeast-2`. Do not enable `login.useDeviceFlow` for the normal browser callback flow; Kiro CLI needs to keep its localhost callback listener alive while the browser redirects back.
65
+ The connector can also prompt for the Start URL, OIDC region, and optional profile ARN. For IAM Identity Center, the plugin opens the AWS portal device URL, such as `https://example.awsapps.com/start/#/device?user_code=...`, instead of a `localhost` callback URL.
66
66
 
67
67
  Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
68
68
 
@@ -78,7 +78,7 @@ Configure the backend through plugin options:
78
78
  {
79
79
  "backend": "auto",
80
80
  "region": "us-east-1",
81
- // Optional Kiro CLI login overrides:
81
+ // Optional Kiro device login defaults:
82
82
  // "login": {
83
83
  // "license": "pro",
84
84
  // "identityProvider": "https://example.awsapps.com/start",
@@ -105,7 +105,7 @@ Supported values:
105
105
 
106
106
  ## Model Churn Handling
107
107
 
108
- The resolver intentionally avoids a hard whitelist. By default, the plugin reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json`. Runtime discovery is the source of truth for the model picker; if discovery cannot return any models, the plugin exposes an `auto` placeholder so OpenCode keeps the Kiro provider visible for connection.
108
+ The resolver intentionally avoids a hard whitelist. Startup always begins with the `auto` placeholder so OpenCode can show Kiro without waiting for Kiro auth or model discovery. By default, the plugin then reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json` in the background. Runtime discovery is the source of truth for the model picker once it has succeeded; failed refreshes do not clear the last successful cache.
109
109
 
110
110
  Useful options:
111
111
 
@@ -151,11 +151,11 @@ Resolution order:
151
151
  6. Hidden/manual model mapping
152
152
  7. Optimistic pass-through unless disabled
153
153
 
154
- This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker. Use `extraModels` only when you explicitly want a model to appear even though it is not listed by your installed Kiro CLI. Set `disableModelPassThrough: true` only when you need strict model governance.
154
+ This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker after discovery has succeeded. Use `extraModels` only when you explicitly want a model to appear even though it is not listed by your installed Kiro CLI. Set `disableModelPassThrough: true` only when you need strict model governance.
155
155
 
156
156
  The plugin injects `provider.kiro` automatically. You only need to add `provider.kiro.models` yourself when overriding OpenCode model-picker metadata, such as a display name or context limit. Use plugin `modelAliases` for aliases that should resolve one requested model id to another.
157
157
 
158
- `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. If this command fails with an auth error, the plugin starts Kiro CLI login, waits for it to finish, and retries the command once. Discovery stdout can be a JSON array, `{ "models": [...] }`, `{ "data": [...] }`, Kiro CLI list-models JSON, Kiro CLI plain list output, or one model id per line.
158
+ `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. Discovery is best-effort and never starts login during OpenCode startup; authenticate manually through the connector or let an API/model request handle auth failure. Discovery stdout can be a JSON array, `{ "models": [...] }`, `{ "data": [...] }`, Kiro CLI list-models JSON, Kiro CLI plain list output, or one model id per line.
159
159
 
160
160
  ## Troubleshooting
161
161
 
package/dist/auth.d.ts CHANGED
@@ -30,6 +30,7 @@ export interface KiroCliSessionCredentialOptions {
30
30
  readonly dbPath?: string;
31
31
  readonly tokenKeys?: ReadonlyArray<string>;
32
32
  }
33
+ export type KiroAuthFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
33
34
  export interface KiroLoginSession {
34
35
  readonly url: string;
35
36
  readonly code: string | undefined;
@@ -44,6 +45,29 @@ export interface KiroCliLoginOptions {
44
45
  readonly useDeviceFlow?: boolean;
45
46
  readonly extraArgs?: ReadonlyArray<string>;
46
47
  }
48
+ export interface KiroDeviceAuthorization {
49
+ readonly verificationUrl: string;
50
+ readonly verificationUrlComplete: string;
51
+ readonly userCode: string;
52
+ readonly deviceCode: string;
53
+ readonly clientId: string;
54
+ readonly clientSecret: string;
55
+ readonly intervalSeconds: number;
56
+ readonly expiresInSeconds: number;
57
+ readonly oidcRegion: string;
58
+ readonly startUrl: string;
59
+ }
60
+ export interface KiroDeviceAuthCredential {
61
+ readonly accessToken: string;
62
+ readonly refreshToken: string;
63
+ readonly expiresAt: number;
64
+ readonly clientId: string;
65
+ readonly clientSecret: string;
66
+ readonly oidcRegion: string;
67
+ readonly region: string;
68
+ readonly startUrl: string;
69
+ readonly profileArn?: string;
70
+ }
47
71
  export interface KiroLoginFlowOptions {
48
72
  readonly spawner?: ProcessSpawner;
49
73
  readonly runner?: CommandRunner;
@@ -56,6 +80,14 @@ export declare const DEFAULT_KIRO_CLI_TOKEN_KEYS: readonly ["kirocli:odic:token"
56
80
  export declare function runCommand(command: string, args: ReadonlyArray<string>, options?: CommandRunOptions): Promise<CommandResult>;
57
81
  export declare function regionFromProfileArn(profileArn: string | undefined): string | undefined;
58
82
  export declare function readKiroCliSessionCredential(options?: KiroCliSessionCredentialOptions, runner?: CommandRunner): Promise<KiroCliSessionCredential | undefined>;
83
+ export declare function kiroDeviceVerificationUrl(startUrl: string, userCode: string): string;
84
+ export declare function authorizeKiroDevice(options?: KiroCliLoginOptions, fetcher?: KiroAuthFetch): Promise<KiroDeviceAuthorization>;
85
+ export declare function pollKiroDeviceToken(authorization: KiroDeviceAuthorization, options: Pick<KiroDeviceAuthCredential, "region" | "profileArn">, fetcher?: KiroAuthFetch): Promise<KiroDeviceAuthCredential>;
86
+ export declare function refreshKiroDeviceAuthCredential(credential: KiroDeviceAuthCredential, fetcher?: KiroAuthFetch): Promise<KiroDeviceAuthCredential>;
87
+ export declare function encodeKiroDeviceAuthKey(credential: KiroDeviceAuthCredential): string;
88
+ export declare function decodeKiroDeviceAuthKey(key: string | undefined): KiroDeviceAuthCredential | undefined;
89
+ export declare function isKiroDeviceAuthKey(key: string | undefined): boolean;
90
+ export declare function credentialFromKiroDeviceAuthKey(key: string, fetcher?: KiroAuthFetch): Promise<KiroCliSessionCredential | undefined>;
59
91
  export declare function extractKiroLoginUrl(output: string): string;
60
92
  export declare function extractKiroLoginCode(output: string): string | undefined;
61
93
  export declare function kiroCliLoginArgs(options?: KiroCliLoginOptions): string[];
@@ -67,4 +99,5 @@ export declare function detectAuth(env?: NodeJS.ProcessEnv, runner?: CommandRunn
67
99
  export declare function resolveApiKey(auth: () => Promise<{
68
100
  type: string;
69
101
  key?: string;
102
+ access?: string;
70
103
  }>, env?: NodeJS.ProcessEnv): Promise<string>;
package/dist/auth.js CHANGED
@@ -8,12 +8,23 @@ const execFileAsync = promisify(execFile);
8
8
  export const KIRO_LOGIN_URL = "https://view.awsapps.com/start";
9
9
  export const DEFAULT_KIRO_CLI_DB_PATH = join(homedir(), "Library", "Application Support", "kiro-cli", "data.sqlite3");
10
10
  export const DEFAULT_KIRO_CLI_TOKEN_KEYS = ["kirocli:odic:token", "codewhisperer:odic:token"];
11
+ const KIRO_DEVICE_AUTH_KEY_PREFIX = "kiro-device:";
12
+ const KIRO_OIDC_SCOPES = [
13
+ "codewhisperer:completions",
14
+ "codewhisperer:analysis",
15
+ "codewhisperer:conversations",
16
+ "codewhisperer:transformations",
17
+ "codewhisperer:taskassist",
18
+ ];
11
19
  const LOGIN_REUSE_WINDOW_MS = 2 * 60 * 1000;
20
+ const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
12
21
  let sharedLoginSession;
13
22
  let sharedLoginStartedAt = 0;
14
23
  let sharedLoginSessionKey = "";
15
24
  let sharedLoginFlow;
16
25
  let sharedLoginFlowKey = "";
26
+ const refreshedDeviceCredentials = new Map();
27
+ const deviceRefreshes = new Map();
17
28
  export async function runCommand(command, args, options = {}) {
18
29
  try {
19
30
  const result = await execFileAsync(command, [...args], { timeout: options.timeoutMs ?? 5000 });
@@ -41,6 +52,14 @@ function jsonObject(value) {
41
52
  return undefined;
42
53
  }
43
54
  }
55
+ function numberField(input, ...keys) {
56
+ for (const key of keys) {
57
+ const value = input?.[key];
58
+ if (typeof value === "number" && Number.isFinite(value))
59
+ return value;
60
+ }
61
+ return undefined;
62
+ }
44
63
  function stringField(input, ...keys) {
45
64
  for (const key of keys) {
46
65
  const value = input?.[key];
@@ -98,6 +117,243 @@ export async function readKiroCliSessionCredential(options = {}, runner = runCom
98
117
  function delay(ms) {
99
118
  return new Promise((resolve) => setTimeout(resolve, ms));
100
119
  }
120
+ function normalizeStartUrl(raw) {
121
+ const value = raw?.trim() || KIRO_LOGIN_URL;
122
+ const url = new URL(value);
123
+ url.hash = "";
124
+ url.search = "";
125
+ if (url.pathname.endsWith("/start/"))
126
+ url.pathname = url.pathname.replace(/\/start\/$/, "/start");
127
+ if (!url.pathname.endsWith("/start"))
128
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/start`;
129
+ return url.toString();
130
+ }
131
+ export function kiroDeviceVerificationUrl(startUrl, userCode) {
132
+ const url = new URL(startUrl);
133
+ url.search = "";
134
+ if (url.pathname.endsWith("/start"))
135
+ url.pathname = `${url.pathname}/`;
136
+ url.pathname = url.pathname.replace(/\/start\/?$/, "/start/");
137
+ url.hash = `#/device?user_code=${encodeURIComponent(userCode)}`;
138
+ return url.toString();
139
+ }
140
+ function oidcEndpoint(region) {
141
+ return `https://oidc.${region || "us-east-1"}.amazonaws.com`;
142
+ }
143
+ function deviceAuthUserAgent() {
144
+ return "KiroIDE";
145
+ }
146
+ async function jsonResponse(response) {
147
+ const text = await response.text();
148
+ const parsed = jsonObject(text);
149
+ if (!parsed)
150
+ throw new Error(`Kiro auth returned invalid JSON: ${text.slice(0, 200)}`);
151
+ if (!response.ok) {
152
+ const message = stringField(parsed, "message", "error_description", "error") ?? text.slice(0, 200);
153
+ throw new Error(`Kiro auth request failed: HTTP ${response.status} ${message}`);
154
+ }
155
+ return parsed;
156
+ }
157
+ export async function authorizeKiroDevice(options = {}, fetcher = fetch) {
158
+ const oidcRegion = options.region || "us-east-1";
159
+ const startUrl = normalizeStartUrl(options.identityProvider);
160
+ const endpoint = oidcEndpoint(oidcRegion);
161
+ const register = await jsonResponse(await fetcher(`${endpoint}/client/register`, {
162
+ method: "POST",
163
+ headers: {
164
+ "content-type": "application/json",
165
+ "user-agent": deviceAuthUserAgent(),
166
+ },
167
+ body: JSON.stringify({
168
+ clientName: "Kiro IDE",
169
+ clientType: "public",
170
+ scopes: KIRO_OIDC_SCOPES,
171
+ grantTypes: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
172
+ }),
173
+ }));
174
+ const clientId = stringField(register, "clientId", "client_id");
175
+ const clientSecret = stringField(register, "clientSecret", "client_secret");
176
+ if (!clientId || !clientSecret)
177
+ throw new Error("Kiro auth client registration did not return client credentials.");
178
+ const authorization = await jsonResponse(await fetcher(`${endpoint}/device_authorization`, {
179
+ method: "POST",
180
+ headers: {
181
+ "content-type": "application/json",
182
+ "user-agent": deviceAuthUserAgent(),
183
+ },
184
+ body: JSON.stringify({
185
+ clientId,
186
+ clientSecret,
187
+ startUrl,
188
+ }),
189
+ }));
190
+ const verificationUrl = stringField(authorization, "verificationUri", "verification_uri");
191
+ const verificationUrlComplete = stringField(authorization, "verificationUriComplete", "verification_uri_complete");
192
+ const userCode = stringField(authorization, "userCode", "user_code");
193
+ const deviceCode = stringField(authorization, "deviceCode", "device_code");
194
+ if (!verificationUrl || !verificationUrlComplete || !userCode || !deviceCode) {
195
+ throw new Error("Kiro device authorization response did not return required fields.");
196
+ }
197
+ return {
198
+ verificationUrl,
199
+ verificationUrlComplete,
200
+ userCode,
201
+ deviceCode,
202
+ clientId,
203
+ clientSecret,
204
+ intervalSeconds: numberField(authorization, "interval") ?? 5,
205
+ expiresInSeconds: numberField(authorization, "expiresIn", "expires_in") ?? 600,
206
+ oidcRegion,
207
+ startUrl,
208
+ };
209
+ }
210
+ export async function pollKiroDeviceToken(authorization, options, fetcher = fetch) {
211
+ const maxAttempts = Math.max(1, Math.floor(authorization.expiresInSeconds / authorization.intervalSeconds));
212
+ let intervalMs = authorization.intervalSeconds * 1000;
213
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
214
+ await delay(intervalMs);
215
+ const response = await fetcher(`${oidcEndpoint(authorization.oidcRegion)}/token`, {
216
+ method: "POST",
217
+ headers: {
218
+ "content-type": "application/json",
219
+ "user-agent": deviceAuthUserAgent(),
220
+ },
221
+ body: JSON.stringify({
222
+ clientId: authorization.clientId,
223
+ clientSecret: authorization.clientSecret,
224
+ deviceCode: authorization.deviceCode,
225
+ grantType: "urn:ietf:params:oauth:grant-type:device_code",
226
+ }),
227
+ });
228
+ const data = await response.text().then((text) => jsonObject(text) ?? {});
229
+ const error = stringField(data, "error");
230
+ if (error === "authorization_pending")
231
+ continue;
232
+ if (error === "slow_down") {
233
+ intervalMs += 5000;
234
+ continue;
235
+ }
236
+ if (error) {
237
+ const description = stringField(data, "error_description") ?? "";
238
+ throw new Error(`Kiro device authorization failed: ${error}${description ? ` - ${description}` : ""}`);
239
+ }
240
+ if (!response.ok) {
241
+ throw new Error(`Kiro device token request failed: HTTP ${response.status}`);
242
+ }
243
+ const accessToken = stringField(data, "access_token", "accessToken");
244
+ const refreshToken = stringField(data, "refresh_token", "refreshToken");
245
+ if (!accessToken || !refreshToken)
246
+ throw new Error("Kiro device token response did not return access and refresh tokens.");
247
+ const expiresIn = numberField(data, "expires_in", "expiresIn") ?? 3600;
248
+ return {
249
+ accessToken,
250
+ refreshToken,
251
+ expiresAt: Date.now() + expiresIn * 1000,
252
+ clientId: authorization.clientId,
253
+ clientSecret: authorization.clientSecret,
254
+ oidcRegion: authorization.oidcRegion,
255
+ region: options.profileArn ? (regionFromProfileArn(options.profileArn) ?? options.region) : options.region,
256
+ startUrl: authorization.startUrl,
257
+ ...(options.profileArn ? { profileArn: options.profileArn } : {}),
258
+ };
259
+ }
260
+ throw new Error("Kiro device authorization timed out.");
261
+ }
262
+ export async function refreshKiroDeviceAuthCredential(credential, fetcher = fetch) {
263
+ const data = await jsonResponse(await fetcher(`${oidcEndpoint(credential.oidcRegion)}/token`, {
264
+ method: "POST",
265
+ headers: {
266
+ "content-type": "application/json",
267
+ "user-agent": deviceAuthUserAgent(),
268
+ },
269
+ body: JSON.stringify({
270
+ refreshToken: credential.refreshToken,
271
+ clientId: credential.clientId,
272
+ clientSecret: credential.clientSecret,
273
+ grantType: "refresh_token",
274
+ }),
275
+ }));
276
+ const accessToken = stringField(data, "access_token", "accessToken");
277
+ if (!accessToken)
278
+ throw new Error("Kiro refresh token response did not return an access token.");
279
+ const refreshToken = stringField(data, "refresh_token", "refreshToken") ?? credential.refreshToken;
280
+ const expiresIn = numberField(data, "expires_in", "expiresIn") ?? 3600;
281
+ return {
282
+ ...credential,
283
+ accessToken,
284
+ refreshToken,
285
+ expiresAt: Date.now() + expiresIn * 1000,
286
+ };
287
+ }
288
+ function base64UrlEncode(value) {
289
+ return Buffer.from(value, "utf8").toString("base64url");
290
+ }
291
+ function base64UrlDecode(value) {
292
+ return Buffer.from(value, "base64url").toString("utf8");
293
+ }
294
+ export function encodeKiroDeviceAuthKey(credential) {
295
+ return `${KIRO_DEVICE_AUTH_KEY_PREFIX}${base64UrlEncode(JSON.stringify(credential))}`;
296
+ }
297
+ export function decodeKiroDeviceAuthKey(key) {
298
+ try {
299
+ if (!key?.startsWith(KIRO_DEVICE_AUTH_KEY_PREFIX))
300
+ return undefined;
301
+ const parsed = jsonObject(base64UrlDecode(key.slice(KIRO_DEVICE_AUTH_KEY_PREFIX.length)));
302
+ const accessToken = stringField(parsed, "accessToken");
303
+ const refreshToken = stringField(parsed, "refreshToken");
304
+ const expiresAt = numberField(parsed, "expiresAt");
305
+ const clientId = stringField(parsed, "clientId");
306
+ const clientSecret = stringField(parsed, "clientSecret");
307
+ const oidcRegion = stringField(parsed, "oidcRegion");
308
+ const region = stringField(parsed, "region");
309
+ const startUrl = stringField(parsed, "startUrl");
310
+ if (!accessToken || !refreshToken || !expiresAt || !clientId || !clientSecret || !oidcRegion || !region || !startUrl)
311
+ return undefined;
312
+ const profileArn = stringField(parsed, "profileArn");
313
+ return {
314
+ accessToken,
315
+ refreshToken,
316
+ expiresAt,
317
+ clientId,
318
+ clientSecret,
319
+ oidcRegion,
320
+ region,
321
+ startUrl,
322
+ ...(profileArn ? { profileArn } : {}),
323
+ };
324
+ }
325
+ catch {
326
+ return undefined;
327
+ }
328
+ }
329
+ export function isKiroDeviceAuthKey(key) {
330
+ return Boolean(decodeKiroDeviceAuthKey(key));
331
+ }
332
+ export async function credentialFromKiroDeviceAuthKey(key, fetcher = fetch) {
333
+ const cached = refreshedDeviceCredentials.get(key);
334
+ let credential = cached ?? decodeKiroDeviceAuthKey(key);
335
+ if (!credential)
336
+ return undefined;
337
+ if (credential.expiresAt <= Date.now() + TOKEN_REFRESH_BUFFER_MS) {
338
+ let refresh = deviceRefreshes.get(key);
339
+ if (!refresh) {
340
+ refresh = refreshKiroDeviceAuthCredential(credential, fetcher).finally(() => {
341
+ deviceRefreshes.delete(key);
342
+ });
343
+ deviceRefreshes.set(key, refresh);
344
+ }
345
+ credential = await refresh;
346
+ refreshedDeviceCredentials.set(key, credential);
347
+ }
348
+ return {
349
+ accessToken: credential.accessToken,
350
+ refreshToken: credential.refreshToken,
351
+ expiresAt: new Date(credential.expiresAt).toISOString(),
352
+ ...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
353
+ region: regionFromProfileArn(credential.profileArn) ?? credential.region,
354
+ source: "opencode-device-auth",
355
+ };
356
+ }
101
357
  function urls(text) {
102
358
  return text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
103
359
  }
@@ -340,7 +596,7 @@ export async function resolveApiKey(auth, env = process.env) {
340
596
  return env.KIRO_API_KEY;
341
597
  try {
342
598
  const credential = await auth();
343
- return credential.type === "api" && credential.key ? credential.key : "";
599
+ return credential.key || credential.access || "";
344
600
  }
345
601
  catch {
346
602
  return "";
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { AcpConnection, AcpNotificationHandler, AcpStdioClientOptions, Json
3
3
  export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
4
4
  export type { AcpSessionClient, KiroAcpTransportOptions } from "./acp-transport.js";
5
5
  export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
6
- export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
6
+ export { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, decodeKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, extractKiroLoginUrl, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, refreshKiroDeviceAuthCredential, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
7
7
  export { cliChatArgs, KiroCliChatTransport, promptForCli } from "./cli-transport.js";
8
8
  export { createKiroFetch } from "./fetch-adapter.js";
9
9
  export { loadOptions } from "./config.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { KiroPlugin } from "./plugin.js";
2
2
  export { AcpJsonRpcClient, createAcpStdioClient, decodeJsonRpc, encodeJsonRpc } from "./acp-client.js";
3
3
  export { acpPermissionResponse, KiroAcpTransport } from "./acp-transport.js";
4
4
  export { createKiroPlugin, effectiveBackend, KiroPlugin } from "./plugin.js";
5
- export { detectAuth, extractKiroLoginUrl, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
5
+ export { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, decodeKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, extractKiroLoginUrl, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, KIRO_LOGIN_URL, readKiroCliSessionCredential, redacted, regionFromProfileArn, refreshKiroDeviceAuthCredential, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLogin, startKiroCliLoginOnce, } from "./auth.js";
6
6
  export { cliChatArgs, KiroCliChatTransport, promptForCli } from "./cli-transport.js";
7
7
  export { createKiroFetch } from "./fetch-adapter.js";
8
8
  export { loadOptions } from "./config.js";
package/dist/plugin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import { KiroAcpTransport } from "./acp-transport.js";
3
- import { detectAuth, resolveApiKey, runKiroLoginFlowOnce, startKiroCliLoginOnce } from "./auth.js";
3
+ import { authorizeKiroDevice, credentialFromKiroDeviceAuthKey, detectAuth, encodeKiroDeviceAuthKey, isKiroDeviceAuthKey, kiroDeviceVerificationUrl, pollKiroDeviceToken, resolveApiKey, runKiroLoginFlowOnce, } from "./auth.js";
4
4
  import { KiroCliChatTransport } from "./cli-transport.js";
5
5
  import { loadOptions } from "./config.js";
6
6
  import { createKiroFetch } from "./fetch-adapter.js";
@@ -53,6 +53,10 @@ function bearerToken(init) {
53
53
  const match = header?.match(/^Bearer\s+(.+)$/i);
54
54
  return match?.[1] || undefined;
55
55
  }
56
+ function inputString(inputs, key) {
57
+ const value = inputs?.[key];
58
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
59
+ }
56
60
  export function effectiveBackend(options, accessToken) {
57
61
  const apiKey = accessToken || process.env.KIRO_API_KEY;
58
62
  if (options.backend === "acp")
@@ -78,6 +82,12 @@ function localTransport(options, accessToken) {
78
82
  }
79
83
  if (backend === "fetch") {
80
84
  const apiKey = accessToken || process.env.KIRO_API_KEY;
85
+ if (isKiroDeviceAuthKey(apiKey)) {
86
+ return new KiroRestTransport(fetchTransportOptions(options), {
87
+ credentialProvider: () => credentialFromKiroDeviceAuthKey(apiKey).catch(() => undefined),
88
+ login: async () => false,
89
+ });
90
+ }
81
91
  return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey), {
82
92
  login: () => runKiroLoginFlowOnce({ login: options.login }),
83
93
  });
@@ -114,20 +124,25 @@ export function createKiroPlugin() {
114
124
  modelCache.update(Object.keys(options.extraModels).map((id) => ({ id: normalizeModelName(id) })));
115
125
  }
116
126
  let discovery;
117
- let discoveryAttempted = false;
118
- const discoverIfStale = async () => {
119
- if (options.modelDiscovery === "off" ||
120
- options.modelDiscoveryCommand.length === 0 ||
121
- discoveryAttempted) {
122
- return;
127
+ let lastModelDiscoveryAt = 0;
128
+ const refreshModelsIfStale = () => {
129
+ const discoveryIsStale = lastModelDiscoveryAt === 0 || Date.now() - lastModelDiscoveryAt > options.modelCacheTtlSeconds * 1000;
130
+ if (options.modelDiscovery === "off" || options.modelDiscoveryCommand.length === 0 || !discoveryIsStale || discovery) {
131
+ return discovery;
123
132
  }
124
- discoveryAttempted = true;
125
- discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1), { loginOnAuthFailure: true, login: () => runKiroLoginFlowOnce({ login: options.login }) })
133
+ discovery = refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
134
+ .then((models) => {
135
+ if (models.length > 0)
136
+ lastModelDiscoveryAt = Date.now();
137
+ })
126
138
  .catch(() => undefined)
127
139
  .then(() => {
128
140
  discovery = undefined;
129
141
  });
130
- await discovery;
142
+ return discovery;
143
+ };
144
+ const refreshModelsInBackground = () => {
145
+ void refreshModelsIfStale();
131
146
  };
132
147
  const resolver = new ModelResolver({
133
148
  cache: modelCache,
@@ -155,7 +170,7 @@ export function createKiroPlugin() {
155
170
  localServer = undefined;
156
171
  },
157
172
  config: async (config) => {
158
- await discoverIfStale();
173
+ refreshModelsInBackground();
159
174
  config.provider ??= {};
160
175
  config.provider[options.providerID] ??= {};
161
176
  const provider = config.provider[options.providerID];
@@ -176,26 +191,66 @@ export function createKiroPlugin() {
176
191
  methods: [
177
192
  {
178
193
  type: "oauth",
179
- label: "Kiro CLI login",
180
- authorize: async () => {
181
- const session = startKiroCliLoginOnce(options.login);
182
- await session.waitForPrompt();
194
+ label: "Kiro device login",
195
+ prompts: [
196
+ {
197
+ type: "text",
198
+ key: "startUrl",
199
+ message: options.login.identityProvider
200
+ ? `IAM Identity Center Start URL (current: ${options.login.identityProvider}, leave blank to keep)`
201
+ : "IAM Identity Center Start URL (leave blank for AWS Builder ID)",
202
+ placeholder: "https://your-company.awsapps.com/start",
203
+ },
204
+ {
205
+ type: "text",
206
+ key: "idcRegion",
207
+ message: options.login.region && options.login.region !== "us-east-1"
208
+ ? `IAM Identity Center region (current: ${options.login.region}, leave blank to keep)`
209
+ : "IAM Identity Center region (leave blank for us-east-1)",
210
+ placeholder: "us-east-1",
211
+ },
212
+ {
213
+ type: "text",
214
+ key: "profileArn",
215
+ message: options.profileArn
216
+ ? `Profile ARN (current: ${options.profileArn}, leave blank to keep)`
217
+ : "Profile ARN (optional, improves region/profile routing for IAM Identity Center)",
218
+ placeholder: "arn:aws:codewhisperer:us-east-1:123456789012:profile/XXXXXXXXXX",
219
+ },
220
+ ],
221
+ authorize: async (inputs) => {
222
+ const promptInputs = inputs;
223
+ const startUrl = inputString(promptInputs, "startUrl") ?? options.login.identityProvider;
224
+ const idcRegion = inputString(promptInputs, "idcRegion") ?? options.login.region ?? "us-east-1";
225
+ const profileArn = inputString(promptInputs, "profileArn") ?? options.profileArn;
226
+ const authorization = await authorizeKiroDevice({
227
+ region: idcRegion,
228
+ ...(startUrl ? { identityProvider: startUrl } : {}),
229
+ });
230
+ const url = startUrl
231
+ ? kiroDeviceVerificationUrl(authorization.startUrl, authorization.userCode)
232
+ : authorization.verificationUrlComplete;
183
233
  return {
184
- url: session.url,
185
- instructions: session.instructions,
234
+ url,
235
+ instructions: `Open the verification URL and complete Kiro sign-in.\nCode: ${authorization.userCode}`,
186
236
  method: "auto",
187
237
  callback: async () => {
188
- const authenticated = await session.waitForAuth();
189
- if (!authenticated)
190
- return { type: "failed" };
191
- const diagnostics = await detectAuth().catch(() => undefined);
238
+ const credential = await pollKiroDeviceToken(authorization, {
239
+ region: options.region,
240
+ ...(profileArn ? { profileArn } : {}),
241
+ });
242
+ const key = encodeKiroDeviceAuthKey(credential);
192
243
  return {
193
244
  type: "success",
194
- key: "kiro-plugin-local-transport",
245
+ key,
246
+ access: key,
247
+ refresh: credential.refreshToken,
248
+ expires: credential.expiresAt,
195
249
  metadata: {
196
- source: "kiro-cli",
197
- ...(diagnostics?.account ? { account: diagnostics.account } : {}),
198
- ...(diagnostics?.region ? { region: diagnostics.region } : {}),
250
+ source: "kiro-device-auth",
251
+ region: credential.region,
252
+ oidcRegion: credential.oidcRegion,
253
+ ...(credential.profileArn ? { profileArn: credential.profileArn } : {}),
199
254
  },
200
255
  };
201
256
  },
@@ -222,7 +277,6 @@ export function createKiroPlugin() {
222
277
  },
223
278
  ],
224
279
  loader: async (auth) => {
225
- await discoverIfStale();
226
280
  const apiKey = await resolveApiKey(auth);
227
281
  const server = await ensureLocalServer();
228
282
  return {
@@ -236,7 +290,7 @@ export function createKiroPlugin() {
236
290
  description: "Show Kiro plugin backend, auth, region, and discovered model status.",
237
291
  args: {},
238
292
  execute: async () => {
239
- await discoverIfStale();
293
+ refreshModelsInBackground();
240
294
  const auth = await detectAuth();
241
295
  return {
242
296
  title: "Kiro status",
@@ -265,7 +319,7 @@ export function createKiroPlugin() {
265
319
  provider: {
266
320
  id: options.providerID,
267
321
  models: async (provider) => {
268
- await discoverIfStale();
322
+ refreshModelsInBackground();
269
323
  const existing = modelRecord(provider.models);
270
324
  const visibleModels = visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels);
271
325
  const server = await ensureLocalServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",