@zhafron/opencode-kiro-auth 1.1.0 → 1.1.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/dist/kiro/oauth-idc.js +73 -57
- package/dist/plugin/config/schema.d.ts +3 -0
- package/dist/plugin/config/schema.js +4 -2
- package/dist/plugin/logger.d.ts +3 -0
- package/dist/plugin/logger.js +21 -1
- package/dist/plugin/request.js +8 -0
- package/dist/plugin/server.js +7 -1
- package/dist/plugin/storage.js +51 -16
- package/dist/plugin/token.js +50 -40
- package/dist/plugin.js +117 -50
- package/package.json +1 -2
package/dist/kiro/oauth-idc.js
CHANGED
|
@@ -2,64 +2,74 @@ import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS, buildUrl, normalizeRegion } from '..
|
|
|
2
2
|
export async function authorizeKiroIDC(region) {
|
|
3
3
|
const effectiveRegion = normalizeRegion(region);
|
|
4
4
|
const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
|
|
5
|
+
try {
|
|
6
|
+
const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
'User-Agent': KIRO_CONSTANTS.USER_AGENT
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify({
|
|
13
|
+
clientName: 'Kiro IDE',
|
|
14
|
+
clientType: 'public',
|
|
15
|
+
scopes: KIRO_AUTH_SERVICE.SCOPES,
|
|
16
|
+
grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
if (!registerResponse.ok) {
|
|
20
|
+
const errorText = await registerResponse.text().catch(() => '');
|
|
21
|
+
const error = new Error(`Client registration failed: ${registerResponse.status} ${errorText}`);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const registerData = await registerResponse.json();
|
|
25
|
+
const { clientId, clientSecret } = registerData;
|
|
26
|
+
if (!clientId || !clientSecret) {
|
|
27
|
+
const error = new Error('Client registration response missing clientId or clientSecret');
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const deviceAuthResponse = await fetch(`${ssoOIDCEndpoint}/device_authorization`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'User-Agent': KIRO_CONSTANTS.USER_AGENT
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
clientId,
|
|
38
|
+
clientSecret,
|
|
39
|
+
startUrl: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (!deviceAuthResponse.ok) {
|
|
43
|
+
const errorText = await deviceAuthResponse.text().catch(() => '');
|
|
44
|
+
const error = new Error(`Device authorization failed: ${deviceAuthResponse.status} ${errorText}`);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const deviceAuthData = await deviceAuthResponse.json();
|
|
48
|
+
const { verificationUri, verificationUriComplete, userCode, deviceCode, interval = 5, expiresIn = 600 } = deviceAuthData;
|
|
49
|
+
if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
|
|
50
|
+
const error = new Error('Device authorization response missing required fields');
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
verificationUrl: verificationUri,
|
|
55
|
+
verificationUriComplete,
|
|
56
|
+
userCode,
|
|
57
|
+
deviceCode,
|
|
34
58
|
clientId,
|
|
35
59
|
clientSecret,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const errorText = await deviceAuthResponse.text().catch(() => '');
|
|
41
|
-
throw new Error(`Device authorization failed: ${deviceAuthResponse.status} ${errorText}`);
|
|
60
|
+
interval,
|
|
61
|
+
expiresIn,
|
|
62
|
+
region: effectiveRegion
|
|
63
|
+
};
|
|
42
64
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
|
|
46
|
-
throw new Error('Device authorization response missing required fields');
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw error;
|
|
47
67
|
}
|
|
48
|
-
return {
|
|
49
|
-
verificationUrl: verificationUri,
|
|
50
|
-
verificationUriComplete,
|
|
51
|
-
userCode,
|
|
52
|
-
deviceCode,
|
|
53
|
-
clientId,
|
|
54
|
-
clientSecret,
|
|
55
|
-
interval,
|
|
56
|
-
expiresIn,
|
|
57
|
-
region: effectiveRegion
|
|
58
|
-
};
|
|
59
68
|
}
|
|
60
69
|
export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, interval, expiresIn, region) {
|
|
61
70
|
if (!clientId || !clientSecret || !deviceCode) {
|
|
62
|
-
|
|
71
|
+
const error = new Error('Missing required parameters for token polling');
|
|
72
|
+
throw error;
|
|
63
73
|
}
|
|
64
74
|
const effectiveRegion = normalizeRegion(region);
|
|
65
75
|
const ssoOIDCEndpoint = buildUrl(KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT, effectiveRegion);
|
|
@@ -94,12 +104,15 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
|
|
|
94
104
|
continue;
|
|
95
105
|
}
|
|
96
106
|
if (errorType === 'expired_token') {
|
|
97
|
-
|
|
107
|
+
const error = new Error('Device code has expired. Please restart the authorization process.');
|
|
108
|
+
throw error;
|
|
98
109
|
}
|
|
99
110
|
if (errorType === 'access_denied') {
|
|
100
|
-
|
|
111
|
+
const error = new Error('Authorization was denied by the user.');
|
|
112
|
+
throw error;
|
|
101
113
|
}
|
|
102
|
-
|
|
114
|
+
const error = new Error(`Token polling failed: ${errorType} - ${tokenData.error_description || ''}`);
|
|
115
|
+
throw error;
|
|
103
116
|
}
|
|
104
117
|
if (tokenData.accessToken && tokenData.refreshToken) {
|
|
105
118
|
const expiresInSeconds = tokenData.expiresIn || 3600;
|
|
@@ -116,7 +129,8 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
|
|
|
116
129
|
};
|
|
117
130
|
}
|
|
118
131
|
if (!tokenResponse.ok) {
|
|
119
|
-
|
|
132
|
+
const error = new Error(`Token request failed with status: ${tokenResponse.status}`);
|
|
133
|
+
throw error;
|
|
120
134
|
}
|
|
121
135
|
}
|
|
122
136
|
catch (error) {
|
|
@@ -127,9 +141,11 @@ export async function pollKiroIDCToken(clientId, clientSecret, deviceCode, inter
|
|
|
127
141
|
throw error;
|
|
128
142
|
}
|
|
129
143
|
if (attempts >= maxAttempts) {
|
|
130
|
-
|
|
144
|
+
const finalError = new Error(`Token polling failed after ${attempts} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
145
|
+
throw finalError;
|
|
131
146
|
}
|
|
132
147
|
}
|
|
133
148
|
}
|
|
134
|
-
|
|
149
|
+
const timeoutError = new Error('Token polling timed out. Authorization may have expired.');
|
|
150
|
+
throw timeoutError;
|
|
135
151
|
}
|
|
@@ -15,6 +15,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
15
15
|
rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
16
16
|
rate_limit_max_retries: z.ZodDefault<z.ZodNumber>;
|
|
17
17
|
usage_tracking_enabled: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
enable_log_api_request: z.ZodDefault<z.ZodBoolean>;
|
|
18
19
|
}, "strip", z.ZodTypeAny, {
|
|
19
20
|
session_recovery: boolean;
|
|
20
21
|
auto_resume: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
26
27
|
rate_limit_retry_delay_ms: number;
|
|
27
28
|
rate_limit_max_retries: number;
|
|
28
29
|
usage_tracking_enabled: boolean;
|
|
30
|
+
enable_log_api_request: boolean;
|
|
29
31
|
$schema?: string | undefined;
|
|
30
32
|
}, {
|
|
31
33
|
$schema?: string | undefined;
|
|
@@ -39,6 +41,7 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
39
41
|
rate_limit_retry_delay_ms?: number | undefined;
|
|
40
42
|
rate_limit_max_retries?: number | undefined;
|
|
41
43
|
usage_tracking_enabled?: boolean | undefined;
|
|
44
|
+
enable_log_api_request?: boolean | undefined;
|
|
42
45
|
}>;
|
|
43
46
|
export type KiroConfig = z.infer<typeof KiroConfigSchema>;
|
|
44
47
|
export declare const DEFAULT_CONFIG: KiroConfig;
|
|
@@ -12,7 +12,8 @@ export const KiroConfigSchema = z.object({
|
|
|
12
12
|
default_region: RegionSchema.default('us-east-1'),
|
|
13
13
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
|
14
14
|
rate_limit_max_retries: z.number().min(0).max(10).default(3),
|
|
15
|
-
usage_tracking_enabled: z.boolean().default(true)
|
|
15
|
+
usage_tracking_enabled: z.boolean().default(true),
|
|
16
|
+
enable_log_api_request: z.boolean().default(false)
|
|
16
17
|
});
|
|
17
18
|
export const DEFAULT_CONFIG = {
|
|
18
19
|
session_recovery: true,
|
|
@@ -24,5 +25,6 @@ export const DEFAULT_CONFIG = {
|
|
|
24
25
|
default_region: 'us-east-1',
|
|
25
26
|
rate_limit_retry_delay_ms: 5000,
|
|
26
27
|
rate_limit_max_retries: 3,
|
|
27
|
-
usage_tracking_enabled: true
|
|
28
|
+
usage_tracking_enabled: true,
|
|
29
|
+
enable_log_api_request: false
|
|
28
30
|
};
|
package/dist/plugin/logger.d.ts
CHANGED
|
@@ -2,3 +2,6 @@ export declare function log(message: string, ...args: unknown[]): void;
|
|
|
2
2
|
export declare function error(message: string, ...args: unknown[]): void;
|
|
3
3
|
export declare function warn(message: string, ...args: unknown[]): void;
|
|
4
4
|
export declare function debug(message: string, ...args: unknown[]): void;
|
|
5
|
+
export declare function logApiRequest(data: any, timestamp: string): void;
|
|
6
|
+
export declare function logApiResponse(data: any, timestamp: string): void;
|
|
7
|
+
export declare function getTimestamp(): string;
|
package/dist/plugin/logger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
const getLogDir = () => {
|
|
@@ -19,6 +19,17 @@ const writeToFile = (level, message, ...args) => {
|
|
|
19
19
|
}
|
|
20
20
|
catch (e) { }
|
|
21
21
|
};
|
|
22
|
+
const writeApiLog = (type, data, timestamp) => {
|
|
23
|
+
try {
|
|
24
|
+
const dir = getLogDir();
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
const filename = `${timestamp}_${type}.json`;
|
|
27
|
+
const path = join(dir, filename);
|
|
28
|
+
const content = JSON.stringify(data, null, 2);
|
|
29
|
+
writeFileSync(path, content);
|
|
30
|
+
}
|
|
31
|
+
catch (e) { }
|
|
32
|
+
};
|
|
22
33
|
export function log(message, ...args) {
|
|
23
34
|
writeToFile('INFO', message, ...args);
|
|
24
35
|
}
|
|
@@ -33,3 +44,12 @@ export function debug(message, ...args) {
|
|
|
33
44
|
writeToFile('DEBUG', message, ...args);
|
|
34
45
|
}
|
|
35
46
|
}
|
|
47
|
+
export function logApiRequest(data, timestamp) {
|
|
48
|
+
writeApiLog('request', data, timestamp);
|
|
49
|
+
}
|
|
50
|
+
export function logApiResponse(data, timestamp) {
|
|
51
|
+
writeApiLog('response', data, timestamp);
|
|
52
|
+
}
|
|
53
|
+
export function getTimestamp() {
|
|
54
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
55
|
+
}
|
package/dist/plugin/request.js
CHANGED
|
@@ -81,6 +81,10 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
|
|
|
81
81
|
uim.images = imgs;
|
|
82
82
|
if (trs.length)
|
|
83
83
|
uim.userInputMessageContext = { toolResults: deduplicateToolResults(trs) };
|
|
84
|
+
const prev = history[history.length - 1];
|
|
85
|
+
if (prev && prev.userInputMessage) {
|
|
86
|
+
history.push({ assistantResponseMessage: { content: 'Continue' } });
|
|
87
|
+
}
|
|
84
88
|
history.push({ userInputMessage: uim });
|
|
85
89
|
}
|
|
86
90
|
else if (m.role === 'tool') {
|
|
@@ -100,6 +104,10 @@ export function transformToCodeWhisperer(url, body, model, auth, think = false,
|
|
|
100
104
|
toolUseId: m.tool_call_id
|
|
101
105
|
});
|
|
102
106
|
}
|
|
107
|
+
const prev = history[history.length - 1];
|
|
108
|
+
if (prev && prev.userInputMessage) {
|
|
109
|
+
history.push({ assistantResponseMessage: { content: 'Continue' } });
|
|
110
|
+
}
|
|
103
111
|
history.push({
|
|
104
112
|
userInputMessage: {
|
|
105
113
|
content: 'Tool results provided.',
|
package/dist/plugin/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { getIDCAuthHtml, getSuccessHtml, getErrorHtml } from './auth-page';
|
|
3
|
+
import * as logger from './logger';
|
|
3
4
|
export function startIDCAuthServer(authData, port = 19847) {
|
|
4
5
|
return new Promise((resolve, reject) => {
|
|
5
6
|
let server = null;
|
|
@@ -50,11 +51,13 @@ export function startIDCAuthServer(authData, port = 19847) {
|
|
|
50
51
|
});
|
|
51
52
|
setTimeout(cleanup, 2000);
|
|
52
53
|
}
|
|
53
|
-
else if (d.error === 'authorization_pending')
|
|
54
|
+
else if (d.error === 'authorization_pending') {
|
|
54
55
|
setTimeout(poll, authData.interval * 1000);
|
|
56
|
+
}
|
|
55
57
|
else {
|
|
56
58
|
status.status = 'failed';
|
|
57
59
|
status.error = d.error_description || d.error;
|
|
60
|
+
logger.error(`Auth polling failed: ${status.error}`);
|
|
58
61
|
if (rejector)
|
|
59
62
|
rejector(new Error(status.error));
|
|
60
63
|
setTimeout(cleanup, 2000);
|
|
@@ -63,6 +66,7 @@ export function startIDCAuthServer(authData, port = 19847) {
|
|
|
63
66
|
catch (e) {
|
|
64
67
|
status.status = 'failed';
|
|
65
68
|
status.error = e.message;
|
|
69
|
+
logger.error(`Auth polling error: ${e.message}`, e);
|
|
66
70
|
if (rejector)
|
|
67
71
|
rejector(e);
|
|
68
72
|
setTimeout(cleanup, 2000);
|
|
@@ -86,12 +90,14 @@ export function startIDCAuthServer(authData, port = 19847) {
|
|
|
86
90
|
}
|
|
87
91
|
});
|
|
88
92
|
server.on('error', (e) => {
|
|
93
|
+
logger.error(`Auth server error on port ${port}`, e);
|
|
89
94
|
cleanup();
|
|
90
95
|
reject(e);
|
|
91
96
|
});
|
|
92
97
|
server.listen(port, '127.0.0.1', () => {
|
|
93
98
|
timeoutId = setTimeout(() => {
|
|
94
99
|
status.status = 'timeout';
|
|
100
|
+
logger.warn('Auth timeout waiting for authorization');
|
|
95
101
|
if (rejector)
|
|
96
102
|
rejector(new Error('Timeout'));
|
|
97
103
|
cleanup();
|
package/dist/plugin/storage.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import lockfile from 'proper-lockfile';
|
|
5
|
-
import { xdgConfig } from 'xdg-basedir';
|
|
6
6
|
import * as logger from './logger';
|
|
7
7
|
const LOCK_OPTIONS = {
|
|
8
8
|
stale: 10000,
|
|
9
9
|
retries: { retries: 5, minTimeout: 100, maxTimeout: 1000, factor: 2 }
|
|
10
10
|
};
|
|
11
11
|
function getBaseDir() {
|
|
12
|
-
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
if (platform === 'win32') {
|
|
14
|
+
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
|
|
15
|
+
}
|
|
16
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
17
|
+
return join(xdgConfig, 'opencode');
|
|
13
18
|
}
|
|
14
19
|
export function getStoragePath() {
|
|
15
20
|
return join(getBaseDir(), 'kiro-accounts.json');
|
|
@@ -18,12 +23,24 @@ export function getUsagePath() {
|
|
|
18
23
|
return join(getBaseDir(), 'kiro-usage.json');
|
|
19
24
|
}
|
|
20
25
|
async function withLock(path, fn) {
|
|
21
|
-
|
|
26
|
+
try {
|
|
27
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.error(`Failed to create directory ${dirname(path)}`, error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
22
33
|
try {
|
|
23
34
|
await fs.access(path);
|
|
24
35
|
}
|
|
25
36
|
catch {
|
|
26
|
-
|
|
37
|
+
try {
|
|
38
|
+
await fs.writeFile(path, '{}');
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error(`Failed to initialize file ${path}`, error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
45
|
let release = null;
|
|
29
46
|
try {
|
|
@@ -35,8 +52,14 @@ async function withLock(path, fn) {
|
|
|
35
52
|
throw error;
|
|
36
53
|
}
|
|
37
54
|
finally {
|
|
38
|
-
if (release)
|
|
39
|
-
|
|
55
|
+
if (release) {
|
|
56
|
+
try {
|
|
57
|
+
await release();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger.warn(`Failed to release lock for ${path}`, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
40
63
|
}
|
|
41
64
|
}
|
|
42
65
|
export async function loadAccounts() {
|
|
@@ -50,11 +73,17 @@ export async function loadAccounts() {
|
|
|
50
73
|
}
|
|
51
74
|
export async function saveAccounts(storage) {
|
|
52
75
|
const path = getStoragePath();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
try {
|
|
77
|
+
await withLock(path, async () => {
|
|
78
|
+
const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
|
|
79
|
+
await fs.writeFile(tmp, JSON.stringify(storage, null, 2));
|
|
80
|
+
await fs.rename(tmp, path);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error(`Failed to save accounts to ${path}`, error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
58
87
|
}
|
|
59
88
|
export async function loadUsage() {
|
|
60
89
|
try {
|
|
@@ -67,9 +96,15 @@ export async function loadUsage() {
|
|
|
67
96
|
}
|
|
68
97
|
export async function saveUsage(storage) {
|
|
69
98
|
const path = getUsagePath();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
try {
|
|
100
|
+
await withLock(path, async () => {
|
|
101
|
+
const tmp = `${path}.${randomBytes(6).toString('hex')}.tmp`;
|
|
102
|
+
await fs.writeFile(tmp, JSON.stringify(storage, null, 2));
|
|
103
|
+
await fs.rename(tmp, path);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.error(`Failed to save usage to ${path}`, error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
75
110
|
}
|
package/dist/plugin/token.js
CHANGED
|
@@ -3,54 +3,64 @@ import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth';
|
|
|
3
3
|
export async function refreshAccessToken(auth) {
|
|
4
4
|
const url = `https://oidc.${auth.region}.amazonaws.com/token`;
|
|
5
5
|
const p = decodeRefreshToken(auth.refresh);
|
|
6
|
-
if (!p.clientId || !p.clientSecret)
|
|
6
|
+
if (!p.clientId || !p.clientSecret) {
|
|
7
7
|
throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
|
|
8
|
+
}
|
|
8
9
|
const requestBody = {
|
|
9
10
|
refreshToken: p.refreshToken,
|
|
10
11
|
clientId: p.clientId,
|
|
11
12
|
clientSecret: p.clientSecret,
|
|
12
13
|
grantType: 'refresh_token'
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(url, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
Accept: 'application/json',
|
|
21
|
+
'amz-sdk-request': 'attempt=1; max=1',
|
|
22
|
+
'x-amzn-kiro-agent-mode': 'vibe',
|
|
23
|
+
Connection: 'close'
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(requestBody)
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const txt = await res.text();
|
|
29
|
+
let data = {};
|
|
30
|
+
try {
|
|
31
|
+
data = JSON.parse(txt);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
data = { message: txt };
|
|
35
|
+
}
|
|
36
|
+
throw new KiroTokenRefreshError(`Refresh failed: ${data.message || data.error_description || txt}`, data.error || `HTTP_${res.status}`);
|
|
30
37
|
}
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const d = await res.json();
|
|
39
|
+
const acc = d.access_token || d.accessToken;
|
|
40
|
+
if (!acc) {
|
|
41
|
+
throw new KiroTokenRefreshError('No access token', 'INVALID_RESPONSE');
|
|
33
42
|
}
|
|
34
|
-
|
|
43
|
+
const upP = {
|
|
44
|
+
refreshToken: d.refresh_token || d.refreshToken || p.refreshToken,
|
|
45
|
+
clientId: p.clientId,
|
|
46
|
+
clientSecret: p.clientSecret,
|
|
47
|
+
authMethod: 'idc'
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
refresh: encodeRefreshToken(upP),
|
|
51
|
+
access: acc,
|
|
52
|
+
expires: Date.now() + (d.expires_in || 3600) * 1000,
|
|
53
|
+
authMethod: 'idc',
|
|
54
|
+
region: auth.region,
|
|
55
|
+
clientId: auth.clientId,
|
|
56
|
+
clientSecret: auth.clientSecret,
|
|
57
|
+
email: auth.email
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof KiroTokenRefreshError) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
throw new KiroTokenRefreshError(`Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', error instanceof Error ? error : undefined);
|
|
35
65
|
}
|
|
36
|
-
const d = await res.json();
|
|
37
|
-
const acc = d.access_token || d.accessToken;
|
|
38
|
-
if (!acc)
|
|
39
|
-
throw new KiroTokenRefreshError('No access token', 'INVALID_RESPONSE');
|
|
40
|
-
const upP = {
|
|
41
|
-
refreshToken: d.refresh_token || d.refreshToken || p.refreshToken,
|
|
42
|
-
clientId: p.clientId,
|
|
43
|
-
clientSecret: p.clientSecret,
|
|
44
|
-
authMethod: 'idc'
|
|
45
|
-
};
|
|
46
|
-
return {
|
|
47
|
-
refresh: encodeRefreshToken(upP),
|
|
48
|
-
access: acc,
|
|
49
|
-
expires: Date.now() + (d.expires_in || 3600) * 1000,
|
|
50
|
-
authMethod: 'idc',
|
|
51
|
-
region: auth.region,
|
|
52
|
-
clientId: auth.clientId,
|
|
53
|
-
clientSecret: auth.clientSecret,
|
|
54
|
-
email: auth.email
|
|
55
|
-
};
|
|
56
66
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -72,8 +72,41 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
const prep = transformToCodeWhisperer(url, init?.body, model, auth, think, budget);
|
|
75
|
+
const apiTimestamp = config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
76
|
+
if (config.enable_log_api_request && apiTimestamp) {
|
|
77
|
+
let parsedBody = null;
|
|
78
|
+
if (prep.init.body && typeof prep.init.body === 'string') {
|
|
79
|
+
try {
|
|
80
|
+
parsedBody = JSON.parse(prep.init.body);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
parsedBody = prep.init.body;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
logger.logApiRequest({
|
|
87
|
+
url: prep.url,
|
|
88
|
+
method: prep.init.method,
|
|
89
|
+
headers: prep.init.headers,
|
|
90
|
+
body: parsedBody,
|
|
91
|
+
conversationId: prep.conversationId,
|
|
92
|
+
model: prep.effectiveModel
|
|
93
|
+
}, apiTimestamp);
|
|
94
|
+
}
|
|
75
95
|
try {
|
|
76
96
|
const res = await fetch(prep.url, prep.init);
|
|
97
|
+
if (config.enable_log_api_request && apiTimestamp) {
|
|
98
|
+
const responseHeaders = {};
|
|
99
|
+
res.headers.forEach((value, key) => {
|
|
100
|
+
responseHeaders[key] = value;
|
|
101
|
+
});
|
|
102
|
+
logger.logApiResponse({
|
|
103
|
+
status: res.status,
|
|
104
|
+
statusText: res.statusText,
|
|
105
|
+
headers: responseHeaders,
|
|
106
|
+
conversationId: prep.conversationId,
|
|
107
|
+
model: prep.effectiveModel
|
|
108
|
+
}, apiTimestamp);
|
|
109
|
+
}
|
|
77
110
|
if (res.ok) {
|
|
78
111
|
if (config.usage_tracking_enabled)
|
|
79
112
|
fetchUsageLimits(auth)
|
|
@@ -155,9 +188,30 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
155
188
|
await am.saveToDisk();
|
|
156
189
|
continue;
|
|
157
190
|
}
|
|
191
|
+
if (config.enable_log_api_request && apiTimestamp) {
|
|
192
|
+
const responseHeaders = {};
|
|
193
|
+
res.headers.forEach((value, key) => {
|
|
194
|
+
responseHeaders[key] = value;
|
|
195
|
+
});
|
|
196
|
+
logger.logApiResponse({
|
|
197
|
+
status: res.status,
|
|
198
|
+
statusText: res.statusText,
|
|
199
|
+
headers: responseHeaders,
|
|
200
|
+
error: `Kiro Error: ${res.status}`,
|
|
201
|
+
conversationId: prep.conversationId,
|
|
202
|
+
model: prep.effectiveModel
|
|
203
|
+
}, apiTimestamp);
|
|
204
|
+
}
|
|
158
205
|
throw new Error(`Kiro Error: ${res.status}`);
|
|
159
206
|
}
|
|
160
207
|
catch (e) {
|
|
208
|
+
if (config.enable_log_api_request && apiTimestamp) {
|
|
209
|
+
logger.logApiResponse({
|
|
210
|
+
error: String(e),
|
|
211
|
+
conversationId: prep.conversationId,
|
|
212
|
+
model: prep.effectiveModel
|
|
213
|
+
}, apiTimestamp);
|
|
214
|
+
}
|
|
161
215
|
if (isNetworkError(e) && retry < config.rate_limit_max_retries) {
|
|
162
216
|
const delay = 5000 * Math.pow(2, retry);
|
|
163
217
|
showToast(`Network error. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
|
|
@@ -178,65 +232,78 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
178
232
|
type: 'oauth',
|
|
179
233
|
authorize: async () => new Promise(async (resolve) => {
|
|
180
234
|
const region = config.default_region;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const res = await waitForAuth();
|
|
190
|
-
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
191
|
-
const acc = {
|
|
192
|
-
id: generateAccountId(),
|
|
193
|
-
email: res.email,
|
|
194
|
-
authMethod: 'idc',
|
|
195
|
-
region,
|
|
196
|
-
clientId: res.clientId,
|
|
197
|
-
clientSecret: res.clientSecret,
|
|
198
|
-
refreshToken: res.refreshToken,
|
|
199
|
-
accessToken: res.accessToken,
|
|
200
|
-
expiresAt: res.expiresAt,
|
|
201
|
-
rateLimitResetTime: 0,
|
|
202
|
-
isHealthy: true
|
|
203
|
-
};
|
|
235
|
+
try {
|
|
236
|
+
const authData = await authorizeKiroIDC(region);
|
|
237
|
+
const { url, waitForAuth } = await startIDCAuthServer(authData);
|
|
238
|
+
resolve({
|
|
239
|
+
url,
|
|
240
|
+
instructions: 'Opening browser...',
|
|
241
|
+
method: 'auto',
|
|
242
|
+
callback: async () => {
|
|
204
243
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
authMethod: 'idc'
|
|
211
|
-
}),
|
|
212
|
-
access: res.accessToken,
|
|
213
|
-
expires: res.expiresAt,
|
|
244
|
+
const res = await waitForAuth();
|
|
245
|
+
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
246
|
+
const acc = {
|
|
247
|
+
id: generateAccountId(),
|
|
248
|
+
email: res.email,
|
|
214
249
|
authMethod: 'idc',
|
|
215
250
|
region,
|
|
216
251
|
clientId: res.clientId,
|
|
217
252
|
clientSecret: res.clientSecret,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
refreshToken: res.refreshToken,
|
|
254
|
+
accessToken: res.accessToken,
|
|
255
|
+
expiresAt: res.expiresAt,
|
|
256
|
+
rateLimitResetTime: 0,
|
|
257
|
+
isHealthy: true
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
const u = await fetchUsageLimits({
|
|
261
|
+
refresh: encodeRefreshToken({
|
|
262
|
+
refreshToken: res.refreshToken,
|
|
263
|
+
clientId: res.clientId,
|
|
264
|
+
clientSecret: res.clientSecret,
|
|
265
|
+
authMethod: 'idc'
|
|
266
|
+
}),
|
|
267
|
+
access: res.accessToken,
|
|
268
|
+
expires: res.expiresAt,
|
|
269
|
+
authMethod: 'idc',
|
|
270
|
+
region,
|
|
271
|
+
clientId: res.clientId,
|
|
272
|
+
clientSecret: res.clientSecret,
|
|
273
|
+
email: res.email
|
|
274
|
+
});
|
|
275
|
+
am.updateUsage(acc.id, {
|
|
276
|
+
usedCount: u.usedCount,
|
|
277
|
+
limitCount: u.limitCount,
|
|
278
|
+
realEmail: u.email
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
logger.warn(`Initial usage fetch failed: ${e.message}`, e);
|
|
283
|
+
}
|
|
284
|
+
am.addAccount(acc);
|
|
285
|
+
await am.saveToDisk();
|
|
286
|
+
showToast(`Successfully logged in as ${res.email}`, 'success');
|
|
287
|
+
return { type: 'success', key: res.accessToken };
|
|
225
288
|
}
|
|
226
289
|
catch (e) {
|
|
227
|
-
logger.
|
|
290
|
+
logger.error(`Login failed: ${e.message}`, e);
|
|
291
|
+
showToast(`Login failed: ${e.message}`, 'error');
|
|
292
|
+
return { type: 'failed' };
|
|
228
293
|
}
|
|
229
|
-
am.addAccount(acc);
|
|
230
|
-
await am.saveToDisk();
|
|
231
|
-
showToast(`Successfully logged in as ${res.email}`, 'success');
|
|
232
|
-
return { type: 'success', key: res.accessToken };
|
|
233
|
-
}
|
|
234
|
-
catch (e) {
|
|
235
|
-
logger.error(`Login failed: ${e.message}`);
|
|
236
|
-
return { type: 'failed' };
|
|
237
294
|
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
logger.error(`Authorization failed: ${e.message}`, e);
|
|
299
|
+
showToast(`Authorization failed: ${e.message}`, 'error');
|
|
300
|
+
resolve({
|
|
301
|
+
url: '',
|
|
302
|
+
instructions: 'Authorization failed',
|
|
303
|
+
method: 'auto',
|
|
304
|
+
callback: async () => ({ type: 'failed' })
|
|
305
|
+
});
|
|
306
|
+
}
|
|
240
307
|
})
|
|
241
308
|
}
|
|
242
309
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhafron/opencode-kiro-auth",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@opencode-ai/plugin": "^0.15.30",
|
|
34
34
|
"proper-lockfile": "^4.1.2",
|
|
35
|
-
"xdg-basedir": "^5.1.0",
|
|
36
35
|
"zod": "^3.24.0"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|