@zhafron/opencode-kiro-auth 1.0.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 +85 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/kiro/auth.d.ts +5 -0
- package/dist/kiro/auth.js +24 -0
- package/dist/kiro/oauth-idc.d.ts +24 -0
- package/dist/kiro/oauth-idc.js +132 -0
- package/dist/kiro/oauth-social.d.ts +17 -0
- package/dist/kiro/oauth-social.js +51 -0
- package/dist/plugin/accounts.d.ts +28 -0
- package/dist/plugin/accounts.js +156 -0
- package/dist/plugin/auth-page.d.ts +3 -0
- package/dist/plugin/auth-page.js +568 -0
- package/dist/plugin/cli.d.ts +6 -0
- package/dist/plugin/cli.js +98 -0
- package/dist/plugin/config/index.d.ts +3 -0
- package/dist/plugin/config/index.js +2 -0
- package/dist/plugin/config/loader.d.ts +6 -0
- package/dist/plugin/config/loader.js +125 -0
- package/dist/plugin/config/schema.d.ts +44 -0
- package/dist/plugin/config/schema.js +28 -0
- package/dist/plugin/debug.d.ts +2 -0
- package/dist/plugin/debug.js +9 -0
- package/dist/plugin/errors.d.ts +17 -0
- package/dist/plugin/errors.js +34 -0
- package/dist/plugin/logger.d.ts +4 -0
- package/dist/plugin/logger.js +37 -0
- package/dist/plugin/models.d.ts +3 -0
- package/dist/plugin/models.js +14 -0
- package/dist/plugin/oauth-parser.d.ts +5 -0
- package/dist/plugin/oauth-parser.js +23 -0
- package/dist/plugin/quota.d.ts +15 -0
- package/dist/plugin/quota.js +68 -0
- package/dist/plugin/recovery.d.ts +19 -0
- package/dist/plugin/recovery.js +302 -0
- package/dist/plugin/refresh-queue.d.ts +14 -0
- package/dist/plugin/refresh-queue.js +69 -0
- package/dist/plugin/request.d.ts +4 -0
- package/dist/plugin/request.js +240 -0
- package/dist/plugin/response.d.ts +6 -0
- package/dist/plugin/response.js +246 -0
- package/dist/plugin/server.d.ts +24 -0
- package/dist/plugin/server.js +96 -0
- package/dist/plugin/storage.d.ts +7 -0
- package/dist/plugin/storage.js +75 -0
- package/dist/plugin/streaming.d.ts +3 -0
- package/dist/plugin/streaming.js +503 -0
- package/dist/plugin/token.d.ts +2 -0
- package/dist/plugin/token.js +56 -0
- package/dist/plugin/types.d.ts +148 -0
- package/dist/plugin/types.js +0 -0
- package/dist/plugin/usage.d.ts +3 -0
- package/dist/plugin/usage.js +36 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.js +222 -0
- package/dist/src/constants.d.ts +22 -0
- package/dist/src/constants.js +35 -0
- package/dist/src/kiro/auth.d.ts +5 -0
- package/dist/src/kiro/auth.js +69 -0
- package/dist/src/kiro/oauth-idc.d.ts +22 -0
- package/dist/src/kiro/oauth-idc.js +99 -0
- package/dist/src/kiro/oauth-social.d.ts +17 -0
- package/dist/src/kiro/oauth-social.js +69 -0
- package/dist/src/plugin/accounts.d.ts +23 -0
- package/dist/src/plugin/accounts.js +265 -0
- package/dist/src/plugin/cli.d.ts +6 -0
- package/dist/src/plugin/cli.js +98 -0
- package/dist/src/plugin/config/index.d.ts +3 -0
- package/dist/src/plugin/config/index.js +2 -0
- package/dist/src/plugin/config/loader.d.ts +7 -0
- package/dist/src/plugin/config/loader.js +143 -0
- package/dist/src/plugin/config/schema.d.ts +68 -0
- package/dist/src/plugin/config/schema.js +44 -0
- package/dist/src/plugin/debug.d.ts +2 -0
- package/dist/src/plugin/debug.js +9 -0
- package/dist/src/plugin/errors.d.ts +17 -0
- package/dist/src/plugin/errors.js +34 -0
- package/dist/src/plugin/logger.d.ts +4 -0
- package/dist/src/plugin/logger.js +17 -0
- package/dist/src/plugin/models.d.ts +3 -0
- package/dist/src/plugin/models.js +14 -0
- package/dist/src/plugin/oauth-parser.d.ts +5 -0
- package/dist/src/plugin/oauth-parser.js +23 -0
- package/dist/src/plugin/quota.d.ts +25 -0
- package/dist/src/plugin/quota.js +175 -0
- package/dist/src/plugin/recovery.d.ts +19 -0
- package/dist/src/plugin/recovery.js +302 -0
- package/dist/src/plugin/refresh-queue.d.ts +14 -0
- package/dist/src/plugin/refresh-queue.js +69 -0
- package/dist/src/plugin/request.d.ts +35 -0
- package/dist/src/plugin/request.js +411 -0
- package/dist/src/plugin/response.d.ts +6 -0
- package/dist/src/plugin/response.js +246 -0
- package/dist/src/plugin/server.d.ts +10 -0
- package/dist/src/plugin/server.js +203 -0
- package/dist/src/plugin/storage.d.ts +5 -0
- package/dist/src/plugin/storage.js +106 -0
- package/dist/src/plugin/streaming.d.ts +12 -0
- package/dist/src/plugin/streaming.js +444 -0
- package/dist/src/plugin/token.d.ts +8 -0
- package/dist/src/plugin/token.js +130 -0
- package/dist/src/plugin/types.d.ts +144 -0
- package/dist/src/plugin/types.js +0 -0
- package/dist/src/plugin/usage.d.ts +28 -0
- package/dist/src/plugin/usage.js +159 -0
- package/dist/src/plugin.d.ts +2 -0
- package/dist/src/plugin.js +341 -0
- package/package.json +57 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { generatePKCE } from '@openauthjs/openauth/pkce';
|
|
2
|
+
import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS } from '../constants';
|
|
3
|
+
export async function authorizeKiroIDC(region) {
|
|
4
|
+
const effectiveRegion = region || KIRO_CONSTANTS.DEFAULT_REGION;
|
|
5
|
+
const pkce = await generatePKCE();
|
|
6
|
+
const verifier = pkce.verifier;
|
|
7
|
+
const ssoOIDCEndpoint = KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT.replace('{{region}}', effectiveRegion);
|
|
8
|
+
const registerResponse = await fetch(`${ssoOIDCEndpoint}/client/register`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'User-Agent': KIRO_CONSTANTS.USER_AGENT,
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
clientName: 'Kiro IDE',
|
|
16
|
+
clientType: 'public',
|
|
17
|
+
scopes: KIRO_AUTH_SERVICE.SCOPES,
|
|
18
|
+
grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'],
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
if (!registerResponse.ok) {
|
|
22
|
+
throw new Error(`Client registration failed: ${registerResponse.status}`);
|
|
23
|
+
}
|
|
24
|
+
const registerData = await registerResponse.json();
|
|
25
|
+
const { clientId, clientSecret } = registerData;
|
|
26
|
+
const deviceAuthResponse = await fetch(`${ssoOIDCEndpoint}/device_authorization`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'User-Agent': KIRO_CONSTANTS.USER_AGENT,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
clientId,
|
|
34
|
+
clientSecret,
|
|
35
|
+
startUrl: KIRO_AUTH_SERVICE.BUILDER_ID_START_URL,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!deviceAuthResponse.ok) {
|
|
39
|
+
throw new Error(`Device authorization failed: ${deviceAuthResponse.status}`);
|
|
40
|
+
}
|
|
41
|
+
const deviceAuthData = await deviceAuthResponse.json();
|
|
42
|
+
const state = Buffer.from(JSON.stringify({
|
|
43
|
+
deviceCode: deviceAuthData.deviceCode,
|
|
44
|
+
region: effectiveRegion,
|
|
45
|
+
clientId,
|
|
46
|
+
clientSecret,
|
|
47
|
+
})).toString('base64url');
|
|
48
|
+
return {
|
|
49
|
+
url: deviceAuthData.verificationUriComplete,
|
|
50
|
+
verifier,
|
|
51
|
+
region: effectiveRegion,
|
|
52
|
+
deviceCode: deviceAuthData.deviceCode,
|
|
53
|
+
userCode: deviceAuthData.userCode,
|
|
54
|
+
clientId,
|
|
55
|
+
clientSecret,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function exchangeKiroIDC(state) {
|
|
59
|
+
const decodedState = JSON.parse(Buffer.from(state, 'base64url').toString('utf-8'));
|
|
60
|
+
const { deviceCode, region, clientId, clientSecret } = decodedState;
|
|
61
|
+
if (!deviceCode || !region || !clientId || !clientSecret) {
|
|
62
|
+
throw new Error('Invalid state parameter');
|
|
63
|
+
}
|
|
64
|
+
const ssoOIDCEndpoint = KIRO_AUTH_SERVICE.SSO_OIDC_ENDPOINT.replace('{{region}}', region);
|
|
65
|
+
const tokenResponse = await fetch(`${ssoOIDCEndpoint}/token`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'User-Agent': KIRO_CONSTANTS.USER_AGENT,
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
clientId,
|
|
73
|
+
clientSecret,
|
|
74
|
+
deviceCode,
|
|
75
|
+
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!tokenResponse.ok) {
|
|
79
|
+
const errorText = await tokenResponse.text();
|
|
80
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status} ${errorText}`);
|
|
81
|
+
}
|
|
82
|
+
const tokenData = await tokenResponse.json();
|
|
83
|
+
if (!tokenData.accessToken || !tokenData.refreshToken) {
|
|
84
|
+
throw new Error('Invalid token response: missing required fields');
|
|
85
|
+
}
|
|
86
|
+
const expiresIn = tokenData.expiresIn || 3600;
|
|
87
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
88
|
+
const email = 'builder-id@aws.amazon.com';
|
|
89
|
+
return {
|
|
90
|
+
refreshToken: tokenData.refreshToken,
|
|
91
|
+
accessToken: tokenData.accessToken,
|
|
92
|
+
expiresAt,
|
|
93
|
+
email,
|
|
94
|
+
clientId,
|
|
95
|
+
clientSecret,
|
|
96
|
+
region,
|
|
97
|
+
authMethod: 'idc',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { KiroRegion } from '../plugin/types';
|
|
2
|
+
export interface KiroSocialAuthorization {
|
|
3
|
+
url: string;
|
|
4
|
+
verifier: string;
|
|
5
|
+
region: KiroRegion;
|
|
6
|
+
}
|
|
7
|
+
export interface KiroSocialTokenResult {
|
|
8
|
+
refreshToken: string;
|
|
9
|
+
accessToken: string;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
email: string;
|
|
12
|
+
profileArn: string;
|
|
13
|
+
region: KiroRegion;
|
|
14
|
+
authMethod: 'social';
|
|
15
|
+
}
|
|
16
|
+
export declare function authorizeKiroSocial(region?: KiroRegion): Promise<KiroSocialAuthorization>;
|
|
17
|
+
export declare function exchangeKiroSocial(code: string, state: string): Promise<KiroSocialTokenResult>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { generatePKCE } from '@openauthjs/openauth/pkce';
|
|
2
|
+
import { KIRO_AUTH_SERVICE, KIRO_CONSTANTS } from '../constants';
|
|
3
|
+
export async function authorizeKiroSocial(region) {
|
|
4
|
+
const effectiveRegion = region || KIRO_CONSTANTS.DEFAULT_REGION;
|
|
5
|
+
const pkce = await generatePKCE();
|
|
6
|
+
const verifier = pkce.verifier;
|
|
7
|
+
const challenge = pkce.challenge;
|
|
8
|
+
const state = Buffer.from(JSON.stringify({
|
|
9
|
+
verifier,
|
|
10
|
+
region: effectiveRegion
|
|
11
|
+
})).toString('base64url');
|
|
12
|
+
const authServiceEndpoint = KIRO_AUTH_SERVICE.ENDPOINT.replace('{{region}}', effectiveRegion);
|
|
13
|
+
const redirectUri = 'http://localhost:8080/callback';
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
idp: 'Google',
|
|
16
|
+
redirect_uri: redirectUri,
|
|
17
|
+
code_challenge: challenge,
|
|
18
|
+
code_challenge_method: 'S256',
|
|
19
|
+
state: state,
|
|
20
|
+
prompt: 'select_account',
|
|
21
|
+
});
|
|
22
|
+
const url = `${authServiceEndpoint}/login?${params.toString()}`;
|
|
23
|
+
return {
|
|
24
|
+
url,
|
|
25
|
+
verifier,
|
|
26
|
+
region: effectiveRegion,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function exchangeKiroSocial(code, state) {
|
|
30
|
+
const decodedState = JSON.parse(Buffer.from(state, 'base64url').toString('utf-8'));
|
|
31
|
+
const { verifier, region } = decodedState;
|
|
32
|
+
if (!verifier || !region) {
|
|
33
|
+
throw new Error('Invalid state parameter');
|
|
34
|
+
}
|
|
35
|
+
const authServiceEndpoint = KIRO_AUTH_SERVICE.ENDPOINT.replace('{{region}}', region);
|
|
36
|
+
const redirectUri = 'http://localhost:8080/callback';
|
|
37
|
+
const tokenResponse = await fetch(`${authServiceEndpoint}/oauth/token`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'User-Agent': KIRO_CONSTANTS.USER_AGENT,
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
code,
|
|
45
|
+
code_verifier: verifier,
|
|
46
|
+
redirect_uri: redirectUri,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
if (!tokenResponse.ok) {
|
|
50
|
+
const errorText = await tokenResponse.text();
|
|
51
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status} ${errorText}`);
|
|
52
|
+
}
|
|
53
|
+
const tokenData = await tokenResponse.json();
|
|
54
|
+
if (!tokenData.accessToken || !tokenData.refreshToken || !tokenData.profileArn) {
|
|
55
|
+
throw new Error('Invalid token response: missing required fields');
|
|
56
|
+
}
|
|
57
|
+
const expiresIn = tokenData.expiresIn || 3600;
|
|
58
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
59
|
+
const email = tokenData.email || 'unknown@kiro.dev';
|
|
60
|
+
return {
|
|
61
|
+
refreshToken: tokenData.refreshToken,
|
|
62
|
+
accessToken: tokenData.accessToken,
|
|
63
|
+
expiresAt,
|
|
64
|
+
email,
|
|
65
|
+
profileArn: tokenData.profileArn,
|
|
66
|
+
region,
|
|
67
|
+
authMethod: 'social',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ManagedAccount, AccountSelectionStrategy, KiroAuthDetails, RefreshParts } from './types';
|
|
2
|
+
export declare function generateAccountId(): string;
|
|
3
|
+
export declare function isAccountAvailable(account: ManagedAccount): boolean;
|
|
4
|
+
export declare function encodeRefreshToken(parts: RefreshParts): string;
|
|
5
|
+
export declare function decodeRefreshToken(encoded: string): RefreshParts;
|
|
6
|
+
export declare class AccountManager {
|
|
7
|
+
private accounts;
|
|
8
|
+
private cursor;
|
|
9
|
+
private strategy;
|
|
10
|
+
constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
|
|
11
|
+
static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
|
|
12
|
+
getCurrentOrNext(): ManagedAccount | null;
|
|
13
|
+
markRateLimited(account: ManagedAccount, retryAfterMs: number): void;
|
|
14
|
+
markUnhealthy(account: ManagedAccount, reason: string, recoveryTime?: number): void;
|
|
15
|
+
markHealthy(account: ManagedAccount): void;
|
|
16
|
+
updateFromAuth(account: ManagedAccount, auth: KiroAuthDetails): void;
|
|
17
|
+
addAccount(account: ManagedAccount): void;
|
|
18
|
+
removeAccount(account: ManagedAccount): void;
|
|
19
|
+
getAccounts(): ManagedAccount[];
|
|
20
|
+
getAccountCount(): number;
|
|
21
|
+
saveToDisk(): Promise<void>;
|
|
22
|
+
toAuthDetails(account: ManagedAccount): KiroAuthDetails;
|
|
23
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { loadAccounts, saveAccounts } from './storage';
|
|
3
|
+
export function generateAccountId() {
|
|
4
|
+
return randomBytes(16).toString('hex');
|
|
5
|
+
}
|
|
6
|
+
export function isAccountAvailable(account) {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
if (!account.isHealthy) {
|
|
9
|
+
if (account.recoveryTime && now < account.recoveryTime) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (account.recoveryTime && now >= account.recoveryTime) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (account.rateLimitResetTime && now < account.rateLimitResetTime) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
export function encodeRefreshToken(parts) {
|
|
23
|
+
const segments = [parts.refreshToken];
|
|
24
|
+
if (parts.profileArn) {
|
|
25
|
+
segments.push(`profileArn:${parts.profileArn}`);
|
|
26
|
+
}
|
|
27
|
+
if (parts.clientId) {
|
|
28
|
+
segments.push(`clientId:${parts.clientId}`);
|
|
29
|
+
}
|
|
30
|
+
if (parts.clientSecret) {
|
|
31
|
+
segments.push(`clientSecret:${parts.clientSecret}`);
|
|
32
|
+
}
|
|
33
|
+
if (parts.authMethod) {
|
|
34
|
+
segments.push(`authMethod:${parts.authMethod}`);
|
|
35
|
+
}
|
|
36
|
+
return segments.join('|');
|
|
37
|
+
}
|
|
38
|
+
export function decodeRefreshToken(encoded) {
|
|
39
|
+
const segments = encoded.split('|');
|
|
40
|
+
const parts = {
|
|
41
|
+
refreshToken: segments[0] || '',
|
|
42
|
+
};
|
|
43
|
+
for (let i = 1; i < segments.length; i++) {
|
|
44
|
+
const segment = segments[i];
|
|
45
|
+
if (!segment)
|
|
46
|
+
continue;
|
|
47
|
+
const colonIndex = segment.indexOf(':');
|
|
48
|
+
if (colonIndex === -1)
|
|
49
|
+
continue;
|
|
50
|
+
const key = segment.substring(0, colonIndex);
|
|
51
|
+
const value = segment.substring(colonIndex + 1);
|
|
52
|
+
if (key === 'profileArn') {
|
|
53
|
+
parts.profileArn = value;
|
|
54
|
+
}
|
|
55
|
+
else if (key === 'clientId') {
|
|
56
|
+
parts.clientId = value;
|
|
57
|
+
}
|
|
58
|
+
else if (key === 'clientSecret') {
|
|
59
|
+
parts.clientSecret = value;
|
|
60
|
+
}
|
|
61
|
+
else if (key === 'authMethod') {
|
|
62
|
+
parts.authMethod = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts;
|
|
66
|
+
}
|
|
67
|
+
export class AccountManager {
|
|
68
|
+
accounts;
|
|
69
|
+
cursor;
|
|
70
|
+
strategy;
|
|
71
|
+
constructor(accounts, strategy = 'sticky') {
|
|
72
|
+
this.accounts = accounts;
|
|
73
|
+
this.cursor = 0;
|
|
74
|
+
this.strategy = strategy;
|
|
75
|
+
}
|
|
76
|
+
static async loadFromDisk(strategy) {
|
|
77
|
+
const storage = await loadAccounts();
|
|
78
|
+
const accounts = storage.accounts.map((meta) => ({
|
|
79
|
+
id: meta.id,
|
|
80
|
+
email: meta.email,
|
|
81
|
+
authMethod: meta.authMethod,
|
|
82
|
+
region: meta.region,
|
|
83
|
+
profileArn: meta.profileArn,
|
|
84
|
+
clientId: meta.clientId,
|
|
85
|
+
clientSecret: meta.clientSecret,
|
|
86
|
+
refreshToken: meta.refreshToken,
|
|
87
|
+
accessToken: meta.accessToken,
|
|
88
|
+
expiresAt: meta.expiresAt,
|
|
89
|
+
rateLimitResetTime: meta.rateLimitResetTime,
|
|
90
|
+
isHealthy: meta.isHealthy,
|
|
91
|
+
unhealthyReason: meta.unhealthyReason,
|
|
92
|
+
recoveryTime: meta.recoveryTime,
|
|
93
|
+
usedCount: meta.usedCount,
|
|
94
|
+
limitCount: meta.limitCount,
|
|
95
|
+
}));
|
|
96
|
+
return new AccountManager(accounts, strategy || 'sticky');
|
|
97
|
+
}
|
|
98
|
+
getCurrentOrNext() {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const availableAccounts = this.accounts.filter((account) => {
|
|
101
|
+
if (!account.isHealthy) {
|
|
102
|
+
if (account.recoveryTime && now >= account.recoveryTime) {
|
|
103
|
+
account.isHealthy = true;
|
|
104
|
+
delete account.unhealthyReason;
|
|
105
|
+
delete account.recoveryTime;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (account.rateLimitResetTime && now < account.rateLimitResetTime) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
if (availableAccounts.length === 0) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (this.strategy === 'sticky') {
|
|
119
|
+
const currentAccount = this.accounts[this.cursor];
|
|
120
|
+
if (currentAccount && isAccountAvailable(currentAccount)) {
|
|
121
|
+
currentAccount.lastUsed = now;
|
|
122
|
+
return currentAccount;
|
|
123
|
+
}
|
|
124
|
+
const nextAvailable = availableAccounts[0];
|
|
125
|
+
if (nextAvailable) {
|
|
126
|
+
this.cursor = this.accounts.indexOf(nextAvailable);
|
|
127
|
+
nextAvailable.lastUsed = now;
|
|
128
|
+
return nextAvailable;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (this.strategy === 'round-robin') {
|
|
133
|
+
const account = availableAccounts[this.cursor % availableAccounts.length];
|
|
134
|
+
if (account) {
|
|
135
|
+
this.cursor = (this.cursor + 1) % availableAccounts.length;
|
|
136
|
+
account.lastUsed = now;
|
|
137
|
+
return account;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
markRateLimited(account, retryAfterMs) {
|
|
144
|
+
const accountIndex = this.accounts.findIndex((a) => a.id === account.id);
|
|
145
|
+
if (accountIndex !== -1) {
|
|
146
|
+
const acc = this.accounts[accountIndex];
|
|
147
|
+
if (acc) {
|
|
148
|
+
acc.rateLimitResetTime = Date.now() + retryAfterMs;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
markUnhealthy(account, reason, recoveryTime) {
|
|
153
|
+
const accountIndex = this.accounts.findIndex((a) => a.id === account.id);
|
|
154
|
+
if (accountIndex !== -1) {
|
|
155
|
+
const acc = this.accounts[accountIndex];
|
|
156
|
+
if (acc) {
|
|
157
|
+
acc.isHealthy = false;
|
|
158
|
+
acc.unhealthyReason = reason;
|
|
159
|
+
if (recoveryTime) {
|
|
160
|
+
acc.recoveryTime = recoveryTime;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
markHealthy(account) {
|
|
166
|
+
const accountIndex = this.accounts.findIndex((a) => a.id === account.id);
|
|
167
|
+
if (accountIndex !== -1) {
|
|
168
|
+
const acc = this.accounts[accountIndex];
|
|
169
|
+
if (acc) {
|
|
170
|
+
acc.isHealthy = true;
|
|
171
|
+
delete acc.unhealthyReason;
|
|
172
|
+
delete acc.recoveryTime;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
updateFromAuth(account, auth) {
|
|
177
|
+
const accountIndex = this.accounts.findIndex((a) => a.id === account.id);
|
|
178
|
+
if (accountIndex !== -1) {
|
|
179
|
+
const acc = this.accounts[accountIndex];
|
|
180
|
+
if (acc) {
|
|
181
|
+
acc.accessToken = auth.access;
|
|
182
|
+
acc.expiresAt = auth.expires;
|
|
183
|
+
acc.lastUsed = Date.now();
|
|
184
|
+
const parts = decodeRefreshToken(auth.refresh);
|
|
185
|
+
acc.refreshToken = parts.refreshToken;
|
|
186
|
+
if (parts.profileArn) {
|
|
187
|
+
acc.profileArn = parts.profileArn;
|
|
188
|
+
}
|
|
189
|
+
if (parts.clientId) {
|
|
190
|
+
acc.clientId = parts.clientId;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
addAccount(account) {
|
|
196
|
+
if (!account.id) {
|
|
197
|
+
account.id = generateAccountId();
|
|
198
|
+
}
|
|
199
|
+
this.accounts.push(account);
|
|
200
|
+
}
|
|
201
|
+
removeAccount(account) {
|
|
202
|
+
const accountIndex = this.accounts.findIndex((a) => a.id === account.id);
|
|
203
|
+
if (accountIndex !== -1) {
|
|
204
|
+
this.accounts.splice(accountIndex, 1);
|
|
205
|
+
if (this.cursor >= this.accounts.length && this.accounts.length > 0) {
|
|
206
|
+
this.cursor = this.accounts.length - 1;
|
|
207
|
+
}
|
|
208
|
+
else if (this.accounts.length === 0) {
|
|
209
|
+
this.cursor = 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
getAccounts() {
|
|
214
|
+
return [...this.accounts];
|
|
215
|
+
}
|
|
216
|
+
getAccountCount() {
|
|
217
|
+
return this.accounts.length;
|
|
218
|
+
}
|
|
219
|
+
async saveToDisk() {
|
|
220
|
+
const metadata = this.accounts.map((account) => ({
|
|
221
|
+
id: account.id,
|
|
222
|
+
email: account.email,
|
|
223
|
+
authMethod: account.authMethod,
|
|
224
|
+
region: account.region,
|
|
225
|
+
profileArn: account.profileArn,
|
|
226
|
+
clientId: account.clientId,
|
|
227
|
+
clientSecret: account.clientSecret,
|
|
228
|
+
refreshToken: account.refreshToken,
|
|
229
|
+
accessToken: account.accessToken,
|
|
230
|
+
expiresAt: account.expiresAt,
|
|
231
|
+
rateLimitResetTime: account.rateLimitResetTime,
|
|
232
|
+
isHealthy: account.isHealthy,
|
|
233
|
+
unhealthyReason: account.unhealthyReason,
|
|
234
|
+
recoveryTime: account.recoveryTime,
|
|
235
|
+
usedCount: account.usedCount,
|
|
236
|
+
limitCount: account.limitCount,
|
|
237
|
+
}));
|
|
238
|
+
const storage = {
|
|
239
|
+
version: 1,
|
|
240
|
+
accounts: metadata,
|
|
241
|
+
activeIndex: this.cursor,
|
|
242
|
+
};
|
|
243
|
+
await saveAccounts(storage);
|
|
244
|
+
}
|
|
245
|
+
toAuthDetails(account) {
|
|
246
|
+
const parts = {
|
|
247
|
+
refreshToken: account.refreshToken,
|
|
248
|
+
profileArn: account.profileArn,
|
|
249
|
+
clientId: account.clientId,
|
|
250
|
+
clientSecret: account.clientSecret,
|
|
251
|
+
authMethod: account.authMethod,
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
refresh: encodeRefreshToken(parts),
|
|
255
|
+
access: account.accessToken,
|
|
256
|
+
expires: account.expiresAt,
|
|
257
|
+
authMethod: account.authMethod,
|
|
258
|
+
region: account.region,
|
|
259
|
+
profileArn: account.profileArn,
|
|
260
|
+
clientId: account.clientId,
|
|
261
|
+
clientSecret: account.clientSecret,
|
|
262
|
+
email: account.email,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { KiroAuthMethod, KiroRegion } from './types';
|
|
2
|
+
export declare function promptAuthMethod(): Promise<KiroAuthMethod>;
|
|
3
|
+
export declare function promptRegion(): Promise<KiroRegion>;
|
|
4
|
+
export declare function promptConfirmation(message: string): Promise<boolean>;
|
|
5
|
+
export declare function displayAuthUrl(url: string): void;
|
|
6
|
+
export declare function cleanup(): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import * as logger from './logger';
|
|
3
|
+
let rl = null;
|
|
4
|
+
function getReadline() {
|
|
5
|
+
if (!rl) {
|
|
6
|
+
rl = createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout,
|
|
9
|
+
});
|
|
10
|
+
rl.on('SIGINT', () => {
|
|
11
|
+
logger.log('Received SIGINT, closing readline');
|
|
12
|
+
closeReadline();
|
|
13
|
+
process.exit(130);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return rl;
|
|
17
|
+
}
|
|
18
|
+
function closeReadline() {
|
|
19
|
+
if (rl) {
|
|
20
|
+
rl.close();
|
|
21
|
+
rl = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function question(prompt) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const readline = getReadline();
|
|
27
|
+
readline.question(prompt, (answer) => {
|
|
28
|
+
resolve(answer.trim());
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function clearLine() {
|
|
33
|
+
if (process.stdout.isTTY) {
|
|
34
|
+
process.stdout.clearLine(0);
|
|
35
|
+
process.stdout.cursorTo(0);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function promptAuthMethod() {
|
|
39
|
+
console.log('\nSelect authentication method:');
|
|
40
|
+
console.log(' 1. Social (GitHub, Google, etc.)');
|
|
41
|
+
console.log(' 2. IDC (Identity Center)');
|
|
42
|
+
while (true) {
|
|
43
|
+
const answer = await question('\nEnter your choice (1 or 2): ');
|
|
44
|
+
if (answer === '1' || answer.toLowerCase() === 'social') {
|
|
45
|
+
clearLine();
|
|
46
|
+
return 'social';
|
|
47
|
+
}
|
|
48
|
+
if (answer === '2' || answer.toLowerCase() === 'idc') {
|
|
49
|
+
clearLine();
|
|
50
|
+
return 'idc';
|
|
51
|
+
}
|
|
52
|
+
console.log('Invalid choice. Please enter 1 or 2.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function promptRegion() {
|
|
56
|
+
console.log('\nSelect AWS region:');
|
|
57
|
+
console.log(' 1. us-east-1 (default)');
|
|
58
|
+
console.log(' 2. us-west-2');
|
|
59
|
+
while (true) {
|
|
60
|
+
const answer = await question('\nEnter your choice (1 or 2, default: 1): ');
|
|
61
|
+
if (!answer || answer === '1' || answer.toLowerCase() === 'us-east-1') {
|
|
62
|
+
clearLine();
|
|
63
|
+
return 'us-east-1';
|
|
64
|
+
}
|
|
65
|
+
if (answer === '2' || answer.toLowerCase() === 'us-west-2') {
|
|
66
|
+
clearLine();
|
|
67
|
+
return 'us-west-2';
|
|
68
|
+
}
|
|
69
|
+
console.log('Invalid choice. Please enter 1 or 2.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function promptConfirmation(message) {
|
|
73
|
+
while (true) {
|
|
74
|
+
const answer = await question(`${message} (y/n): `);
|
|
75
|
+
const lower = answer.toLowerCase();
|
|
76
|
+
if (lower === 'y' || lower === 'yes') {
|
|
77
|
+
clearLine();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (lower === 'n' || lower === 'no') {
|
|
81
|
+
clearLine();
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
console.log('Invalid input. Please enter y or n.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function displayAuthUrl(url) {
|
|
88
|
+
console.log('\n' + '='.repeat(70));
|
|
89
|
+
console.log('Authentication Required');
|
|
90
|
+
console.log('='.repeat(70));
|
|
91
|
+
console.log('\nPlease open the following URL in your browser to authenticate:\n');
|
|
92
|
+
console.log(` ${url}\n`);
|
|
93
|
+
console.log('Waiting for authentication to complete...');
|
|
94
|
+
console.log('='.repeat(70) + '\n');
|
|
95
|
+
}
|
|
96
|
+
export function cleanup() {
|
|
97
|
+
closeReadline();
|
|
98
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type KiroConfig } from "./schema";
|
|
2
|
+
export declare function getUserConfigPath(): string;
|
|
3
|
+
export declare function getProjectConfigPath(directory: string): string;
|
|
4
|
+
export declare function ensureConfigTemplate(): Promise<void>;
|
|
5
|
+
export declare function loadConfig(directory: string): KiroConfig;
|
|
6
|
+
export declare function configExists(path: string): boolean;
|
|
7
|
+
export declare function getDefaultLogsDir(): string;
|