@zhafron/opencode-kiro-auth 1.2.0 → 1.2.2

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
@@ -1,4 +1,7 @@
1
1
  # OpenCode Kiro Auth Plugin
2
+ [![npm version](https://img.shields.io/npm/v/@zhafron/opencode-kiro-auth)](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth)
3
+ [![npm downloads](https://img.shields.io/npm/dm/@zhafron/opencode-kiro-auth)](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth)
4
+ [![license](https://img.shields.io/npm/l/@zhafron/opencode-kiro-auth)](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth)
2
5
 
3
6
  OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to the latest Claude 3.5/4.5 models with substantial trial quotas.
4
7
 
@@ -4,4 +4,5 @@ export declare function warn(message: string, ...args: unknown[]): void;
4
4
  export declare function debug(message: string, ...args: unknown[]): void;
5
5
  export declare function logApiRequest(data: any, timestamp: string): void;
6
6
  export declare function logApiResponse(data: any, timestamp: string): void;
7
+ export declare function logApiError(requestData: any, responseData: any, timestamp: string): void;
7
8
  export declare function getTimestamp(): string;
@@ -19,11 +19,12 @@ const writeToFile = (level, message, ...args) => {
19
19
  }
20
20
  catch (e) { }
21
21
  };
22
- const writeApiLog = (type, data, timestamp) => {
22
+ const writeApiLog = (type, data, timestamp, isError = false) => {
23
23
  try {
24
24
  const dir = getLogDir();
25
25
  mkdirSync(dir, { recursive: true });
26
- const filename = `${timestamp}_${type}.json`;
26
+ const prefix = isError ? 'error_' : '';
27
+ const filename = `${prefix}${timestamp}_${type}.json`;
27
28
  const path = join(dir, filename);
28
29
  const content = JSON.stringify(data, null, 2);
29
30
  writeFileSync(path, content);
@@ -50,6 +51,13 @@ export function logApiRequest(data, timestamp) {
50
51
  export function logApiResponse(data, timestamp) {
51
52
  writeApiLog('response', data, timestamp);
52
53
  }
54
+ export function logApiError(requestData, responseData, timestamp) {
55
+ writeApiLog('request', requestData, timestamp, true);
56
+ writeApiLog('response', responseData, timestamp, true);
57
+ const errorType = responseData.status ? `HTTP ${responseData.status}` : 'Network Error';
58
+ const email = requestData.email || 'unknown';
59
+ error(`${errorType} on ${email} - See error_${timestamp}_request.json`);
60
+ }
53
61
  export function getTimestamp() {
54
62
  return new Date().toISOString().replace(/[:.]/g, '-');
55
63
  }
@@ -50,25 +50,46 @@ export async function startIDCAuthServer(authData, startPort = 19847, portRange
50
50
  };
51
51
  const poll = async () => {
52
52
  try {
53
- const body = new URLSearchParams({
54
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
55
- device_code: authData.deviceCode,
56
- client_id: authData.clientId,
57
- client_secret: authData.clientSecret
58
- });
53
+ const body = {
54
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code',
55
+ deviceCode: authData.deviceCode,
56
+ clientId: authData.clientId,
57
+ clientSecret: authData.clientSecret
58
+ };
59
59
  const res = await fetch(`https://oidc.${authData.region}.amazonaws.com/token`, {
60
60
  method: 'POST',
61
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
62
- body: body.toString()
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify(body)
63
63
  });
64
- const d = await res.json();
64
+ const responseText = await res.text();
65
+ let d = {};
66
+ if (responseText) {
67
+ try {
68
+ d = JSON.parse(responseText);
69
+ }
70
+ catch (parseError) {
71
+ logger.error(`Auth polling error: Failed to parse JSON (status ${res.status})`, parseError);
72
+ throw parseError;
73
+ }
74
+ }
65
75
  if (res.ok) {
66
- const acc = d.access_token, ref = d.refresh_token, exp = Date.now() + d.expires_in * 1000;
67
- const infoRes = await fetch('https://view.awsapps.com/api/user/info', {
68
- headers: { Authorization: `Bearer ${acc}` }
69
- });
70
- const info = await infoRes.json();
71
- const email = info.email || info.userName || 'builder-id@aws.amazon.com';
76
+ const acc = d.access_token || d.accessToken, ref = d.refresh_token || d.refreshToken, exp = Date.now() + (d.expires_in || d.expiresIn || 0) * 1000;
77
+ let email = 'builder-id@aws.amazon.com';
78
+ try {
79
+ const infoRes = await fetch('https://view.awsapps.com/api/user/info', {
80
+ headers: { Authorization: `Bearer ${acc}` }
81
+ });
82
+ if (infoRes.ok) {
83
+ const info = await infoRes.json();
84
+ email = info.email || info.userName || email;
85
+ }
86
+ else {
87
+ logger.warn(`User info request failed with status ${infoRes.status}; using fallback email`);
88
+ }
89
+ }
90
+ catch (infoError) {
91
+ logger.warn(`Failed to fetch user info; using fallback email: ${infoError?.message || infoError}`);
92
+ }
72
93
  status.status = 'success';
73
94
  if (resolver)
74
95
  resolver({
@@ -87,7 +108,7 @@ export async function startIDCAuthServer(authData, startPort = 19847, portRange
87
108
  else {
88
109
  status.status = 'failed';
89
110
  status.error = d.error_description || d.error;
90
- logger.error(`Auth polling failed: ${status.error}`);
111
+ logger.error(`Auth polling failed a: ${status.error}`);
91
112
  if (rejector)
92
113
  rejector(new Error(status.error));
93
114
  setTimeout(cleanup, 2000);
@@ -96,7 +117,7 @@ export async function startIDCAuthServer(authData, startPort = 19847, portRange
96
117
  catch (e) {
97
118
  status.status = 'failed';
98
119
  status.error = e.message;
99
- logger.error(`Auth polling error: ${e.message}`, e);
120
+ logger.error(`Auth polling error b: ${e.message}`, e);
100
121
  if (rejector)
101
122
  rejector(e);
102
123
  setTimeout(cleanup, 2000);
@@ -67,7 +67,11 @@ export async function loadAccounts() {
67
67
  return withLock(path, async () => {
68
68
  try {
69
69
  const content = await fs.readFile(path, 'utf-8');
70
- return JSON.parse(content);
70
+ const parsed = JSON.parse(content);
71
+ if (!parsed || !Array.isArray(parsed.accounts)) {
72
+ return { version: 1, accounts: [], activeIndex: -1 };
73
+ }
74
+ return parsed;
71
75
  }
72
76
  catch {
73
77
  return { version: 1, accounts: [], activeIndex: -1 };
@@ -93,7 +97,11 @@ export async function loadUsage() {
93
97
  return withLock(path, async () => {
94
98
  try {
95
99
  const content = await fs.readFile(path, 'utf-8');
96
- return JSON.parse(content);
100
+ const parsed = JSON.parse(content);
101
+ if (!parsed || typeof parsed.usage !== 'object' || parsed.usage === null) {
102
+ return { version: 1, usage: {} };
103
+ }
104
+ return parsed;
97
105
  }
98
106
  catch {
99
107
  return { version: 1, usage: {} };
package/dist/plugin.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { loadConfig } from './plugin/config';
2
+ import { exec } from 'node:child_process';
2
3
  import { AccountManager, generateAccountId } from './plugin/accounts';
3
4
  import { accessTokenExpired, encodeRefreshToken } from './kiro/auth';
4
5
  import { refreshAccessToken } from './plugin/token';
@@ -23,6 +24,19 @@ const formatUsageMessage = (usedCount, limitCount, email) => {
23
24
  }
24
25
  return `Usage (${email}): ${usedCount}`;
25
26
  };
27
+ const openBrowser = (url) => {
28
+ const escapedUrl = url.replace(/"/g, '\\"');
29
+ const platform = process.platform;
30
+ const command = platform === 'win32'
31
+ ? `cmd /c start "" "${escapedUrl}"`
32
+ : platform === 'darwin'
33
+ ? `open "${escapedUrl}"`
34
+ : `xdg-open "${escapedUrl}"`;
35
+ exec(command, (error) => {
36
+ if (error)
37
+ logger.warn(`Failed to open browser automatically: ${error.message}`, error);
38
+ });
39
+ };
26
40
  export const createKiroPlugin = (id) => async ({ client, directory }) => {
27
41
  const config = loadConfig(directory);
28
42
  const showToast = (message, variant) => {
@@ -99,24 +113,26 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
99
113
  }
100
114
  const prep = transformToCodeWhisperer(url, init?.body, model, auth, think, budget);
101
115
  const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
102
- if (config.enable_log_api_request && apiTimestamp) {
103
- let parsedBody = null;
104
- if (prep.init.body && typeof prep.init.body === 'string') {
105
- try {
106
- parsedBody = JSON.parse(prep.init.body);
107
- }
108
- catch (e) {
109
- parsedBody = prep.init.body;
110
- }
116
+ let parsedBody = null;
117
+ if (prep.init.body && typeof prep.init.body === 'string') {
118
+ try {
119
+ parsedBody = JSON.parse(prep.init.body);
111
120
  }
112
- logger.logApiRequest({
113
- url: prep.url,
114
- method: prep.init.method,
115
- headers: prep.init.headers,
116
- body: parsedBody,
117
- conversationId: prep.conversationId,
118
- model: prep.effectiveModel
119
- }, apiTimestamp);
121
+ catch (e) {
122
+ parsedBody = prep.init.body;
123
+ }
124
+ }
125
+ const requestData = {
126
+ url: prep.url,
127
+ method: prep.init.method,
128
+ headers: prep.init.headers,
129
+ body: parsedBody,
130
+ conversationId: prep.conversationId,
131
+ model: prep.effectiveModel,
132
+ email: acc.realEmail || acc.email
133
+ };
134
+ if (config.enable_log_api_request && apiTimestamp) {
135
+ logger.logApiRequest(requestData, apiTimestamp);
120
136
  }
121
137
  try {
122
138
  const res = await fetch(prep.url, prep.init);
@@ -201,7 +217,6 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
201
217
  });
202
218
  }
203
219
  if (res.status === 401 && retry < config.rate_limit_max_retries) {
204
- logger.warn(`Unauthorized (401) on ${acc.realEmail || acc.email}, retrying...`);
205
220
  retry++;
206
221
  continue;
207
222
  }
@@ -225,30 +240,28 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
225
240
  await am.saveToDisk();
226
241
  continue;
227
242
  }
243
+ const responseHeaders = {};
244
+ res.headers.forEach((value, key) => {
245
+ responseHeaders[key] = value;
246
+ });
247
+ const responseData = {
248
+ status: res.status,
249
+ statusText: res.statusText,
250
+ headers: responseHeaders,
251
+ error: `Kiro Error: ${res.status}`,
252
+ conversationId: prep.conversationId,
253
+ model: prep.effectiveModel
254
+ };
228
255
  if (config.enable_log_api_request && apiTimestamp) {
229
- const responseHeaders = {};
230
- res.headers.forEach((value, key) => {
231
- responseHeaders[key] = value;
232
- });
233
- logger.logApiResponse({
234
- status: res.status,
235
- statusText: res.statusText,
236
- headers: responseHeaders,
237
- error: `Kiro Error: ${res.status}`,
238
- conversationId: prep.conversationId,
239
- model: prep.effectiveModel
240
- }, apiTimestamp);
256
+ logger.logApiResponse(responseData, apiTimestamp);
257
+ }
258
+ else {
259
+ const errorTimestamp = logger.getTimestamp();
260
+ logger.logApiError(requestData, responseData, errorTimestamp);
241
261
  }
242
262
  throw new Error(`Kiro Error: ${res.status}`);
243
263
  }
244
264
  catch (e) {
245
- if (config.enable_log_api_request && apiTimestamp) {
246
- logger.logApiResponse({
247
- error: String(e),
248
- conversationId: prep.conversationId,
249
- model: prep.effectiveModel
250
- }, apiTimestamp);
251
- }
252
265
  if (isNetworkError(e) && retry < config.rate_limit_max_retries) {
253
266
  const delay = 5000 * Math.pow(2, retry);
254
267
  showToast(`Network error. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
@@ -256,6 +269,18 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
256
269
  retry++;
257
270
  continue;
258
271
  }
272
+ const networkErrorData = {
273
+ error: String(e),
274
+ conversationId: prep.conversationId,
275
+ model: prep.effectiveModel
276
+ };
277
+ if (config.enable_log_api_request && apiTimestamp) {
278
+ logger.logApiResponse(networkErrorData, apiTimestamp);
279
+ }
280
+ else {
281
+ const errorTimestamp = logger.getTimestamp();
282
+ logger.logApiError(requestData, networkErrorData, errorTimestamp);
283
+ }
259
284
  throw e;
260
285
  }
261
286
  }
@@ -272,9 +297,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
272
297
  try {
273
298
  const authData = await authorizeKiroIDC(region);
274
299
  const { url, waitForAuth } = await startIDCAuthServer(authData, config.auth_server_port_start, config.auth_server_port_range);
300
+ openBrowser(url);
275
301
  resolve({
276
302
  url,
277
- instructions: 'Opening browser...',
303
+ instructions: `Open this URL to continue: ${url}`,
278
304
  method: 'auto',
279
305
  callback: async () => {
280
306
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhafron/opencode-kiro-auth",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",